mirror of
https://github.com/penpot/penpot.git
synced 2026-01-03 11:58:46 -05:00
Compare commits
753 Commits
1.6.2-alph
...
1.10.2-bet
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a2d3616171 | ||
|
|
a83e37493a | ||
|
|
384f0a05c6 | ||
|
|
a3016b8400 | ||
|
|
64c456678b | ||
|
|
16ed09a303 | ||
|
|
f8cecfd61f | ||
|
|
8a2a1d6d70 | ||
|
|
4ad34ab5c8 | ||
|
|
33c7847dfc | ||
|
|
7a04f15710 | ||
|
|
b8043a2432 | ||
|
|
ed5de525aa | ||
|
|
8105d9388b | ||
|
|
8151dcc05f | ||
|
|
25b1c5fe90 | ||
|
|
ea218839e4 | ||
|
|
4c18a1881b | ||
|
|
0bdbbd35e3 | ||
|
|
401afe7c1a | ||
|
|
c5adeecd90 | ||
|
|
da6c62414b | ||
|
|
6650fe863f | ||
|
|
76c00c42b5 | ||
|
|
f8609419a1 | ||
|
|
250e79eda1 | ||
|
|
f7401daeae | ||
|
|
7390e372e0 | ||
|
|
239c521ad9 | ||
|
|
77b4f09cfb | ||
|
|
bb178af278 | ||
|
|
3c39661174 | ||
|
|
1fffc1e828 | ||
|
|
ed50cd1fa8 | ||
|
|
ef6a02e8ef | ||
|
|
e7003dde83 | ||
|
|
bf2a393fd3 | ||
|
|
bb2cfd52f4 | ||
|
|
6a6f88c6ef | ||
|
|
0a2b1a4fbe | ||
|
|
5fd48c9e98 | ||
|
|
022d32cd44 | ||
|
|
af10cf71db | ||
|
|
1bf1de8ce8 | ||
|
|
b80ddfa580 | ||
|
|
aa276ab308 | ||
|
|
f50943d470 | ||
|
|
959c998664 | ||
|
|
b6b6b6043c | ||
|
|
8e0807d502 | ||
|
|
78d027b25e | ||
|
|
503f0bee69 | ||
|
|
50d756b189 | ||
|
|
7c3d71e572 | ||
|
|
bf895d26b0 | ||
|
|
5530e8581a | ||
|
|
f913816d87 | ||
|
|
3d59d31b0a | ||
|
|
9a66f26bd9 | ||
|
|
d5b6605ce8 | ||
|
|
38e5184be4 | ||
|
|
369ec9f814 | ||
|
|
620b454c49 | ||
|
|
2e5040e65d | ||
|
|
71fe7ef125 | ||
|
|
e0e8fd7ddc | ||
|
|
01b4b4933e | ||
|
|
fced22bc60 | ||
|
|
898ae64a57 | ||
|
|
8d50852cbe | ||
|
|
a11c7b10ac | ||
|
|
fe9033b8be | ||
|
|
e26f9e4a71 | ||
|
|
c477328da4 | ||
|
|
214c64c49e | ||
|
|
bce0e9194c | ||
|
|
a0f98e3823 | ||
|
|
bff6768adf | ||
|
|
8ce2eb448c | ||
|
|
7c5d00f8a4 | ||
|
|
30cd499014 | ||
|
|
99d173789e | ||
|
|
ae72db8129 | ||
|
|
9437cc1806 | ||
|
|
0e76aa0265 | ||
|
|
756e654d32 | ||
|
|
78d1c57b7c | ||
|
|
bb27405e8f | ||
|
|
0cfc46b417 | ||
|
|
63959b4b22 | ||
|
|
66d086892f | ||
|
|
b878570b14 | ||
|
|
b059610d16 | ||
|
|
972aa7f4e3 | ||
|
|
797c1421da | ||
|
|
e65cbcba65 | ||
|
|
6d96dd3818 | ||
|
|
16db31c53c | ||
|
|
c72138d15a | ||
|
|
6d28a9ad58 | ||
|
|
75c8d97a6e | ||
|
|
c35f53af89 | ||
|
|
55784f64b8 | ||
|
|
a7241d4128 | ||
|
|
1573d794b9 | ||
|
|
bc725800ed | ||
|
|
007728819b | ||
|
|
f32f13069f | ||
|
|
5ec73da17f | ||
|
|
5fd3689333 | ||
|
|
cca1431012 | ||
|
|
7ba9558a7a | ||
|
|
c65e8b4a5e | ||
|
|
eed75bcbda | ||
|
|
1af4325e8f | ||
|
|
5e6719e22e | ||
|
|
a4bbfe3c79 | ||
|
|
63f42fc8bb | ||
|
|
5b9bcf8b1d | ||
|
|
f02bc82525 | ||
|
|
92f89c6cc1 | ||
|
|
a1908be982 | ||
|
|
f08894629d | ||
|
|
e46f11e6f8 | ||
|
|
df6234ea28 | ||
|
|
21cdf8b0ae | ||
|
|
192fb07ef1 | ||
|
|
226216d111 | ||
|
|
bed2deb683 | ||
|
|
6e327b69f5 | ||
|
|
1d3c8e867e | ||
|
|
d0f761172a | ||
|
|
fd6a8aec71 | ||
|
|
e00e501605 | ||
|
|
81a42ef1df | ||
|
|
ee5eb2abc5 | ||
|
|
0ed14f0288 | ||
|
|
c55f740978 | ||
|
|
38952b6734 | ||
|
|
925058467f | ||
|
|
e5afeccadf | ||
|
|
ad18604552 | ||
|
|
d2d506dbf0 | ||
|
|
2833d3126f | ||
|
|
950367b055 | ||
|
|
703859ac75 | ||
|
|
4bf5434e8f | ||
|
|
350c44f56f | ||
|
|
679c630a4d | ||
|
|
dbbb0a4a3d | ||
|
|
0ca7d074ac | ||
|
|
65894bf582 | ||
|
|
8eacf738c2 | ||
|
|
1b69eda43e | ||
|
|
09d1c958ce | ||
|
|
589d16bc37 | ||
|
|
a6dfa6bbbd | ||
|
|
b2721305c5 | ||
|
|
24b3404876 | ||
|
|
92ca1e4873 | ||
|
|
59d44c41e4 | ||
|
|
08a503f160 | ||
|
|
e0e68835ef | ||
|
|
734287b66d | ||
|
|
ddc9d30a3e | ||
|
|
890bf9eced | ||
|
|
b8677b2b9a | ||
|
|
1f5e974cfc | ||
|
|
54e7e44df1 | ||
|
|
9736810f87 | ||
|
|
5547383434 | ||
|
|
85f8e77928 | ||
|
|
6918216b86 | ||
|
|
efd2ad8f8b | ||
|
|
5a8ce52105 | ||
|
|
cbee65671c | ||
|
|
75a7ce24bf | ||
|
|
013f56347d | ||
|
|
a052bfd2fa | ||
|
|
4b1fa2589e | ||
|
|
1a61c855ca | ||
|
|
0159eea526 | ||
|
|
f3bb5c55f5 | ||
|
|
9ecbddc18c | ||
|
|
d36bf188ae | ||
|
|
b8cddbca88 | ||
|
|
ee9b7166a6 | ||
|
|
9c1c755836 | ||
|
|
6722ca41bf | ||
|
|
9586d478ad | ||
|
|
77cf4a5332 | ||
|
|
7199ab7cbe | ||
|
|
5de2ff40d8 | ||
|
|
790d532cee | ||
|
|
9f03e353c7 | ||
|
|
68e3d53cb7 | ||
|
|
f9082e18e2 | ||
|
|
02d31a7947 | ||
|
|
3e3faf6576 | ||
|
|
bee1db135f | ||
|
|
09d39ca425 | ||
|
|
d58b6e5117 | ||
|
|
f0cf3e6411 | ||
|
|
b64d5ef357 | ||
|
|
2eccf77986 | ||
|
|
0b8b766b62 | ||
|
|
8d634a79c8 | ||
|
|
4b9e7fdb15 | ||
|
|
165a84534a | ||
|
|
fe4cab3a9e | ||
|
|
9e5166d991 | ||
|
|
48e78125e8 | ||
|
|
3fb3a92a8f | ||
|
|
8dba55d5cb | ||
|
|
045a5156d1 | ||
|
|
8a162e39d5 | ||
|
|
695788df0e | ||
|
|
4df96b03eb | ||
|
|
49c2cb985c | ||
|
|
a189dc8243 | ||
|
|
ff8db0cd77 | ||
|
|
eff3e4015b | ||
|
|
9ad43e13da | ||
|
|
1bd3a792da | ||
|
|
75f8e473a5 | ||
|
|
8c25ee7796 | ||
|
|
c3520cf606 | ||
|
|
75d2d97d8e | ||
|
|
778a542e1c | ||
|
|
74f3d551f2 | ||
|
|
fcc7b6791e | ||
|
|
56e2db22eb | ||
|
|
c56f024a86 | ||
|
|
6fd35ae5d9 | ||
|
|
1db2895606 | ||
|
|
df60ee06a1 | ||
|
|
0b4b2d3814 | ||
|
|
9f08153a85 | ||
|
|
5031700af6 | ||
|
|
57245dd77e | ||
|
|
4697a1904a | ||
|
|
ed380c86eb | ||
|
|
02deecf54b | ||
|
|
133c0312be | ||
|
|
45e501ce02 | ||
|
|
87dfa8c7fc | ||
|
|
edefb588b6 | ||
|
|
8ce8b85089 | ||
|
|
54c409a71c | ||
|
|
2f8960d34f | ||
|
|
20036bd72b | ||
|
|
38a84d4598 | ||
|
|
bc1372c2f9 | ||
|
|
c241100886 | ||
|
|
fea2d91a63 | ||
|
|
f2c4aa852d | ||
|
|
f8d09917a5 | ||
|
|
bbdf1152c1 | ||
|
|
f208731746 | ||
|
|
0516cfa296 | ||
|
|
157e8413fb | ||
|
|
4708af3b91 | ||
|
|
bee47d7fda | ||
|
|
d246db7be8 | ||
|
|
02025bc70a | ||
|
|
4275298f19 | ||
|
|
f0a02e4734 | ||
|
|
59464469c2 | ||
|
|
4d880a0d77 | ||
|
|
26b28e2364 | ||
|
|
835b597af5 | ||
|
|
c44d22ccf5 | ||
|
|
a11cda91de | ||
|
|
cfbbb85254 | ||
|
|
8a0bba3c7a | ||
|
|
da1135c80f | ||
|
|
7fcf481243 | ||
|
|
06e54a17c0 | ||
|
|
1fe23ff732 | ||
|
|
39278b47dd | ||
|
|
bff0030f2b | ||
|
|
b4b2f91363 | ||
|
|
c7252a950b | ||
|
|
e48b01fd18 | ||
|
|
ef2337f6d8 | ||
|
|
13d83cb0d1 | ||
|
|
033355395f | ||
|
|
6c332b949b | ||
|
|
0711438433 | ||
|
|
ee6350189f | ||
|
|
46189c0ff1 | ||
|
|
45d55e87eb | ||
|
|
8a158146cd | ||
|
|
1a859fc639 | ||
|
|
43518c6cfe | ||
|
|
7bfb7b6da0 | ||
|
|
c0474b206e | ||
|
|
fe6623b342 | ||
|
|
de8220245c | ||
|
|
562f0d9872 | ||
|
|
ed89f858e1 | ||
|
|
9527b2c456 | ||
|
|
5da2e5e7b7 | ||
|
|
e55e5aa168 | ||
|
|
22b45266bf | ||
|
|
b280b5a517 | ||
|
|
60cb358cce | ||
|
|
f03a74abc7 | ||
|
|
34885b64bd | ||
|
|
f3bfa4e587 | ||
|
|
3136ce7dc2 | ||
|
|
15a050517b | ||
|
|
85a1c61880 | ||
|
|
15991d0226 | ||
|
|
413bc41695 | ||
|
|
36137808f0 | ||
|
|
12c1852297 | ||
|
|
95e3c3eafc | ||
|
|
c458fa6441 | ||
|
|
66c1e386ce | ||
|
|
59e203fd52 | ||
|
|
7e0c097f23 | ||
|
|
926fa483b9 | ||
|
|
2ebc92a167 | ||
|
|
eb511757db | ||
|
|
b5b97f7626 | ||
|
|
ba0f7416bb | ||
|
|
f6e18de6af | ||
|
|
320a4552bc | ||
|
|
203473c965 | ||
|
|
255177d12b | ||
|
|
290bf00b2d | ||
|
|
8464e6a822 | ||
|
|
8af46ac7fc | ||
|
|
daeaf14032 | ||
|
|
bd52a7c926 | ||
|
|
c8c43de510 | ||
|
|
bb49071088 | ||
|
|
7a523a9d89 | ||
|
|
885d7de11b | ||
|
|
f44675a1e4 | ||
|
|
ce912c7430 | ||
|
|
e9fdd74a99 | ||
|
|
df8269bc7f | ||
|
|
23e4fa82c8 | ||
|
|
9bea604a46 | ||
|
|
119fbd114d | ||
|
|
1b6e6ec2e4 | ||
|
|
2dfa4f9ec9 | ||
|
|
3cd3e89679 | ||
|
|
c3be1c870d | ||
|
|
6b571fd2bb | ||
|
|
92df7abcf0 | ||
|
|
498d1570ce | ||
|
|
e587179359 | ||
|
|
c9985121c4 | ||
|
|
e768600df3 | ||
|
|
3dffb9c8a0 | ||
|
|
eb40297a35 | ||
|
|
837985ccc5 | ||
|
|
1def4b0f0c | ||
|
|
4c430cedf5 | ||
|
|
18d9212253 | ||
|
|
36314691f1 | ||
|
|
24da25f0f7 | ||
|
|
84ba8e6dde | ||
|
|
c6fe035939 | ||
|
|
be9073f0b7 | ||
|
|
ac6c07b771 | ||
|
|
c8102f4bff | ||
|
|
df1fcd5e22 | ||
|
|
de87da9c91 | ||
|
|
3532263af4 | ||
|
|
a9cf4dad82 | ||
|
|
1de1eb6b9b | ||
|
|
f6742d1bbf | ||
|
|
a377c602cc | ||
|
|
58f0ad999c | ||
|
|
f612d35daf | ||
|
|
7d202cb492 | ||
|
|
39bb7f209d | ||
|
|
bbd38a7e47 | ||
|
|
d8b2cc7e1b | ||
|
|
09b328167c | ||
|
|
4439ef07b6 | ||
|
|
f8491e9631 | ||
|
|
63259b3f92 | ||
|
|
10db35eab4 | ||
|
|
0fa79c7a46 | ||
|
|
e20f557bd6 | ||
|
|
25d8d76524 | ||
|
|
cc0f99333f | ||
|
|
982aa874f2 | ||
|
|
2a70964dce | ||
|
|
3051a185e5 | ||
|
|
5e788fff99 | ||
|
|
326c52604b | ||
|
|
e7d1647769 | ||
|
|
1e35116d8f | ||
|
|
35ca3ec895 | ||
|
|
3435684c87 | ||
|
|
7c30cccc97 | ||
|
|
4194abe4f2 | ||
|
|
0b698576da | ||
|
|
3fbd73129e | ||
|
|
bbd6d171be | ||
|
|
f7929bbf93 | ||
|
|
29cd8530a3 | ||
|
|
574387acac | ||
|
|
6a1ab4d73c | ||
|
|
29e0c32679 | ||
|
|
db7fe023c6 | ||
|
|
bed702d8de | ||
|
|
ccf3d7a285 | ||
|
|
e4f755416d | ||
|
|
4d5b0731be | ||
|
|
fde6ea1c83 | ||
|
|
7a94a2f087 | ||
|
|
97b8f742dd | ||
|
|
06733ea7cd | ||
|
|
efa5120fac | ||
|
|
80ab6bbda2 | ||
|
|
53620b9f1b | ||
|
|
259b405526 | ||
|
|
c6fe19c321 | ||
|
|
9d545004cb | ||
|
|
7fe419ecb0 | ||
|
|
55ddf9cc38 | ||
|
|
38292bcda7 | ||
|
|
08062e8ce8 | ||
|
|
bff35de39f | ||
|
|
394e6b08ad | ||
|
|
d61a86cad1 | ||
|
|
43198eb263 | ||
|
|
8493e51070 | ||
|
|
07eeb76a5f | ||
|
|
6ee6a03e4a | ||
|
|
8e3eb98789 | ||
|
|
c5b23816e9 | ||
|
|
0a3cd4f8e4 | ||
|
|
7882dead81 | ||
|
|
44f96dd6a3 | ||
|
|
a442afd8d2 | ||
|
|
bdbc57b926 | ||
|
|
9ed53ba064 | ||
|
|
9d372301ed | ||
|
|
b483513fa8 | ||
|
|
578c561473 | ||
|
|
f6134a6bd3 | ||
|
|
fb59d5d268 | ||
|
|
2758b6ffd9 | ||
|
|
fa99dea8fe | ||
|
|
6ced56301c | ||
|
|
008134fde8 | ||
|
|
3ed593e4b6 | ||
|
|
1fc5182979 | ||
|
|
9ebafddac2 | ||
|
|
26467187c4 | ||
|
|
69e256ab86 | ||
|
|
b4b12e68bf | ||
|
|
768216d9bc | ||
|
|
f29d54ad0d | ||
|
|
946309a485 | ||
|
|
7c98336148 | ||
|
|
455b0efa71 | ||
|
|
05cf14846c | ||
|
|
9ddcb036cf | ||
|
|
185e06ed79 | ||
|
|
17ae6bf89d | ||
|
|
7efc1a0366 | ||
|
|
899dc5b680 | ||
|
|
5126c85623 | ||
|
|
9ec23ceed6 | ||
|
|
a6d156438f | ||
|
|
23e4915d60 | ||
|
|
5ecfe05f3b | ||
|
|
d35192d50f | ||
|
|
e2f9ce0fc5 | ||
|
|
8f55741c3e | ||
|
|
b7dc6d6cce | ||
|
|
8fb8a5d89a | ||
|
|
dc22c2763e | ||
|
|
a77863d3c5 | ||
|
|
0c8e0ed3dd | ||
|
|
fb7751eaae | ||
|
|
56795f8d26 | ||
|
|
741d3050ad | ||
|
|
0ff0fd7ced | ||
|
|
b9b287d3b2 | ||
|
|
dc089ba84a | ||
|
|
55d2acdf13 | ||
|
|
3a64efd136 | ||
|
|
4e439792ec | ||
|
|
895889d27a | ||
|
|
d2777f5915 | ||
|
|
9b878bd1cc | ||
|
|
73a08fd119 | ||
|
|
7b9b3dabbe | ||
|
|
163215d5c9 | ||
|
|
7cc9fa6d30 | ||
|
|
2d38d7af82 | ||
|
|
26e9f652b6 | ||
|
|
19afc2274a | ||
|
|
16fcc60a59 | ||
|
|
1b44fe8fec | ||
|
|
028e1d63a3 | ||
|
|
e1e825f350 | ||
|
|
65a4aff5fc | ||
|
|
8f95f2ba12 | ||
|
|
991e0d5e5b | ||
|
|
84cf63d1ba | ||
|
|
60009476d6 | ||
|
|
1894fc7cfa | ||
|
|
c9c24c3464 | ||
|
|
cb731176eb | ||
|
|
1ee14a76f4 | ||
|
|
e9945235ed | ||
|
|
60b29a3bf5 | ||
|
|
3eb209b602 | ||
|
|
d1cce44616 | ||
|
|
c02638e10e | ||
|
|
ddbdc2a27f | ||
|
|
f312c122ca | ||
|
|
1d6a421388 | ||
|
|
6e40e4e994 | ||
|
|
2149576289 | ||
|
|
96891a5e5c | ||
|
|
2771cab71a | ||
|
|
d0ab813520 | ||
|
|
1b1c0ff9e4 | ||
|
|
083696a899 | ||
|
|
1376c26def | ||
|
|
e13cfad9da | ||
|
|
723cb3b546 | ||
|
|
dac7a6497f | ||
|
|
ea8bc687c0 | ||
|
|
c98958053c | ||
|
|
5f1ed511ea | ||
|
|
61b7c279d6 | ||
|
|
4c84b18bb6 | ||
|
|
484eb3a7c4 | ||
|
|
f73880e565 | ||
|
|
36cca0d871 | ||
|
|
08d2dbc9bb | ||
|
|
ce13902680 | ||
|
|
e818170eec | ||
|
|
91b6a0bf69 | ||
|
|
85a6edb1fd | ||
|
|
7d14122746 | ||
|
|
aa14d9626f | ||
|
|
98f072619f | ||
|
|
150427cd39 | ||
|
|
3295685938 | ||
|
|
ca4ce569e7 | ||
|
|
ca9edf2bc9 | ||
|
|
be387ad892 | ||
|
|
9b9959da9a | ||
|
|
234a698538 | ||
|
|
fbf1c10077 | ||
|
|
4d0dcc5876 | ||
|
|
4e909dc369 | ||
|
|
ac1d0a5502 | ||
|
|
d89a4a1218 | ||
|
|
71759386c5 | ||
|
|
fdbf94f415 | ||
|
|
ad4115acc8 | ||
|
|
432a8f2338 | ||
|
|
b994363972 | ||
|
|
2a81321ead | ||
|
|
dd7f5fd228 | ||
|
|
047791413e | ||
|
|
358fa7b20f | ||
|
|
c937ccc92b | ||
|
|
e796c3dfba | ||
|
|
0f3e4c289c | ||
|
|
e0846ce00e | ||
|
|
30e77556db | ||
|
|
3e4e54870b | ||
|
|
e90185b553 | ||
|
|
4a82c14808 | ||
|
|
80371233c9 | ||
|
|
09314c8926 | ||
|
|
0e67e0d87e | ||
|
|
c21ad48370 | ||
|
|
9e3ba85b72 | ||
|
|
c82d936e96 | ||
|
|
7b4603e33e | ||
|
|
84a7ab8568 | ||
|
|
beaea73276 | ||
|
|
ef1c1d8ced | ||
|
|
91425050e4 | ||
|
|
41d05d6de0 | ||
|
|
376d0663c2 | ||
|
|
231a133f23 | ||
|
|
eacc945254 | ||
|
|
16b5bb595c | ||
|
|
a1ad6ca289 | ||
|
|
a8523f41b3 | ||
|
|
1d6905cb25 | ||
|
|
a548bd7ffd | ||
|
|
46e0151c28 | ||
|
|
23b315c58f | ||
|
|
ac37f903d4 | ||
|
|
5572c0798f | ||
|
|
cb5e300534 | ||
|
|
50e0284084 | ||
|
|
e08788190d | ||
|
|
44441ae928 | ||
|
|
e42e1e8751 | ||
|
|
ae4b743ea4 | ||
|
|
370b6bb2f2 | ||
|
|
796141f2b8 | ||
|
|
2711181e19 | ||
|
|
2cd7f0f74c | ||
|
|
96e7910cf9 | ||
|
|
4683d959a5 | ||
|
|
9300adf374 | ||
|
|
5c9ec92cc5 | ||
|
|
76e2309778 | ||
|
|
9fc633080a | ||
|
|
8952cb4e00 | ||
|
|
d6e009ce78 | ||
|
|
a106c728ba | ||
|
|
5cddc9836f | ||
|
|
2728fa2b8d | ||
|
|
2293253558 | ||
|
|
ee7248204f | ||
|
|
0c97a44a2a | ||
|
|
0c49ed1fec | ||
|
|
dd15bf7328 | ||
|
|
3aa5fda695 | ||
|
|
e880d94f51 | ||
|
|
0647fa832a | ||
|
|
4af83eadc4 | ||
|
|
cc2c249a07 | ||
|
|
152bcf451a | ||
|
|
83879fb931 | ||
|
|
8d703a3fb4 | ||
|
|
022d57ef42 | ||
|
|
4928f875b3 | ||
|
|
840430c189 | ||
|
|
024cc88738 | ||
|
|
eee0cf569e | ||
|
|
371c78b1d3 | ||
|
|
6988ae83c9 | ||
|
|
f95705d2d6 | ||
|
|
ff3caec36c | ||
|
|
4c4dac8e90 | ||
|
|
beaa62c9a9 | ||
|
|
69fe8bc9b5 | ||
|
|
092a973f9a | ||
|
|
55b0f6e950 | ||
|
|
b9df489962 | ||
|
|
144127224c | ||
|
|
2202f90d74 | ||
|
|
860e0227af | ||
|
|
c4b4976be0 | ||
|
|
a2b0305162 | ||
|
|
6404907699 | ||
|
|
d4b02e36a7 | ||
|
|
71c4145ea2 | ||
|
|
075f0a1bb0 | ||
|
|
d80bd3661d | ||
|
|
44f4441372 | ||
|
|
782e060448 | ||
|
|
8c223b9fb8 | ||
|
|
1232f93f1a | ||
|
|
8f3c5b5cea | ||
|
|
c4d3023fd3 | ||
|
|
a97c7cada4 | ||
|
|
5b0cd974ac | ||
|
|
bb5804cde3 | ||
|
|
7819757759 | ||
|
|
b861e261ed | ||
|
|
17b32d6518 | ||
|
|
d2359046c4 | ||
|
|
8a700170b0 | ||
|
|
8c68e29bf3 | ||
|
|
1a81631886 | ||
|
|
634fe2c458 | ||
|
|
6cc8fca506 | ||
|
|
053d46144e | ||
|
|
b2e7bb6be1 | ||
|
|
31689cd947 | ||
|
|
d855b930c5 | ||
|
|
61545ea13e | ||
|
|
21aa23e7f5 | ||
|
|
f197124ee5 | ||
|
|
b76fef1e44 | ||
|
|
9f36f4fbe7 | ||
|
|
a76bf1d0b2 | ||
|
|
6cbbfa6499 | ||
|
|
bf5f845789 | ||
|
|
d7eec3b92b | ||
|
|
bae709df5b | ||
|
|
ba33de815f | ||
|
|
1b495ebad1 | ||
|
|
4e0289b341 | ||
|
|
866d95149e | ||
|
|
e9bbe9fca0 | ||
|
|
8da0e9adb2 | ||
|
|
f0e78f693f | ||
|
|
9333ed5be4 | ||
|
|
a244fbee4d | ||
|
|
9bc2f7dce4 | ||
|
|
056fce9187 | ||
|
|
9f034c7e7e | ||
|
|
2704258dba | ||
|
|
3d5caf18e3 | ||
|
|
e45f7598db | ||
|
|
09b72588d8 | ||
|
|
a0f80e740e | ||
|
|
a6de4e3742 | ||
|
|
2d6a375afc | ||
|
|
585e5d0199 | ||
|
|
fcb4cb38a9 | ||
|
|
de5e8f8e57 | ||
|
|
11f360bdab | ||
|
|
ebc79c278b | ||
|
|
b2fef7b7a8 | ||
|
|
71524fe649 | ||
|
|
55d2768807 | ||
|
|
3c7dda02c6 | ||
|
|
6ed182002b | ||
|
|
ee1738c9d4 | ||
|
|
068c94da4e | ||
|
|
2ec769981a | ||
|
|
548664f6ce | ||
|
|
9d54f71dbb | ||
|
|
d102144746 | ||
|
|
3d7a3f27d5 | ||
|
|
46448bc5c7 | ||
|
|
6a2e45988f | ||
|
|
53cb36dd8a | ||
|
|
9cda361523 | ||
|
|
1a70071405 | ||
|
|
b648fb7446 | ||
|
|
aaef0777b0 | ||
|
|
68d287ed82 | ||
|
|
641e4080bc | ||
|
|
a80120278e | ||
|
|
74756db7e6 | ||
|
|
6489ad4114 | ||
|
|
6e18bc9e04 | ||
|
|
febaec1b1e | ||
|
|
806dc78d2b | ||
|
|
7d80a5a7f7 | ||
|
|
a9e8115088 | ||
|
|
6a68e9c118 | ||
|
|
944e7c6e3d | ||
|
|
3094fe2855 | ||
|
|
deb0ee3d29 | ||
|
|
23076727c7 |
@@ -9,7 +9,7 @@ jobs:
|
||||
# CircleCI maintains a library of pre-built images
|
||||
# documented at https://circleci.com/docs/2.0/circleci-images/
|
||||
# - image: circleci/postgres:9.4
|
||||
- image: circleci/postgres:13.1-ram
|
||||
- image: circleci/postgres:13.3-ram
|
||||
environment:
|
||||
POSTGRES_USER: penpot_test
|
||||
POSTGRES_PASSWORD: penpot_test
|
||||
@@ -29,21 +29,36 @@ jobs:
|
||||
# Download and cache dependencies
|
||||
- restore_cache:
|
||||
keys:
|
||||
- v1-dependencies-{{ checksum "backend/deps.edn" }}-{{ checksum "frontend/deps.edn"}}
|
||||
- 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 lint
|
||||
- run:
|
||||
working_directory: "./backend"
|
||||
name: backend lint
|
||||
command: "clj-kondo --lint src/"
|
||||
name: common lint
|
||||
working_directory: "./common"
|
||||
command: |
|
||||
clj-kondo --version
|
||||
clj-kondo --parallel --lint src/
|
||||
|
||||
# run test
|
||||
- run:
|
||||
name: frontend lint
|
||||
working_directory: "./frontend"
|
||||
command: |
|
||||
clj-kondo --version
|
||||
clj-kondo --parallel --lint src/
|
||||
|
||||
- run:
|
||||
name: backend lint
|
||||
working_directory: "./backend"
|
||||
command: |
|
||||
clj-kondo --version
|
||||
clj-kondo --parallel --lint src/
|
||||
|
||||
# run backend test
|
||||
- run:
|
||||
name: backend test
|
||||
command: "clojure -M:dev:tests"
|
||||
working_directory: "./backend"
|
||||
command: "clojure -X:dev:test"
|
||||
environment:
|
||||
PENPOT_TEST_DATABASE_URI: "postgresql://localhost/penpot_test"
|
||||
PENPOT_TEST_DATABASE_USERNAME: penpot_test
|
||||
@@ -51,11 +66,26 @@ jobs:
|
||||
PENPOT_TEST_REDIS_URI: "redis://localhost/1"
|
||||
|
||||
- run:
|
||||
working_directory: "./frontend"
|
||||
name: frontend tests
|
||||
working_directory: "./frontend"
|
||||
command: |
|
||||
yarn install
|
||||
npx shadow-cljs compile tests
|
||||
clojure -M:dev:shadow-cljs compile test
|
||||
node target/tests.js
|
||||
|
||||
environment:
|
||||
JAVA_HOME: /usr/lib/jvm/openjdk16
|
||||
PATH: /usr/local/nodejs/bin/:/usr/local/bin:/bin:/usr/bin:/usr/lib/jvm/openjdk16/bin
|
||||
|
||||
- run:
|
||||
working_directory: "./common"
|
||||
name: common tests
|
||||
command: |
|
||||
yarn install
|
||||
clojure -M:dev:shadow-cljs compile test
|
||||
node target/tests.js
|
||||
clojure -X:dev:test
|
||||
|
||||
environment:
|
||||
JAVA_HOME: /usr/lib/jvm/openjdk16
|
||||
PATH: /usr/local/nodejs/bin/:/usr/local/bin:/bin:/usr/bin:/usr/lib/jvm/openjdk16/bin
|
||||
@@ -63,5 +93,5 @@ jobs:
|
||||
- save_cache:
|
||||
paths:
|
||||
- ~/.m2
|
||||
key: v1-dependencies-{{ checksum "backend/deps.edn" }}-{{ checksum "frontend/deps.edn"}}
|
||||
key: v1-dependencies-{{ checksum "backend/deps.edn" }}-{{ checksum "frontend/deps.edn"}}-{{ checksum "common/deps.edn"}}
|
||||
|
||||
|
||||
@@ -1,14 +1,32 @@
|
||||
{:lint-as {potok.core/reify clojure.core/reify
|
||||
promesa.core/let clojure.core/let
|
||||
rumext.alpha/defc clojure.core/defn
|
||||
app.db/with-atomic clojure.core/with-open}
|
||||
{:lint-as
|
||||
{promesa.core/let clojure.core/let
|
||||
rumext.alpha/defc clojure.core/defn
|
||||
rumext.alpha/fnc clojure.core/fn
|
||||
app.common.data/export clojure.core/def
|
||||
app.db/with-atomic clojure.core/with-open}
|
||||
|
||||
:hooks
|
||||
{:analyze-call
|
||||
{app.common.data/export hooks.export/export
|
||||
potok.core/reify hooks.export/potok-reify
|
||||
cljs.core/specify! hooks.export/clojure-specify
|
||||
app.util.services/defmethod hooks.export/service-defmethod
|
||||
}}
|
||||
|
||||
:output
|
||||
{:exclude-files ["data_readers.clj"]}
|
||||
{:exclude-files
|
||||
["data_readers.clj"
|
||||
"app/util/perf.cljs"
|
||||
"app/common/logging.cljc"
|
||||
"app/common/exceptions.cljc"]}
|
||||
|
||||
:linters
|
||||
{:unsorted-required-namespaces
|
||||
{:level :warning}
|
||||
|
||||
:potok/reify-type
|
||||
{:level :error}
|
||||
|
||||
:unresolved-namespace
|
||||
{:level :warning
|
||||
:exclude [data_readers]}
|
||||
@@ -16,12 +34,12 @@
|
||||
:single-key-in
|
||||
{:level :warning}
|
||||
|
||||
:redundant-do
|
||||
{:level :off}
|
||||
|
||||
:unused-binding
|
||||
{:exclude-destructured-as true
|
||||
:exclude-destructured-keys-in-fn-args false
|
||||
}
|
||||
|
||||
:unresolved-symbol
|
||||
{:exclude ['(app.util.services/defmethod)
|
||||
]}}}
|
||||
}}
|
||||
|
||||
|
||||
78
.clj-kondo/hooks/export.clj
Normal file
78
.clj-kondo/hooks/export.clj
Normal file
@@ -0,0 +1,78 @@
|
||||
(ns hooks.export
|
||||
(:require [clj-kondo.hooks-api :as api]))
|
||||
|
||||
(defn export
|
||||
[{:keys [:node]}]
|
||||
(let [[_ sname] (:children node)
|
||||
result (api/list-node
|
||||
[(api/token-node (symbol "def"))
|
||||
(api/token-node (symbol (name (:value sname))))
|
||||
sname])]
|
||||
{:node result}))
|
||||
|
||||
(def registry (atom {}))
|
||||
|
||||
(defn potok-reify
|
||||
[{:keys [:node :filename] :as params}]
|
||||
(let [[rnode rtype & other] (:children node)
|
||||
rsym (symbol (str "event-type-" (name (:k rtype))))
|
||||
reg (get @registry filename #{})]
|
||||
(when-not (:namespaced? rtype)
|
||||
(let [{:keys [:row :col]} (meta rtype)]
|
||||
(api/reg-finding! {:message "ptk/reify type should be namespaced"
|
||||
:type :potok/reify-type
|
||||
:row row
|
||||
:col col})))
|
||||
|
||||
(if (contains? reg rsym)
|
||||
(let [{:keys [:row :col]} (meta rtype)]
|
||||
(api/reg-finding! {:message (str "duplicate type: " (name (:k rtype)))
|
||||
:type :potok/reify-type
|
||||
:row row
|
||||
:col col}))
|
||||
(swap! registry update filename (fnil conj #{}) rsym))
|
||||
|
||||
(let [result (api/list-node
|
||||
(into [(api/token-node (symbol "deftype"))
|
||||
(api/token-node rsym)
|
||||
(api/vector-node [])]
|
||||
other))]
|
||||
{:node result})))
|
||||
|
||||
(defn clojure-specify
|
||||
[{:keys [:node]}]
|
||||
(let [[rnode rtype & other] (:children node)
|
||||
result (api/list-node
|
||||
(into [(api/token-node (symbol "extend-type"))
|
||||
(api/token-node (gensym (:string-value rtype)))]
|
||||
other))]
|
||||
{:node result}))
|
||||
|
||||
|
||||
(defn service-defmethod
|
||||
[{:keys [:node]}]
|
||||
(let [[rnode rtype ?meta & other] (:children node)
|
||||
rsym (gensym (name (:k rtype)))
|
||||
result (api/list-node
|
||||
[(api/token-node (symbol "do"))
|
||||
(api/list-node
|
||||
[(api/token-node (symbol "declare"))
|
||||
(api/token-node rsym)])
|
||||
(if (= :map (:tag ?meta))
|
||||
(api/list-node
|
||||
[(api/token-node (symbol "reset-meta!"))
|
||||
(api/token-node rsym)
|
||||
?meta])
|
||||
(api/list-node
|
||||
[(api/token-node (symbol "comment"))
|
||||
(api/token-node rsym)]))
|
||||
(api/list-node
|
||||
(into [(api/token-node (symbol "defmethod"))
|
||||
(api/token-node rsym)
|
||||
rtype]
|
||||
(cons ?meta other)))])]
|
||||
;; (prn "==============" rtype (into {} ?meta))
|
||||
;; (prn (api/sexpr result))
|
||||
{:node result}))
|
||||
|
||||
|
||||
8
.gitignore
vendored
8
.gitignore
vendored
@@ -30,10 +30,16 @@ node_modules
|
||||
/exporter/target
|
||||
/exporter/.shadow-cljs
|
||||
/docker/images/bundle*
|
||||
/common/.shadow-cljs
|
||||
/common/target
|
||||
/.clj-kondo/.cache
|
||||
/bundle*
|
||||
/media
|
||||
/deploy
|
||||
/web
|
||||
/_dump
|
||||
/vendor/svgclean/bundle*.js
|
||||
/vendor/svgclean/bundle*.js
|
||||
|
||||
.calva
|
||||
.clj-kondo
|
||||
.lsp
|
||||
|
||||
315
CHANGES.md
315
CHANGES.md
@@ -1,15 +1,326 @@
|
||||
# CHANGELOG #
|
||||
|
||||
# CHANGELOG
|
||||
|
||||
## :rocket: Next
|
||||
|
||||
### :boom: Breaking changes
|
||||
### :sparkles: New features
|
||||
### :bug: Bugs fixed
|
||||
### :arrow_up: Deps updates
|
||||
### :heart: Community contributions by (Thank you!)
|
||||
|
||||
# 1.10.2-beta
|
||||
|
||||
### :bug: Bugs fixed
|
||||
|
||||
- Fix corner case issues with media file uploads.
|
||||
- Fix issue with default page grids validation.
|
||||
- Fix issue related to some raceconditions on workspace navigation events.
|
||||
|
||||
### :arrow_up: Deps updates
|
||||
|
||||
- Update log4j2 dependency.
|
||||
|
||||
|
||||
# 1.10.1-beta
|
||||
|
||||
### :bug: Bugs fixed
|
||||
|
||||
- Fix problems with team management [#1353](https://github.com/penpot/penpot/issues/1353)
|
||||
|
||||
|
||||
## 1.10.0-beta
|
||||
|
||||
### :boom: Breaking changes
|
||||
|
||||
- The initial project / data mechanism (not documented) has been
|
||||
disabled. Is the mechanism used for creating initial project on user
|
||||
signup. With the new onboarding approach, this subsystem is no
|
||||
longer needed and is disabled.
|
||||
|
||||
### :sparkles: New features
|
||||
|
||||
- Enhance corner radius behavior [Taiga #2190](https://tree.taiga.io/project/penpot/issue/2190).
|
||||
- Allow preserve scroll position in interactions [Taiga #2250](https://tree.taiga.io/project/penpot/us/2250).
|
||||
- Add new onboarding modals.
|
||||
|
||||
### :bug: Bugs fixed
|
||||
|
||||
- Fix problem with exporting before the document is saved [Taiga #2189](https://tree.taiga.io/project/penpot/issue/2189).
|
||||
- Fix undo stacking when changing color from color-picker [Taiga #2191](https://tree.taiga.io/project/penpot/issue/2191).
|
||||
- Fix pages dropdown in viewer [Taiga #2087](https://tree.taiga.io/project/penpot/issue/2087).
|
||||
- Fix problem when exporting texts with gradients or opacity [Taiga #2200](https://tree.taiga.io/project/penpot/issue/2200).
|
||||
- Fix problem with view mode comments [Taiga #2226](https://tree.taiga.io/project/penpot/issue/2226).
|
||||
- Disallow to create a component when already has one [Taiga #2237](https://tree.taiga.io/project/penpot/issue/2237).
|
||||
- Add ellipsis in long labels for input fields [Taiga #2224](https://tree.taiga.io/project/penpot/issue/2224)
|
||||
- Fix problem with text rendering on export [Taiga #2223](https://tree.taiga.io/project/penpot/issue/2223)
|
||||
- Fix problem when flattening booleans losing styles [Taiga #2217](https://tree.taiga.io/project/penpot/issue/2217)
|
||||
- Add shortcuts to boolean icons popups [Taiga #2220](https://tree.taiga.io/project/penpot/issue/2220)
|
||||
- Fix a worker error when transforming a rectangle into path
|
||||
- Fix max/min values for opacity fields [Taiga #2183](https://tree.taiga.io/project/penpot/issue/2183)
|
||||
- Fix viewer comment position when zoom applied [Taiga #2240](https://tree.taiga.io/project/penpot/issue/2240)
|
||||
- Remove change style on hover for options [Taiga #2172](https://tree.taiga.io/project/penpot/issue/2172)
|
||||
- Fix problem in viewer with dropdowns when comments active [#1303](https://github.com/penpot/penpot/issues/1303)
|
||||
- Add placeholder to create shareable link
|
||||
- Fix project files count not refreshing correctly after import [Taiga #2216](https://tree.taiga.io/project/penpot/issue/2216)
|
||||
- Remove button after import process finish [Taiga #2215](https://tree.taiga.io/project/penpot/issue/2215)
|
||||
|
||||
### :heart: Community contributions by (Thank you!)
|
||||
|
||||
- To the translation community for the hard work on making penpot
|
||||
available on so many languages.
|
||||
|
||||
## 1.9.0-alpha
|
||||
|
||||
### :boom: Breaking changes
|
||||
|
||||
- Some stroke-caps can change behaviour.
|
||||
- Text display bug fix could potentialy make some texts jump a line.
|
||||
|
||||
### :sparkles: New features
|
||||
|
||||
- Add boolean shapes: intersections, unions, difference and exclusions[Taiga #748](https://tree.taiga.io/project/penpot/us/748).
|
||||
- Add advanced prototyping [Taiga #244](https://tree.taiga.io/project/penpot/us/244).
|
||||
- Add multiple flows [Taiga #2091](https://tree.taiga.io/project/penpot/us/2091).
|
||||
- Change order of the teams menu so it's in the joined time order.
|
||||
|
||||
### :bug: Bugs fixed
|
||||
|
||||
- Enhance duplicating prototype connections behaviour [Taiga #2093](https://tree.taiga.io/project/penpot/us/2093).
|
||||
- Ignore constraints in horizontal or vertical flip [Taiga #2038](https://tree.taiga.io/project/penpot/issue/2038).
|
||||
- Fix color and typographies refs lost when duplicated file [Taiga #2165](https://tree.taiga.io/project/penpot/issue/2165).
|
||||
- Fix problem with overflow dropdown on stroke-cap [#1216](https://github.com/penpot/penpot/issues/1216).
|
||||
- Fix menu context for single element nested in components [#1186](https://github.com/penpot/penpot/issues/1186).
|
||||
- Fix error screen when operations over comments fail [#1219](https://github.com/penpot/penpot/issues/1219).
|
||||
- Fix undo problem when changing typography/color from library [#1230](https://github.com/penpot/penpot/issues/1230).
|
||||
- Fix problem with text margin while rendering [#1231](https://github.com/penpot/penpot/issues/1231).
|
||||
- Fix problem with masked texts on exporting [Taiga #2116](https://tree.taiga.io/project/penpot/issue/2116).
|
||||
- Fix text editor enter behaviour with centered texts [Taiga #2126](https://tree.taiga.io/project/penpot/issue/2126).
|
||||
- Fix residual stroke on imported svg [Taiga #2125](https://tree.taiga.io/project/penpot/issue/2125).
|
||||
- Add links for terms of service and privacy policy in register checkbox [Taiga #2020](https://tree.taiga.io/project/penpot/issue/2020).
|
||||
- Allow three character hex and web colors in color picker hex input [#1184](https://github.com/penpot/penpot/issues/1184).
|
||||
- Allow lowercase search for fonts [#1180](https://github.com/penpot/penpot/issues/1180).
|
||||
- Fix group renaming problem [Taiga #1969](https://tree.taiga.io/project/penpot/issue/1969).
|
||||
- Fix export group with shadows on children [Taiga #2036](https://tree.taiga.io/project/penpot/issue/2036).
|
||||
- Fix zoom context menu in viewer [Taiga #2041](https://tree.taiga.io/project/penpot/issue/2041).
|
||||
- Fix stroke caps adjustments in relation with stroke size [Taiga #2123](https://tree.taiga.io/project/penpot/issue/2123).
|
||||
- Fix problem duplicating paths [Taiga #2147](https://tree.taiga.io/project/penpot/issue/2147).
|
||||
- Fix problem inheriting attributes from SVG root when importing [Taiga #2124](https://tree.taiga.io/project/penpot/issue/2124).
|
||||
- Fix problem with lines and inside/outside stroke [Taiga #2146](https://tree.taiga.io/project/penpot/issue/2146).
|
||||
- Add stroke width in selection calculation [Taiga #2146](https://tree.taiga.io/project/penpot/issue/2146).
|
||||
- Fix shift+wheel to horizontal scrolling in MacOS [#1217](https://github.com/penpot/penpot/issues/1217).
|
||||
- Fix path stroke is not working properly with high thickness [Taiga #2154](https://tree.taiga.io/project/penpot/issue/2154).
|
||||
- Fix bug with transformation operations [Taiga #2155](https://tree.taiga.io/project/penpot/issue/2155).
|
||||
- Fix bug in firefox when a text box is inside a mask [Taiga #2152](https://tree.taiga.io/project/penpot/issue/2152).
|
||||
- Fix problem with stroke inside/outside [Taiga #2186](https://tree.taiga.io/project/penpot/issue/2186)
|
||||
- Fix masks export area [Taiga #2189](https://tree.taiga.io/project/penpot/issue/2189)
|
||||
- Fix paste in place in arboards [Taiga #2188](https://tree.taiga.io/project/penpot/issue/2188)
|
||||
- Fix font size input stuck on selection change [Taiga #2184](https://tree.taiga.io/project/penpot/issue/2184)
|
||||
- Fix stroke cut on shapes export [Taiga #2171](https://tree.taiga.io/project/penpot/issue/2171)
|
||||
- Fix no color when boolean with an SVG [Taiga #2193](https://tree.taiga.io/project/penpot/issue/2193)
|
||||
- Fix unlink color styles at strokes [Taiga #2206](https://tree.taiga.io/project/penpot/issue/2206).
|
||||
|
||||
### :arrow_up: Deps updates
|
||||
|
||||
### :heart: Community contributions by (Thank you!)
|
||||
|
||||
- To the translation community for the hard work on making penpot
|
||||
available on so many languages.
|
||||
|
||||
|
||||
|
||||
## 1.8.4-alpha
|
||||
|
||||
### :bug: Bugs fixed
|
||||
|
||||
- Fix problem importing components [Taiga #2151](https://tree.taiga.io/project/penpot/issue/2151).
|
||||
|
||||
## 1.8.3-alpha
|
||||
|
||||
### :sparkles: New features
|
||||
|
||||
- Adds progress report to importing process.
|
||||
|
||||
## 1.8.2-alpha
|
||||
|
||||
### :bug: Bugs fixed
|
||||
|
||||
- Fix problem with masking images in viewer [#1238](https://github.com/penpot/penpot/issues/1238).
|
||||
|
||||
## 1.8.1-alpha
|
||||
|
||||
### :bug: Bugs fixed
|
||||
|
||||
- Fix project renaming issue (and some other related to the same underlying bug).
|
||||
- Fix internal exception on audit log persistence layer.
|
||||
- Set proper environment variable on docker images for chrome executable.
|
||||
- Fix internal metrics on websocket connections.
|
||||
|
||||
|
||||
## 1.8.0-alpha
|
||||
|
||||
### :boom: Breaking changes
|
||||
|
||||
- This release includes a new approach for handling share links, and
|
||||
this feature is incompatible with the previous one. This means that
|
||||
all the public share links generated previously will stop working.
|
||||
|
||||
### :sparkles: New features
|
||||
|
||||
- Add tooltips to color picker tabs [Taiga #1814](https://tree.taiga.io/project/penpot/us/1814).
|
||||
- Add styling to the end point of any open paths [Taiga #1107](https://tree.taiga.io/project/penpot/us/1107).
|
||||
- Allow to zoom with ctrl + middle button [Taiga #1428](https://tree.taiga.io/project/penpot/us/1428).
|
||||
- Auto placement of duplicated objects [Taiga #1386](https://tree.taiga.io/project/penpot/us/1386).
|
||||
- Enable penpot SVG metadata only when exporting complete files [Taiga #1914](https://tree.taiga.io/project/penpot/us/1914?milestone=295883).
|
||||
- Export to PDF all artboards of one page [Taiga #1895](https://tree.taiga.io/project/penpot/us/1895).
|
||||
- Go to a undo step clicking on a history element of the list [Taiga #1374](https://tree.taiga.io/project/penpot/us/1374).
|
||||
- Increment font size by 10 with shift+arrows [1047](https://github.com/penpot/penpot/issues/1047).
|
||||
- New shortcut to detach components Ctrl+Shift+K [Taiga #1799](https://tree.taiga.io/project/penpot/us/1799).
|
||||
- Set email inputs to type "email", to aid keyboard entry [Taiga #1921](https://tree.taiga.io/project/penpot/issue/1921).
|
||||
- Use shift+move to move element orthogonally [#823](https://github.com/penpot/penpot/issues/823).
|
||||
- Use space + mouse drag to pan, instead of only space [Taiga #1800](https://tree.taiga.io/project/penpot/us/1800).
|
||||
- Allow navigate through pages on the viewer [Taiga #1550](https://tree.taiga.io/project/penpot/us/1550).
|
||||
- Allow create share links with specific pages [Taiga #1844](https://tree.taiga.io/project/penpot/us/1844).
|
||||
|
||||
### :bug: Bugs fixed
|
||||
|
||||
- Prevent adding numeric suffix to layer names when not needed [Taiga #1929](https://tree.taiga.io/project/penpot/us/1929).
|
||||
- Prevent deleting or moving the drafts project [Taiga #1935](https://tree.taiga.io/project/penpot/issue/1935).
|
||||
- Fix problem with zoom and selection [Taiga #1919](https://tree.taiga.io/project/penpot/issue/1919)
|
||||
- Fix problem with borders on shape export [#1092](https://github.com/penpot/penpot/issues/1092)
|
||||
- Fix thumbnail cropping issue [Taiga #1964](https://tree.taiga.io/project/penpot/issue/1964)
|
||||
- Fix repeated fetch on file selection [Taiga #1933](https://tree.taiga.io/project/penpot/issue/1933)
|
||||
- Fix rename typography on text options [Taiga #1963](https://tree.taiga.io/project/penpot/issue/1963)
|
||||
- Fix problems with order in groups [Taiga #1960](https://tree.taiga.io/project/penpot/issue/1960)
|
||||
- Fix SVG components preview [#1134](https://github.com/penpot/penpot/issues/1134)
|
||||
- Fix group renaming problem [Taiga #1969](https://tree.taiga.io/project/penpot/issue/1969)
|
||||
- Fix problem with import broken images links [#1197](https://github.com/penpot/penpot/issues/1197)
|
||||
- Fix problem while moving imported SVG's [#1199](https://github.com/penpot/penpot/issues/1199)
|
||||
|
||||
### :arrow_up: Deps updates
|
||||
### :boom: Breaking changes
|
||||
### :heart: Community contributions by (Thank you!)
|
||||
|
||||
- eduayme [#1129](https://github.com/penpot/penpot/pull/1129).
|
||||
|
||||
|
||||
## 1.7.4-alpha
|
||||
|
||||
### :bug: Bugs fixed
|
||||
|
||||
- Fix demo user creation (self-hosted only)
|
||||
- Add better ldap response validation and reporting (self-hosted only)
|
||||
|
||||
|
||||
## 1.7.3-alpha
|
||||
|
||||
### :bug: Bugs fixed
|
||||
|
||||
- Fix font uploading issue on Windows.
|
||||
|
||||
|
||||
## 1.7.2-alpha
|
||||
|
||||
### :sparkles: New features
|
||||
|
||||
- Add many improvements to text tool.
|
||||
|
||||
### :bug: Bugs fixed
|
||||
|
||||
- Add scroll bar to Teams menu [Taiga #1894](https://tree.taiga.io/project/penpot/issue/1894).
|
||||
- Fix repeated names when duplicating artboards or groups [Taiga #1892](https://tree.taiga.io/project/penpot/issue/1892).
|
||||
- Fix properly messages lifecycle on navigate.
|
||||
- Fix handling repeated names on duplicate object trees.
|
||||
- Fix group naming on group creation.
|
||||
- Fix some issues in svg transformation.
|
||||
|
||||
### :arrow_up: Deps updates
|
||||
|
||||
- Update frontend build tooling.
|
||||
|
||||
### :heart: Community contributions by (Thank you!)
|
||||
|
||||
- soultipsy [#1100](https://github.com/penpot/penpot/pull/1100)
|
||||
|
||||
|
||||
## 1.7.1-alpha
|
||||
|
||||
### :bug: Bugs fixed
|
||||
|
||||
- Fix issue related to the GC and images in path shapes.
|
||||
- Fix issue on the shape order on some undo operations.
|
||||
- Fix issue on undo page deletion.
|
||||
- Fix some issues related to constraints.
|
||||
|
||||
|
||||
## 1.7.0-alpha
|
||||
|
||||
### :sparkles: New features
|
||||
|
||||
- Allow nested asset groups [Taiga #1716](https://tree.taiga.io/project/penpot/us/1716).
|
||||
- Allow to ungroup assets [Taiga #1719](https://tree.taiga.io/project/penpot/us/1719).
|
||||
- Allow to rename assets groups [Taiga #1721](https://tree.taiga.io/project/penpot/us/1721).
|
||||
- Component constraints (left, right, left and right, center, scale...) [Taiga #1125](https://tree.taiga.io/project/penpot/us/1125).
|
||||
- Export elements to PDF [Taiga #519](https://tree.taiga.io/project/penpot/us/519).
|
||||
- Memorize collapse state of assets in panel [Taiga #1718](https://tree.taiga.io/project/penpot/us/1718).
|
||||
- Headers button sets and menus review [Taiga #1663](https://tree.taiga.io/project/penpot/us/1663).
|
||||
- Preserve components if possible, when pasted into a different file [Taiga #1063](https://tree.taiga.io/project/penpot/issue/1063).
|
||||
- Add the ability to offload file data to a cheaper storage when file becomes inactive.
|
||||
- Import/Export Penpot files from dashboard.
|
||||
- Double click won't make a shape a path until you change a node [Taiga #1796](https://tree.taiga.io/project/penpot/us/1796)
|
||||
- Incremental area selection [#779](https://github.com/penpot/penpot/discussions/779)
|
||||
|
||||
### :bug: Bugs fixed
|
||||
|
||||
- Process numeric input changes only if the value actually changed.
|
||||
- Remove unnecesary redirect from history when user goes to workspace from dashboard [Taiga #1820](https://tree.taiga.io/project/penpot/issue/1820).
|
||||
- Detach shapes from deleted assets [Taiga #1850](https://tree.taiga.io/project/penpot/issue/1850).
|
||||
- Fix tooltip position on view application [Taiga #1819](https://tree.taiga.io/project/penpot/issue/1819).
|
||||
- Fix dashboard navigation on moving file to other team [Taiga #1817](https://tree.taiga.io/project/penpot/issue/1817).
|
||||
- Fix workspace header presence styles and invalid link [Taiga #1813](https://tree.taiga.io/project/penpot/issue/1813).
|
||||
- Fix color-input wrong behavior (on workspace page color) [Taiga #1795](https://tree.taiga.io/project/penpot/issue/1795).
|
||||
- Fix file contextual menu in shared libraries at dashboard [Taiga #1865](https://tree.taiga.io/project/penpot/issue/1865).
|
||||
- Fix problem with color picker and fonts [#1049](https://github.com/penpot/penpot/issues/1049)
|
||||
- Fix negative values in blur [Taiga #1815](https://tree.taiga.io/project/penpot/issue/1815)
|
||||
- Fix problem when editing color in group [Taiga #1816](https://tree.taiga.io/project/penpot/issue/1816)
|
||||
- Fix resize/rotate with mouse buttons different than left [#1060](https://github.com/penpot/penpot/issues/1060)
|
||||
- Fix header partialy visible on fullscreen viewer mode [Taiga #1875](https://tree.taiga.io/project/penpot/issue/1875)
|
||||
- Fix dynamic alignment enabled with hidden objects [#1063](https://github.com/penpot/penpot/issues/1063)
|
||||
|
||||
|
||||
## 1.6.5-alpha
|
||||
|
||||
### :bug: Bugs fixed
|
||||
|
||||
- Fix problem with paths editing after flip [#1040](https://github.com/penpot/penpot/issues/1040)
|
||||
|
||||
## 1.6.4-alpha
|
||||
|
||||
### :sparkles: Minor improvements
|
||||
|
||||
- Decrease default bulk buffers on storage tasks.
|
||||
- Reduce file_change preserve interval to 24h.
|
||||
|
||||
### :bug: Bugs fixed
|
||||
|
||||
- Don't allow rename drafts project.
|
||||
- Fix custom font deletion task.
|
||||
- Fix custom font rendering on exporting shapes.
|
||||
- Fix font loading on viewer app.
|
||||
- Fix problem when moving files with drag & drop.
|
||||
- Fix unexpected exception on searching without term.
|
||||
- Properly handle nil values on `update-shapes` function.
|
||||
- Replace frame term usage by artboard on viewer app.
|
||||
|
||||
|
||||
## 1.6.3-alpha
|
||||
|
||||
### :bug: Bugs fixed
|
||||
|
||||
- Fix problem with merge and join nodes [#990](https://github.com/penpot/penpot/issues/990)
|
||||
- Fix problem with empty path editing.
|
||||
- Fix problem with create component.
|
||||
- Fix problem with move-objects.
|
||||
- Fix problem with merge and join nodes.
|
||||
|
||||
## 1.6.2-alpha
|
||||
|
||||
|
||||
73
README.md
73
README.md
@@ -2,24 +2,60 @@
|
||||
[uri_license]: https://www.mozilla.org/en-US/MPL/2.0
|
||||
[uri_license_image]: https://img.shields.io/badge/MPL-2.0-blue.svg
|
||||
|
||||
[![License: MPL-2.0][uri_license_image]][uri_license]
|
||||
[](https://gitter.im/penpot/community)
|
||||
[](https://tree.taiga.io/project/penpot/ "Managed with Taiga.io")
|
||||
[](https://gitpod.io/#https://github.com/penpot/penpot)
|
||||
<h1 align="center">
|
||||
<br>
|
||||
<img src="https://penpot.app/images/readme/readme-logo.jpg" alt="PENPOT">
|
||||
</h1>
|
||||
|
||||
<p align="center"><a href="https://www.mozilla.org/en-US/MPL/2.0" rel="nofollow"><img src="https://camo.githubusercontent.com/3fcf3d6b678ea15fde3cf7d6af0e242160366282d62a7c182d83a50bfee3f45e/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f4d504c2d322e302d626c75652e737667" alt="License: MPL-2.0" data-canonical-src="https://img.shields.io/badge/MPL-2.0-blue.svg" style="max-width:100%;"></a>
|
||||
<a href="https://gitter.im/penpot/community" rel="nofollow"><img src="https://camo.githubusercontent.com/5b0aecb33434f82a7b158eab7247544235ada0cf7eeb9ce8e52562dd67f614b7/68747470733a2f2f6261646765732e6769747465722e696d2f736572656e6f2d78797a2f636f6d6d756e6974792e737667" alt="Gitter" data-canonical-src="https://badges.gitter.im/sereno-xyz/community.svg" style="max-width:100%;"></a>
|
||||
<a href="https://tree.taiga.io/project/penpot/" title="Managed with Taiga.io" rel="nofollow"><img src="https://camo.githubusercontent.com/4a1d1112f0272e3393b1e8da312ff4435418e9e2eb4c0964881e3680f90a653c/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f6d616e61676564253230776974682d54414947412e696f2d3730396631342e737667" alt="Managed with Taiga.io" data-canonical-src="https://img.shields.io/badge/managed%20with-TAIGA.io-709f14.svg" style="max-width:100%;"></a>
|
||||
<a href="https://gitpod.io/#https://github.com/penpot/penpot" rel="nofollow"><img src="https://camo.githubusercontent.com/daadb4894128d1e19b72d80236f5959f1f2b47f9fe081373f3246131f0189f6c/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f476974706f642d72656164792d2d746f2d2d636f64652d626c75653f6c6f676f3d676974706f64" alt="Gitpod ready-to-code" data-canonical-src="https://img.shields.io/badge/Gitpod-ready--to--code-blue?logo=gitpod" style="max-width:100%;"></a></p>
|
||||
|
||||

|
||||
|
||||
|
||||
# PENPOT #
|
||||
## What is Penpot? ##
|
||||
|
||||
Penpot is the first Open Source design and prototyping platform meant
|
||||
for cross-domain teams. Non dependent on operating systems, Penpot is
|
||||
web based and works with open web standards (SVG). For all and
|
||||
empowered by the community.
|
||||
Penpot is the first **Open Source design** and prototyping platform meant for cross-domain teams. Non dependent on operating systems, Penpot is web based and works with open web standards (SVG). For all and empowered by the community.
|
||||
|
||||

|
||||
- [How to use](#how-to-use)
|
||||
- [Help center](#help-center)
|
||||
- [Contributing](#contributing)
|
||||
- [Give feedback](#give-feedback)
|
||||
- [Tutorials](#tutorials)
|
||||
- [License](#license)
|
||||
|
||||
## How to use ##
|
||||
|
||||
Login or Register on our Penpot cloud app. Create a team to work together on projects and share design assets or jump right away into Penpot and **start designing** by your own.
|
||||
|
||||
✏️ [Start using Penpot](https://design.penpot.app)
|
||||
|
||||
You can also install Penpot in a local environment. This section details everything you need to know to get Penpot up and running in production environments. Although it can be installed in many ways, the recommended approach is using **docker** and **docker-compose**.
|
||||
|
||||
🐳 [Install docker](https://help.penpot.app/technical-guide/getting-started/)
|
||||
|
||||
## Help center ##
|
||||
|
||||
In this documentation you will find (almost) everything you need to know about how to work with Penpot. From the interface basics to advanced functionality.
|
||||
|
||||
📖 [User guide](https://help.penpot.app/user-guide/)
|
||||
|
||||
❓ [FAQs](https://help.penpot.app/faqs/)
|
||||
|
||||
🖥️ [Technical guide](https://help.penpot.app/technical-guide/)
|
||||
|
||||
❤️ [Contributing guide](https://help.penpot.app/contributing-guide/)
|
||||
|
||||

|
||||
|
||||
## Contributing ##
|
||||
|
||||
<p align="center">
|
||||
<img src="https://penpot.app/images/open-source.png" alt="Open Source">
|
||||
</p>
|
||||
|
||||
**Open to you!**
|
||||
|
||||
We love the open source software community. Contributing is our
|
||||
@@ -28,11 +64,24 @@ and improve Penpot. All your awesome ideas and code are welcome!
|
||||
|
||||
Please refer to the [Contributing Guide](./CONTRIBUTING.md)
|
||||
|
||||
## Give feedback ##
|
||||
|
||||
## Documentation ##
|
||||
You can ask and answer questions, have open-ended conversations, and follow along on decisions affecting the project.
|
||||
|
||||
Please refer to the [help center](https://help.penpot.app).
|
||||
✉️ [Mail us](mailto:info@penpot.app)
|
||||
|
||||
💬 [Github discussions](https://github.com/penpot/penpot/discussions)
|
||||
|
||||
🐞 [Github issues](mailto:info@penpot.apphttps://github.com/penpot/penpot/issues)
|
||||
|
||||
✍️️ [Gitter](https://gitter.im/penpot/community)
|
||||
|
||||
## Tutorials ##
|
||||
|
||||
You can ask and answer questions, have open-ended conversations, and follow along on decisions affecting the project.
|
||||
Would you like to know more about Penpot? We recommend you to visit our youtube channel and learn more about the functionalities and possibilities of Penpot with our video tutorials.
|
||||
|
||||
🎞️ [Youtube channel](https://www.youtube.com/channel/UCAqS8G72uv9P5HG1IfgnQ9g)
|
||||
|
||||
## License ##
|
||||
|
||||
|
||||
@@ -1,26 +1,18 @@
|
||||
{:mvn/repos
|
||||
{"central" {:url "https://repo1.maven.org/maven2/"}
|
||||
"clojars" {:url "https://clojars.org/repo"}
|
||||
"jcenter" {:url "https://jcenter.bintray.com/"}}
|
||||
{
|
||||
;; :mvn/repos
|
||||
;; {"central" {:url "https://repo1.maven.org/maven2/"}
|
||||
;; "clojars" {:url "https://clojars.org/repo"}
|
||||
;; "jcenter" {:url "https://jcenter.bintray.com/"}
|
||||
;; }
|
||||
:deps
|
||||
{org.clojure/clojure {:mvn/version "1.10.3"}
|
||||
org.clojure/data.json {:mvn/version "2.2.3"}
|
||||
org.clojure/core.async {:mvn/version "1.3.618"}
|
||||
org.clojure/tools.cli {:mvn/version "1.0.206"}
|
||||
org.clojure/clojurescript {:mvn/version "1.10.844"}
|
||||
{penpot/common
|
||||
{:local/root "../common"}
|
||||
|
||||
;; Logging
|
||||
org.clojure/tools.logging {:mvn/version "1.1.0"}
|
||||
org.apache.logging.log4j/log4j-api {:mvn/version "2.14.1"}
|
||||
org.apache.logging.log4j/log4j-core {:mvn/version "2.14.1"}
|
||||
org.apache.logging.log4j/log4j-web {:mvn/version "2.14.1"}
|
||||
org.apache.logging.log4j/log4j-jul {:mvn/version "2.14.1"}
|
||||
org.apache.logging.log4j/log4j-slf4j18-impl {:mvn/version "2.14.1"}
|
||||
org.slf4j/slf4j-api {:mvn/version "2.0.0-alpha1"}
|
||||
org.zeromq/jeromq {:mvn/version "0.5.2"}
|
||||
|
||||
com.taoensso/nippy {:mvn/version "3.1.1"}
|
||||
com.github.luben/zstd-jni {:mvn/version "1.4.9-5"}
|
||||
com.github.luben/zstd-jni {:mvn/version "1.5.0-4"}
|
||||
|
||||
;; NOTE: don't upgrade to latest version, breaking change is
|
||||
;; introduced on 0.10.0 that suffixes counters with _total if they
|
||||
@@ -32,69 +24,60 @@
|
||||
org.eclipse.jetty/jetty-servlet]}
|
||||
io.prometheus/simpleclient_httpserver {:mvn/version "0.9.0"}
|
||||
|
||||
selmer/selmer {:mvn/version "1.12.40"}
|
||||
expound/expound {:mvn/version "0.8.9"}
|
||||
com.cognitect/transit-clj {:mvn/version "1.0.324"}
|
||||
io.lettuce/lettuce-core {:mvn/version "6.1.5.RELEASE"}
|
||||
java-http-clj/java-http-clj {:mvn/version "0.4.3"}
|
||||
|
||||
io.lettuce/lettuce-core {:mvn/version "6.1.2.RELEASE"}
|
||||
java-http-clj/java-http-clj {:mvn/version "0.4.2"}
|
||||
|
||||
info.sunng/ring-jetty9-adapter {:mvn/version "0.15.1"}
|
||||
com.github.seancorfield/next.jdbc {:mvn/version "1.2.659"}
|
||||
metosin/reitit-ring {:mvn/version "0.5.13"}
|
||||
metosin/jsonista {:mvn/version "0.3.3"}
|
||||
|
||||
org.postgresql/postgresql {:mvn/version "42.2.20"}
|
||||
com.zaxxer/HikariCP {:mvn/version "4.0.3"}
|
||||
info.sunng/ring-jetty9-adapter {:mvn/version "0.15.2"}
|
||||
com.github.seancorfield/next.jdbc {:mvn/version "1.2.709"}
|
||||
metosin/reitit-ring {:mvn/version "0.5.15"}
|
||||
org.postgresql/postgresql {:mvn/version "42.2.23"}
|
||||
com.zaxxer/HikariCP {:mvn/version "5.0.0"}
|
||||
|
||||
funcool/datoteka {:mvn/version "2.0.0"}
|
||||
funcool/promesa {:mvn/version "6.0.1"}
|
||||
funcool/cuerdas {:mvn/version "2021.05.09-0"}
|
||||
|
||||
buddy/buddy-core {:mvn/version "1.10.1"}
|
||||
buddy/buddy-hashers {:mvn/version "1.8.1"}
|
||||
buddy/buddy-sign {:mvn/version "3.4.1"}
|
||||
|
||||
lambdaisland/uri {:mvn/version "1.4.54"
|
||||
:exclusions [org.clojure/data.json]}
|
||||
|
||||
frankiesardo/linked {:mvn/version "1.3.0"}
|
||||
danlentz/clj-uuid {:mvn/version "0.1.9"}
|
||||
org.jsoup/jsoup {:mvn/version "1.13.1"}
|
||||
org.jsoup/jsoup {:mvn/version "1.14.2"}
|
||||
org.im4java/im4java {:mvn/version "1.4.0"}
|
||||
org.lz4/lz4-java {:mvn/version "1.7.1"}
|
||||
commons-io/commons-io {:mvn/version "2.8.0"}
|
||||
com.sun.mail/jakarta.mail {:mvn/version "2.0.1"}
|
||||
org.lz4/lz4-java {:mvn/version "1.8.0"}
|
||||
|
||||
org.clojars.pntblnk/clj-ldap {:mvn/version "0.0.17"}
|
||||
integrant/integrant {:mvn/version "0.8.0"}
|
||||
|
||||
software.amazon.awssdk/s3 {:mvn/version "2.16.62"}
|
||||
io.sentry/sentry {:mvn/version "5.1.2"}
|
||||
|
||||
;; exception printing
|
||||
io.aviso/pretty {:mvn/version "0.1.37"}
|
||||
environ/environ {:mvn/version "1.2.0"}}
|
||||
:paths ["src" "resources" "../common" "common"]
|
||||
;; Pretty Print specs
|
||||
fipp/fipp {:mvn/version "0.6.24"}
|
||||
pretty-spec/pretty-spec {:mvn/version "0.1.4"}
|
||||
|
||||
software.amazon.awssdk/s3 {:mvn/version "2.17.40"}}
|
||||
|
||||
:paths ["src" "resources"]
|
||||
:aliases
|
||||
{:dev
|
||||
{:extra-deps
|
||||
{com.bhauman/rebel-readline {:mvn/version "RELEASE"}
|
||||
org.clojure/tools.namespace {:mvn/version "RELEASE"}
|
||||
org.clojure/test.check {:mvn/version "RELEASE"}
|
||||
org.clojure/data.csv {:mvn/version "1.0.0"}
|
||||
com.clojure-goes-fast/clj-async-profiler {:mvn/version "0.5.1"}
|
||||
|
||||
fipp/fipp {:mvn/version "0.6.23"}
|
||||
criterium/criterium {:mvn/version "0.4.6"}
|
||||
mockery/mockery {:mvn/version "0.1.4"}}
|
||||
:extra-paths ["tests" "dev"]}
|
||||
criterium/criterium {:mvn/version "RELEASE"}
|
||||
mockery/mockery {:mvn/version "RELEASE"}}
|
||||
:extra-paths ["test" "dev"]}
|
||||
|
||||
:fn-fixtures
|
||||
{:exec-fn app.cli.fixtures/run
|
||||
:args {}}
|
||||
|
||||
:tests
|
||||
{:extra-deps {lambdaisland/kaocha {:mvn/version "1.0.829"}}
|
||||
:kaocha
|
||||
{:extra-deps {lambdaisland/kaocha {:mvn/version "1.0.887"}}
|
||||
:main-opts ["-m" "kaocha.runner"]}
|
||||
|
||||
:test
|
||||
{:extra-deps {io.github.cognitect-labs/test-runner
|
||||
{:git/url "https://github.com/cognitect-labs/test-runner.git"
|
||||
:git/sha "dd6da11611eeb87f08780a30ac8ea6012d4c05ce"}}
|
||||
:exec-fn cognitect.test-runner.api/test}
|
||||
|
||||
:outdated
|
||||
{:extra-deps {com.github.liquidz/antq {:mvn/version "RELEASE"}}
|
||||
:main-opts ["-m" "antq.core"]}
|
||||
|
||||
@@ -50,7 +50,7 @@
|
||||
;; --- Development Stuff
|
||||
|
||||
(defn- run-tests
|
||||
([] (run-tests #"^app.tests.*"))
|
||||
([] (run-tests #"^app.*-test$"))
|
||||
([o]
|
||||
(repl/refresh)
|
||||
(cond
|
||||
|
||||
101
backend/resources/api-doc.css
Normal file
101
backend/resources/api-doc.css
Normal file
@@ -0,0 +1,101 @@
|
||||
* {
|
||||
font-family: "JetBrains Mono", monospace;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
pre {
|
||||
margin: 0px;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0px;
|
||||
padding: 0px;
|
||||
padding-top: 20px;
|
||||
padding-bottom: 20px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
main {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
min-width: 900px;
|
||||
width: 900px;
|
||||
}
|
||||
|
||||
header {
|
||||
border-bottom: 1px solid #c0c0c0;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.rpc-doc-content {
|
||||
margin-top: 20px;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
/* border: 1px solid red; */
|
||||
padding: 5px;
|
||||
}
|
||||
|
||||
.rpc-doc-content > h2:not(:first-child) {
|
||||
margin-top: 30px;
|
||||
}
|
||||
|
||||
|
||||
.rpc-items {
|
||||
list-style: none;
|
||||
padding: 0px;
|
||||
margin: 0px;
|
||||
}
|
||||
|
||||
.rpc-item {
|
||||
/* border: 1px solid red; */
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.rpc-item:not(:last-child) {
|
||||
margin-bottom: 3px;
|
||||
}
|
||||
|
||||
.rpc-row-info {
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
background-color: #eeeeee;
|
||||
padding: 5px 10px;
|
||||
}
|
||||
|
||||
.rpc-row-info > *:not(:last-child) {
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.rpc-row-info > * {
|
||||
/* border: 1px solid green; */
|
||||
}
|
||||
|
||||
.rpc-row-info > .type {
|
||||
font-weight: bold;
|
||||
width: 70px;
|
||||
}
|
||||
|
||||
.rpc-row-info > .name {
|
||||
width: 280px;
|
||||
/* font-weight: bold; */
|
||||
}
|
||||
|
||||
.rpc-row-info > .tags > .tag > span:first-child {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.rpc-row-detail {
|
||||
padding: 5px 10px;
|
||||
padding-bottom: 20px;
|
||||
}
|
||||
27
backend/resources/api-doc.js
Normal file
27
backend/resources/api-doc.js
Normal file
@@ -0,0 +1,27 @@
|
||||
(function() {
|
||||
document.addEventListener("DOMContentLoaded", function(event) {
|
||||
const rows = document.querySelectorAll(".rpc-row-info");
|
||||
|
||||
const onRowClick = (event) => {
|
||||
const target = event.currentTarget;
|
||||
for (let node of rows) {
|
||||
if (node !== target) {
|
||||
node.nextElementSibling.classList.add("hidden");
|
||||
} else {
|
||||
const sibling = target.nextElementSibling;
|
||||
|
||||
if (sibling.classList.contains("hidden")) {
|
||||
sibling.classList.remove("hidden");
|
||||
} else {
|
||||
sibling.classList.add("hidden");
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
for (let node of rows) {
|
||||
node.addEventListener("click", onRowClick);
|
||||
}
|
||||
|
||||
});
|
||||
})();
|
||||
80
backend/resources/api-doc.tmpl
Normal file
80
backend/resources/api-doc.tmpl
Normal file
@@ -0,0 +1,80 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="robots" content="noindex,nofollow">
|
||||
<meta http-equiv="x-ua-compatible" content="ie=edge" />
|
||||
<title>Builtin API Documentation - Penpot</title>
|
||||
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=JetBrains+Mono">
|
||||
<style>
|
||||
{% include "api-doc.css" %}
|
||||
</style>
|
||||
<script>
|
||||
{% include "api-doc.js" %}
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<main>
|
||||
<header>
|
||||
<h1>Penpot API Documentation</h1>
|
||||
</header>
|
||||
<section class="rpc-doc-content">
|
||||
|
||||
<h2>RPC QUERY METHODS:</h2>
|
||||
<ul class="rpc-items">
|
||||
{% for item in query-methods %}
|
||||
<li class="rpc-item">
|
||||
<div class="rpc-row-info">
|
||||
{# <div class="type">{{item.type}}</div> #}
|
||||
<div class="name">{{item.name}}</div>
|
||||
<div class="tags">
|
||||
<span class="tag">
|
||||
<span>Auth:</span>
|
||||
<span>{% if item.auth %}YES{% else %}NO{% endif %}</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="rpc-row-detail hidden">
|
||||
{% if item.docs %}
|
||||
<h3>DOCSTRING:</h3>
|
||||
<p>{{item.docs}}</p>
|
||||
{% endif %}
|
||||
|
||||
<h3>SPEC EXPLAIN:</h3>
|
||||
<pre>{{item.spec}}</pre>
|
||||
</div>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
|
||||
<h2>RPC MUTATION METHODS:</h2>
|
||||
<ul class="rpc-items">
|
||||
{% for item in mutation-methods %}
|
||||
<li class="rpc-item">
|
||||
<div class="rpc-row-info">
|
||||
{# <div class="type">{{item.type}}</div> #}
|
||||
<div class="name">{{item.name}}</div>
|
||||
<div class="tags">
|
||||
<span class="tag">
|
||||
<span>Auth:</span>
|
||||
<span>{% if item.auth %}YES{% else %}NO{% endif %}</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="rpc-row-detail hidden">
|
||||
{% if item.docs %}
|
||||
<h3>DOCSTRING:</h3>
|
||||
<p>{{item.docs}}</p>
|
||||
{% endif %}
|
||||
|
||||
<h3>SPEC EXPLAIN:</h3>
|
||||
<pre>{{item.spec}}</pre>
|
||||
</div>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</section>
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
<mj-text font-size="24px" font-weight="600">Hello {{name}}!</mj-text>
|
||||
<mj-text>
|
||||
Thanks for signing up for your Penpot account! Please verify your
|
||||
email using the link below adn get started building mockups and
|
||||
email using the link below and get started building mockups and
|
||||
prototypes today!
|
||||
</mj-text>
|
||||
<mj-button href="{{ public-uri }}/#/auth/verify-token?token={{token}}">
|
||||
|
||||
@@ -1 +1 @@
|
||||
[PENPOT FEEDBACK]: {{subject|abbreviate:19}} (from {{email}})
|
||||
[PENPOT FEEDBACK]: {{subject}}
|
||||
|
||||
@@ -173,7 +173,7 @@
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;">
|
||||
<div style="font-family:Source Sans Pro, sans-serif;font-size:16px;line-height:150%;text-align:left;color:#000000;">Thanks for signing up for your Penpot account! Please verify your email using the link below adn get started building mockups and prototypes today!</div>
|
||||
<div style="font-family:Source Sans Pro, sans-serif;font-size:16px;line-height:150%;text-align:left;color:#000000;">Thanks for signing up for your Penpot account! Please verify your email using the link below and get started building mockups and prototypes today!</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
@@ -465,4 +465,4 @@
|
||||
</div>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
</html>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
Hello {{name}}!
|
||||
|
||||
Thanks for signing up for your Penpot account! Please verify your email using the
|
||||
link below adn get started building mockups and prototypes today!
|
||||
link below and get started building mockups and prototypes today!
|
||||
|
||||
{{ public-uri }}/#/auth/verify-token?token={{token}}
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="robots" content="noindex,nofollow">
|
||||
<meta http-equiv="x-ua-compatible" content="ie=edge" />
|
||||
<title>penpot - error report {{id}}</title>
|
||||
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=JetBrains+Mono">
|
||||
|
||||
@@ -49,6 +49,7 @@
|
||||
|
||||
;; Create the application jar
|
||||
(spit "./target/dist/version.txt" version)
|
||||
|
||||
(-> ($ jar cvf "./target/dist/deps/app.jar" -C ~(first classpath-paths) ".") check)
|
||||
(-> ($ jar uvf "./target/dist/deps/app.jar" -C "./target/dist" "version.txt") check)
|
||||
(run! (fn [item]
|
||||
|
||||
@@ -1,8 +1,14 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
export PENPOT_ASSERTS_ENABLED=true
|
||||
export PENPOT_FLAGS="enable-asserts enable-audit-log $PENPOT_FLAGS"
|
||||
|
||||
export OPTIONS="-A:jmx-remote:dev \
|
||||
-J-Djava.util.logging.manager=org.apache.logging.log4j.jul.LogManager \
|
||||
-J-Dlog4j2.contextSelector=org.apache.logging.log4j.core.async.AsyncLoggerContextSelector \
|
||||
-J-Dlog4j2.configurationFile=log4j2-devenv.xml \
|
||||
-J-Dclojure.tools.logging.factory=clojure.tools.logging.impl/log4j2-factory \
|
||||
-J-XX:+UseShenandoahGC -J-XX:-OmitStackTraceInFastThrow -J-Xms50m -J-Xmx512m";
|
||||
|
||||
export OPTIONS="-A:jmx-remote:dev -J-Djava.util.logging.manager=org.apache.logging.log4j.jul.LogManager -J-Dlog4j2.configurationFile=log4j2-devenv.xml -J-XX:+UseZGC -J-XX:ConcGCThreads=1 -J-XX:-OmitStackTraceInFastThrow -J-Xms50m -J-Xmx512m";
|
||||
# export OPTIONS="$OPTIONS -J-XX:+UnlockDiagnosticVMOptions";
|
||||
# export OPTIONS="$OPTIONS -J-XX:-TieredCompilation -J-XX:CompileThreshold=10000";
|
||||
|
||||
|
||||
@@ -1,15 +1,23 @@
|
||||
#!/bin/sh
|
||||
#!/usr/bin/env bash
|
||||
|
||||
export PENPOT_ASSERTS_ENABLED=true
|
||||
export PENPOT_FLAGS="$PENPOT_FLAGS enable-asserts"
|
||||
|
||||
set -ex
|
||||
|
||||
if [ ! -e ~/.fixtures-loaded ]; then
|
||||
echo "Loading fixtures..."
|
||||
clojure -Adev -X:fn-fixtures
|
||||
touch ~/.fixtures-loaded
|
||||
if [ "$1" = "--watch" ]; then
|
||||
echo "Start Watch..."
|
||||
|
||||
clojure -A:dev -M -m app.main &
|
||||
PID=$!
|
||||
|
||||
npx nodemon \
|
||||
--watch src \
|
||||
--watch ../common \
|
||||
--ext "clj" \
|
||||
--signal SIGKILL \
|
||||
--exec 'echo "(user/restart)" | nc -N localhost 6062'
|
||||
|
||||
kill -9 $PID
|
||||
else
|
||||
clojure -A:dev -M -m app.main
|
||||
fi
|
||||
|
||||
clojure -A:dev -M -m app.main
|
||||
|
||||
|
||||
|
||||
@@ -1,258 +0,0 @@
|
||||
;; This Source Code Form is subject to the terms of the Mozilla Public
|
||||
;; License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
;;
|
||||
;; Copyright (c) UXBOX Labs SL
|
||||
|
||||
(ns app.cli.fixtures
|
||||
"A initial fixtures."
|
||||
(:require
|
||||
[app.common.pages :as cp]
|
||||
[app.common.uuid :as uuid]
|
||||
[app.db :as db]
|
||||
[app.main :as main]
|
||||
[app.rpc.mutations.profile :as profile]
|
||||
[app.util.blob :as blob]
|
||||
[app.util.logging :as l]
|
||||
[buddy.hashers :as hashers]
|
||||
[integrant.core :as ig]))
|
||||
|
||||
(defn- mk-uuid
|
||||
[prefix & args]
|
||||
(uuid/namespaced uuid/zero (apply str prefix (interpose "-" args))))
|
||||
|
||||
;; --- Profiles creation
|
||||
|
||||
(def password (hashers/derive "123123"))
|
||||
|
||||
(def preset-small
|
||||
{:num-teams 5
|
||||
:num-profiles 5
|
||||
:num-profiles-per-team 5
|
||||
:num-projects-per-team 5
|
||||
:num-files-per-project 5
|
||||
:num-draft-files-per-profile 10})
|
||||
|
||||
(defn- rng-ids
|
||||
[rng n max]
|
||||
(let [stream (->> (.longs rng 0 max)
|
||||
(.iterator)
|
||||
(iterator-seq))]
|
||||
(reduce (fn [acc item]
|
||||
(if (= (count acc) n)
|
||||
(reduced acc)
|
||||
(conj acc item)))
|
||||
#{}
|
||||
stream)))
|
||||
|
||||
(defn- rng-vec
|
||||
[rng vdata n]
|
||||
(let [ids (rng-ids rng n (count vdata))]
|
||||
(mapv #(nth vdata %) ids)))
|
||||
|
||||
(defn- rng-nth
|
||||
[rng vdata]
|
||||
(let [stream (->> (.longs rng 0 (count vdata))
|
||||
(.iterator)
|
||||
(iterator-seq))]
|
||||
(nth vdata (first stream))))
|
||||
|
||||
(defn- collect
|
||||
[f items]
|
||||
(reduce #(conj %1 (f %2)) [] items))
|
||||
|
||||
(defn- register-profile
|
||||
[conn params]
|
||||
(->> (#'profile/create-profile conn params)
|
||||
(#'profile/create-profile-relations conn)))
|
||||
|
||||
(defn impl-run
|
||||
[pool opts]
|
||||
(let [rng (java.util.Random. 1)]
|
||||
(letfn [(create-profile [conn index]
|
||||
(let [id (mk-uuid "profile" index)
|
||||
_ (l/info :action "create profile"
|
||||
:index index
|
||||
:id id)
|
||||
|
||||
prof (register-profile conn
|
||||
{:id id
|
||||
:fullname (str "Profile " index)
|
||||
:password "123123"
|
||||
:is-demo true
|
||||
:email (str "profile" index "@example.com")})
|
||||
team-id (:default-team-id prof)
|
||||
owner-id id]
|
||||
(let [project-ids (collect (partial create-project conn team-id owner-id)
|
||||
(range (:num-projects-per-team opts)))]
|
||||
(run! (partial create-files conn owner-id) project-ids))
|
||||
prof))
|
||||
|
||||
(create-profiles [conn]
|
||||
(l/info :action "create profiles")
|
||||
(collect (partial create-profile conn)
|
||||
(range (:num-profiles opts))))
|
||||
|
||||
(create-team [conn index]
|
||||
(let [id (mk-uuid "team" index)
|
||||
name (str "Team" index)]
|
||||
(l/info :action "create team"
|
||||
:index index
|
||||
:id id)
|
||||
(db/insert! conn :team {:id id
|
||||
:name name})
|
||||
id))
|
||||
|
||||
(create-teams [conn]
|
||||
(l/info :action "create teams")
|
||||
(collect (partial create-team conn)
|
||||
(range (:num-teams opts))))
|
||||
|
||||
(create-file [conn owner-id project-id index]
|
||||
(let [id (mk-uuid "file" project-id index)
|
||||
name (str "file" index)
|
||||
data (cp/make-file-data id)]
|
||||
(l/info :action "create file"
|
||||
:index index
|
||||
:id id)
|
||||
(db/insert! conn :file
|
||||
{:id id
|
||||
:data (blob/encode data)
|
||||
:project-id project-id
|
||||
:name name})
|
||||
(db/insert! conn :file-profile-rel
|
||||
{:file-id id
|
||||
:profile-id owner-id
|
||||
:is-owner true
|
||||
:is-admin true
|
||||
:can-edit true})
|
||||
id))
|
||||
|
||||
(create-files [conn owner-id project-id]
|
||||
(l/info :action "create files")
|
||||
(run! (partial create-file conn owner-id project-id)
|
||||
(range (:num-files-per-project opts))))
|
||||
|
||||
(create-project [conn team-id owner-id index]
|
||||
(let [id (if index
|
||||
(mk-uuid "project" team-id index)
|
||||
(mk-uuid "project" team-id))
|
||||
name (if index
|
||||
(str "project " index)
|
||||
"Drafts")
|
||||
is-default (nil? index)]
|
||||
(l/info :action "create project"
|
||||
:index index
|
||||
:id id)
|
||||
(db/insert! conn :project
|
||||
{:id id
|
||||
:team-id team-id
|
||||
:is-default is-default
|
||||
:name name})
|
||||
(db/insert! conn :project-profile-rel
|
||||
{:project-id id
|
||||
:profile-id owner-id
|
||||
:is-owner true
|
||||
:is-admin true
|
||||
:can-edit true})
|
||||
id))
|
||||
|
||||
(create-projects [conn team-id profile-ids]
|
||||
(l/info :action "create projects")
|
||||
(let [owner-id (rng-nth rng profile-ids)
|
||||
project-ids (conj
|
||||
(collect (partial create-project conn team-id owner-id)
|
||||
(range (:num-projects-per-team opts)))
|
||||
(create-project conn team-id owner-id nil))]
|
||||
(run! (partial create-files conn owner-id) project-ids)))
|
||||
|
||||
(assign-profile-to-team [conn team-id owner? profile-id]
|
||||
(db/insert! conn :team-profile-rel
|
||||
{:team-id team-id
|
||||
:profile-id profile-id
|
||||
:is-owner owner?
|
||||
:is-admin true
|
||||
:can-edit true}))
|
||||
|
||||
(setup-team [conn team-id profile-ids]
|
||||
(l/info :action "setup team"
|
||||
:team-id team-id
|
||||
:profile-ids (pr-str profile-ids))
|
||||
(assign-profile-to-team conn team-id true (first profile-ids))
|
||||
(run! (partial assign-profile-to-team conn team-id false)
|
||||
(rest profile-ids))
|
||||
(create-projects conn team-id profile-ids))
|
||||
|
||||
(assign-teams-and-profiles [conn teams profiles]
|
||||
(l/info :action "assign teams and profiles")
|
||||
(loop [team-id (first teams)
|
||||
teams (rest teams)]
|
||||
(when-not (nil? team-id)
|
||||
(let [n-profiles-team (:num-profiles-per-team opts)
|
||||
selected-profiles (rng-vec rng profiles n-profiles-team)]
|
||||
(setup-team conn team-id selected-profiles)
|
||||
(recur (first teams)
|
||||
(rest teams))))))
|
||||
|
||||
(create-draft-file [conn owner index]
|
||||
(let [owner-id (:id owner)
|
||||
id (mk-uuid "file" "draft" owner-id index)
|
||||
name (str "file" index)
|
||||
project-id (:default-project-id owner)
|
||||
data (cp/make-file-data id)]
|
||||
|
||||
(l/info :action "create draft file"
|
||||
:index index
|
||||
:id id)
|
||||
(db/insert! conn :file
|
||||
{:id id
|
||||
:data (blob/encode data)
|
||||
:project-id project-id
|
||||
:name name})
|
||||
(db/insert! conn :file-profile-rel
|
||||
{:file-id id
|
||||
:profile-id owner-id
|
||||
:is-owner true
|
||||
:is-admin true
|
||||
:can-edit true})
|
||||
id))
|
||||
|
||||
(create-draft-files [conn profile]
|
||||
(run! (partial create-draft-file conn profile)
|
||||
(range (:num-draft-files-per-profile opts))))
|
||||
]
|
||||
(db/with-atomic [conn pool]
|
||||
(let [profiles (create-profiles conn)
|
||||
teams (create-teams conn)]
|
||||
(assign-teams-and-profiles conn teams (map :id profiles))
|
||||
(run! (partial create-draft-files conn) profiles))))))
|
||||
|
||||
(defn run-in-system
|
||||
[system preset]
|
||||
(let [pool (:app.db/pool system)
|
||||
preset (if (map? preset)
|
||||
preset
|
||||
(case preset
|
||||
(nil "small" :small) preset-small
|
||||
;; "medium" preset-medium
|
||||
;; "big" preset-big
|
||||
preset-small))]
|
||||
(impl-run pool preset)))
|
||||
|
||||
(defn run
|
||||
[{:keys [preset] :or {preset :small}}]
|
||||
(let [config (select-keys main/system-config
|
||||
[:app.db/pool
|
||||
:app.telemetry/migrations
|
||||
:app.migrations/migrations
|
||||
:app.migrations/all
|
||||
:app.metrics/metrics])
|
||||
_ (ig/load-namespaces config)
|
||||
system (-> (ig/prep config)
|
||||
(ig/init))]
|
||||
(try
|
||||
(run-in-system system preset)
|
||||
(catch Exception e
|
||||
(l/error :hint "unhandled exception" :cause e))
|
||||
(finally
|
||||
(ig/halt! system)))))
|
||||
@@ -7,11 +7,11 @@
|
||||
(ns app.cli.manage
|
||||
"A manage cli api."
|
||||
(:require
|
||||
[app.common.logging :as l]
|
||||
[app.db :as db]
|
||||
[app.main :as main]
|
||||
[app.rpc.mutations.profile :as profile]
|
||||
[app.rpc.queries.profile :refer [retrieve-profile-data-by-email]]
|
||||
[app.util.logging :as l]
|
||||
[clojure.string :as str]
|
||||
[clojure.tools.cli :refer [parse-opts]]
|
||||
[integrant.core :as ig])
|
||||
|
||||
@@ -6,12 +6,12 @@
|
||||
|
||||
(ns app.cli.migrate-media
|
||||
(:require
|
||||
[app.common.logging :as l]
|
||||
[app.common.media :as cm]
|
||||
[app.config :as cf]
|
||||
[app.db :as db]
|
||||
[app.main :as main]
|
||||
[app.storage :as sto]
|
||||
[app.util.logging :as l]
|
||||
[cuerdas.core :as str]
|
||||
[datoteka.core :as fs]
|
||||
[integrant.core :as ig]))
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
(:require
|
||||
[app.common.data :as d]
|
||||
[app.common.exceptions :as ex]
|
||||
[app.common.flags :as flags]
|
||||
[app.common.spec :as us]
|
||||
[app.common.version :as v]
|
||||
[app.util.time :as dt]
|
||||
@@ -50,29 +51,21 @@
|
||||
:default-blob-version 3
|
||||
:loggers-zmq-uri "tcp://localhost:45556"
|
||||
|
||||
:asserts-enabled false
|
||||
|
||||
:public-uri "http://localhost:3449"
|
||||
:redis-uri "redis://redis/0"
|
||||
|
||||
:srepl-host "127.0.0.1"
|
||||
:srepl-port 6062
|
||||
|
||||
:storage-backend :fs
|
||||
|
||||
:storage-fs-directory "assets"
|
||||
:storage-s3-region :eu-central-1
|
||||
:storage-s3-bucket "penpot-devenv-assets-pre"
|
||||
|
||||
:feedback-destination "info@example.com"
|
||||
:feedback-enabled false
|
||||
:assets-storage-backend :assets-fs
|
||||
:storage-assets-fs-directory "assets"
|
||||
|
||||
:assets-path "/internal/assets/"
|
||||
|
||||
:rlimits-password 10
|
||||
:rlimits-image 2
|
||||
:rlimit-password 10
|
||||
:rlimit-image 2
|
||||
:rlimit-font 5
|
||||
|
||||
:smtp-enabled false
|
||||
:smtp-default-reply-to "Penpot <no-reply@example.com>"
|
||||
:smtp-default-from "Penpot <no-reply@example.com>"
|
||||
|
||||
@@ -82,10 +75,6 @@
|
||||
:profile-bounce-max-age (dt/duration {:days 7})
|
||||
:profile-bounce-threshold 10
|
||||
|
||||
:allow-demo-users true
|
||||
:registration-enabled true
|
||||
|
||||
:telemetry-enabled false
|
||||
:telemetry-uri "https://telemetry.penpot.app/"
|
||||
|
||||
:ldap-user-query "(|(uid=:username)(mail=:username))"
|
||||
@@ -95,27 +84,29 @@
|
||||
:ldap-attrs-photo "jpegPhoto"
|
||||
|
||||
;; a server prop key where initial project is stored.
|
||||
:initial-project-skey "initial-project"
|
||||
})
|
||||
:initial-project-skey "initial-project"})
|
||||
|
||||
(s/def ::audit-enabled ::us/boolean)
|
||||
(s/def ::audit-archive-enabled ::us/boolean)
|
||||
(s/def ::audit-archive-uri ::us/string)
|
||||
(s/def ::audit-archive-gc-enabled ::us/boolean)
|
||||
(s/def ::audit-archive-gc-max-age ::dt/duration)
|
||||
(s/def ::flags ::us/set-of-keywords)
|
||||
|
||||
;; DEPRECATED PROPERTIES: should be removed in 1.10
|
||||
(s/def ::registration-enabled ::us/boolean)
|
||||
(s/def ::smtp-enabled ::us/boolean)
|
||||
(s/def ::telemetry-enabled ::us/boolean)
|
||||
(s/def ::asserts-enabled ::us/boolean)
|
||||
;; END DEPRECATED
|
||||
|
||||
(s/def ::audit-log-archive-uri ::us/string)
|
||||
(s/def ::audit-log-gc-max-age ::dt/duration)
|
||||
|
||||
(s/def ::secret-key ::us/string)
|
||||
(s/def ::allow-demo-users ::us/boolean)
|
||||
(s/def ::asserts-enabled ::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 ::default-blob-version ::us/integer)
|
||||
(s/def ::error-report-webhook ::us/string)
|
||||
(s/def ::feedback-destination ::us/string)
|
||||
(s/def ::feedback-enabled ::us/boolean)
|
||||
(s/def ::feedback-token ::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)
|
||||
@@ -161,12 +152,11 @@
|
||||
(s/def ::public-uri ::us/string)
|
||||
(s/def ::redis-uri ::us/string)
|
||||
(s/def ::registration-domain-whitelist ::us/set-of-str)
|
||||
(s/def ::registration-enabled ::us/boolean)
|
||||
(s/def ::rlimits-image ::us/integer)
|
||||
(s/def ::rlimits-password ::us/integer)
|
||||
(s/def ::rlimit-font ::us/integer)
|
||||
(s/def ::rlimit-image ::us/integer)
|
||||
(s/def ::rlimit-password ::us/integer)
|
||||
(s/def ::smtp-default-from ::us/string)
|
||||
(s/def ::smtp-default-reply-to ::us/string)
|
||||
(s/def ::smtp-enabled ::us/boolean)
|
||||
(s/def ::smtp-host ::us/string)
|
||||
(s/def ::smtp-password (s/nilable ::us/string))
|
||||
(s/def ::smtp-port ::us/integer)
|
||||
@@ -175,32 +165,35 @@
|
||||
(s/def ::smtp-username (s/nilable ::us/string))
|
||||
(s/def ::srepl-host ::us/string)
|
||||
(s/def ::srepl-port ::us/integer)
|
||||
(s/def ::storage-backend ::us/keyword)
|
||||
(s/def ::storage-fs-directory ::us/string)
|
||||
(s/def ::storage-s3-bucket ::us/string)
|
||||
(s/def ::storage-s3-region ::us/keyword)
|
||||
(s/def ::telemetry-enabled ::us/boolean)
|
||||
(s/def ::assets-storage-backend ::us/keyword)
|
||||
(s/def ::fdata-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-fdata-s3-bucket ::us/string)
|
||||
(s/def ::storage-fdata-s3-region ::us/keyword)
|
||||
(s/def ::storage-fdata-s3-prefix ::us/string)
|
||||
(s/def ::telemetry-uri ::us/string)
|
||||
(s/def ::telemetry-with-taiga ::us/boolean)
|
||||
(s/def ::tenant ::us/string)
|
||||
|
||||
(s/def ::sentry-trace-sample-rate ::us/number)
|
||||
(s/def ::sentry-attach-stack-trace ::us/boolean)
|
||||
(s/def ::sentry-debug ::us/boolean)
|
||||
(s/def ::sentry-dsn ::us/string)
|
||||
|
||||
(s/def ::config
|
||||
(s/keys :opt-un [::secret-key
|
||||
::flags
|
||||
::allow-demo-users
|
||||
::audit-enabled
|
||||
::audit-archive-enabled
|
||||
::audit-archive-uri
|
||||
::audit-archive-gc-enabled
|
||||
::audit-archive-gc-max-age
|
||||
::asserts-enabled
|
||||
::audit-log-archive-uri
|
||||
::audit-log-gc-max-age
|
||||
::database-password
|
||||
::database-uri
|
||||
::database-username
|
||||
::default-blob-version
|
||||
::error-report-webhook
|
||||
::feedback-destination
|
||||
::feedback-enabled
|
||||
::feedback-token
|
||||
::user-feedback-destination
|
||||
::github-client-id
|
||||
::github-client-secret
|
||||
::gitlab-base-uri
|
||||
@@ -246,8 +239,13 @@
|
||||
::redis-uri
|
||||
::registration-domain-whitelist
|
||||
::registration-enabled
|
||||
::rlimits-image
|
||||
::rlimits-password
|
||||
::rlimit-font
|
||||
::rlimit-image
|
||||
::rlimit-password
|
||||
::sentry-dsn
|
||||
::sentry-debug
|
||||
::sentry-attach-stack-trace
|
||||
::sentry-trace-sample-rate
|
||||
::smtp-default-from
|
||||
::smtp-default-reply-to
|
||||
::smtp-enabled
|
||||
@@ -259,16 +257,31 @@
|
||||
::smtp-username
|
||||
::srepl-host
|
||||
::srepl-port
|
||||
::storage-backend
|
||||
::storage-fs-directory
|
||||
::storage-s3-bucket
|
||||
::storage-s3-region
|
||||
::assets-storage-backend
|
||||
::storage-assets-fs-directory
|
||||
::storage-assets-s3-bucket
|
||||
::storage-assets-s3-region
|
||||
::fdata-storage-backend
|
||||
::storage-fdata-s3-bucket
|
||||
::storage-fdata-s3-region
|
||||
::storage-fdata-s3-prefix
|
||||
::telemetry-enabled
|
||||
::telemetry-uri
|
||||
::telemetry-referer
|
||||
::telemetry-with-taiga
|
||||
::tenant]))
|
||||
|
||||
(def default-flags
|
||||
[:enable-backend-asserts
|
||||
:enable-backend-api-doc
|
||||
:enable-secure-session-cookies])
|
||||
|
||||
(defn- parse-flags
|
||||
[config]
|
||||
(flags/parse flags/default
|
||||
default-flags
|
||||
(:flags config)))
|
||||
|
||||
(defn read-env
|
||||
[prefix]
|
||||
(let [prefix (str prefix "-")
|
||||
@@ -295,11 +308,14 @@
|
||||
(println ";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;")))
|
||||
(throw e))))
|
||||
|
||||
(def version (v/parse (or (some-> (io/resource "version.txt")
|
||||
(slurp)
|
||||
(str/trim))
|
||||
"%version%")))
|
||||
(def config (atom (read-config)))
|
||||
(def version
|
||||
(v/parse (or (some-> (io/resource "version.txt")
|
||||
(slurp)
|
||||
(str/trim))
|
||||
"%version%")))
|
||||
|
||||
(def ^:dynamic config (read-config))
|
||||
(def ^:dynamic flags (parse-flags config))
|
||||
|
||||
(def deletion-delay
|
||||
(dt/duration {:days 7}))
|
||||
@@ -307,9 +323,9 @@
|
||||
(defn get
|
||||
"A configuration getter. Helps code be more testable."
|
||||
([key]
|
||||
(c/get @config key))
|
||||
(c/get config key))
|
||||
([key default]
|
||||
(c/get @config key default)))
|
||||
(c/get config key default)))
|
||||
|
||||
;; Set value for all new threads bindings.
|
||||
(alter-var-root #'*assert* (constantly (get :asserts-enabled)))
|
||||
(alter-var-root #'*assert* (constantly (contains? flags :backend-asserts)))
|
||||
|
||||
@@ -9,14 +9,15 @@
|
||||
[app.common.data :as d]
|
||||
[app.common.exceptions :as ex]
|
||||
[app.common.geom.point :as gpt]
|
||||
[app.common.logging :as l]
|
||||
[app.common.spec :as us]
|
||||
[app.common.transit :as t]
|
||||
[app.common.uuid :as uuid]
|
||||
[app.db.sql :as sql]
|
||||
[app.metrics :as mtx]
|
||||
[app.util.json :as json]
|
||||
[app.util.logging :as l]
|
||||
[app.util.migrations :as mg]
|
||||
[app.util.time :as dt]
|
||||
[app.util.transit :as t]
|
||||
[clojure.java.io :as io]
|
||||
[clojure.spec.alpha :as s]
|
||||
[integrant.core :as ig]
|
||||
@@ -45,28 +46,26 @@
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
|
||||
(declare instrument-jdbc!)
|
||||
(declare apply-migrations!)
|
||||
|
||||
(s/def ::name keyword?)
|
||||
(s/def ::uri ::us/not-empty-string)
|
||||
(s/def ::min-pool-size ::us/integer)
|
||||
(s/def ::max-pool-size ::us/integer)
|
||||
(s/def ::migrations map?)
|
||||
(s/def ::read-only ::us/boolean)
|
||||
|
||||
(defmethod ig/pre-init-spec ::pool [_]
|
||||
(s/keys :req-un [::uri ::name ::min-pool-size ::max-pool-size ::migrations ::mtx/metrics]))
|
||||
(s/keys :req-un [::uri ::name ::min-pool-size ::max-pool-size]
|
||||
:opt-un [::migrations ::mtx/metrics ::read-only]))
|
||||
|
||||
(defmethod ig/init-key ::pool
|
||||
[_ {:keys [migrations metrics] :as cfg}]
|
||||
(l/info :action "initialize connection pool"
|
||||
:name (d/name (:name cfg))
|
||||
:uri (:uri cfg))
|
||||
(instrument-jdbc! (:registry metrics))
|
||||
[_ {:keys [migrations metrics name] :as cfg}]
|
||||
(l/info :action "initialize connection pool" :name (d/name name) :uri (:uri cfg))
|
||||
(some-> metrics :registry instrument-jdbc!)
|
||||
|
||||
(let [pool (create-pool cfg)]
|
||||
(when (seq migrations)
|
||||
(with-open [conn ^AutoCloseable (open pool)]
|
||||
(mg/setup! conn)
|
||||
(doseq [[name steps] migrations]
|
||||
(mg/migrate! conn {:name (d/name name) :steps steps}))))
|
||||
(some->> (seq migrations) (apply-migrations! pool))
|
||||
pool))
|
||||
|
||||
(defmethod ig/halt-key! ::pool
|
||||
@@ -83,37 +82,50 @@
|
||||
:name "database_query_total"
|
||||
:help "An absolute counter of database queries."}))
|
||||
|
||||
(defn- apply-migrations!
|
||||
[pool migrations]
|
||||
(with-open [conn ^AutoCloseable (open pool)]
|
||||
(mg/setup! conn)
|
||||
(doseq [[name steps] migrations]
|
||||
(mg/migrate! conn {:name (d/name name) :steps steps}))))
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;; API & Impl
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
|
||||
(def initsql
|
||||
(str "SET statement_timeout = 120000;\n"
|
||||
"SET idle_in_transaction_session_timeout = 120000;"))
|
||||
(str "SET statement_timeout = 200000;\n"
|
||||
"SET idle_in_transaction_session_timeout = 200000;"))
|
||||
|
||||
(defn- create-datasource-config
|
||||
[{:keys [metrics] :as cfg}]
|
||||
[{:keys [metrics read-only] :or {read-only false} :as cfg}]
|
||||
(let [dburi (:uri cfg)
|
||||
username (:username cfg)
|
||||
password (:password cfg)
|
||||
config (HikariConfig.)
|
||||
mtf (PrometheusMetricsTrackerFactory. (:registry metrics))]
|
||||
config (HikariConfig.)]
|
||||
(doto config
|
||||
(.setJdbcUrl (str "jdbc:" dburi))
|
||||
(.setPoolName (d/name (:name cfg)))
|
||||
(.setAutoCommit true)
|
||||
(.setReadOnly false)
|
||||
(.setConnectionTimeout 8000) ;; 8seg
|
||||
(.setValidationTimeout 8000) ;; 8seg
|
||||
(.setIdleTimeout 120000) ;; 2min
|
||||
(.setMaxLifetime 1800000) ;; 30min
|
||||
(.setReadOnly read-only)
|
||||
(.setConnectionTimeout 10000) ;; 10seg
|
||||
(.setValidationTimeout 10000) ;; 10seg
|
||||
(.setIdleTimeout 120000) ;; 2min
|
||||
(.setMaxLifetime 1800000) ;; 30min
|
||||
(.setMinimumIdle (:min-pool-size cfg 0))
|
||||
(.setMaximumPoolSize (:max-pool-size cfg 30))
|
||||
(.setMetricsTrackerFactory mtf)
|
||||
(.setMaximumPoolSize (:max-pool-size cfg 50))
|
||||
(.setConnectionInitSql initsql)
|
||||
(.setInitializationFailTimeout -1))
|
||||
|
||||
;; When metrics namespace is provided
|
||||
(when metrics
|
||||
(->> (:registry metrics)
|
||||
(PrometheusMetricsTrackerFactory.)
|
||||
(.setMetricsTrackerFactory config)))
|
||||
|
||||
(when username (.setUsername config username))
|
||||
(when password (.setPassword config password))
|
||||
|
||||
config))
|
||||
|
||||
(defn pool?
|
||||
@@ -126,7 +138,7 @@
|
||||
[pool]
|
||||
(.isClosed ^HikariDataSource pool))
|
||||
|
||||
(defn- create-pool
|
||||
(defn create-pool
|
||||
[cfg]
|
||||
(let [dsc (create-datasource-config cfg)]
|
||||
(jdbc-dt/read-as-instant)
|
||||
@@ -221,14 +233,20 @@
|
||||
(sql/delete table params opts)
|
||||
(assoc opts :return-keys true))))
|
||||
|
||||
(defn- is-deleted?
|
||||
[{:keys [deleted-at]}]
|
||||
(and (dt/instant? deleted-at)
|
||||
(< (inst-ms deleted-at)
|
||||
(inst-ms (dt/now)))))
|
||||
|
||||
(defn get-by-params
|
||||
([ds table params]
|
||||
(get-by-params ds table params nil))
|
||||
([ds table params {:keys [uncheked] :or {uncheked false} :as opts}]
|
||||
([ds table params {:keys [check-not-found] :or {check-not-found true} :as opts}]
|
||||
(let [res (exec-one! ds (sql/select table params opts))]
|
||||
(when (and (not uncheked)
|
||||
(or (:deleted-at res) (not res)))
|
||||
(when (and check-not-found (or (not res) (is-deleted? res)))
|
||||
(ex/raise :type :not-found
|
||||
:table table
|
||||
:hint "database object not found"))
|
||||
res)))
|
||||
|
||||
@@ -245,8 +263,11 @@
|
||||
(exec! ds (sql/select table params opts))))
|
||||
|
||||
(defn pgobject?
|
||||
[v]
|
||||
(instance? PGobject v))
|
||||
([v]
|
||||
(instance? PGobject v))
|
||||
([v type]
|
||||
(and (instance? PGobject v)
|
||||
(= type (.getType ^PGobject v)))))
|
||||
|
||||
(defn pginterval?
|
||||
[v]
|
||||
@@ -257,13 +278,28 @@
|
||||
(instance? PGpoint v))
|
||||
|
||||
(defn pgarray?
|
||||
[v]
|
||||
(instance? PgArray v))
|
||||
([v] (instance? PgArray v))
|
||||
([v type]
|
||||
(and (instance? PgArray v)
|
||||
(= type (.getBaseTypeName ^PgArray v)))))
|
||||
|
||||
(defn pgarray-of-uuid?
|
||||
[v]
|
||||
(and (pgarray? v) (= "uuid" (.getBaseTypeName ^PgArray v))))
|
||||
|
||||
(defn decode-pgarray
|
||||
([v] (into [] (.getArray ^PgArray v)))
|
||||
([v in] (into in (.getArray ^PgArray v)))
|
||||
([v in xf] (into in xf (.getArray ^PgArray v))))
|
||||
|
||||
(defn pgarray->set
|
||||
[v]
|
||||
(set (.getArray ^PgArray v)))
|
||||
|
||||
(defn pgarray->vector
|
||||
[v]
|
||||
(vec (.getArray ^PgArray v)))
|
||||
|
||||
(defn pgpoint
|
||||
[p]
|
||||
(PGpoint. (:x p) (:y p)))
|
||||
@@ -275,7 +311,6 @@
|
||||
(.createArrayOf conn ^String type (into-array Object objects))
|
||||
(.createArrayOf conn ^String type objects))))
|
||||
|
||||
|
||||
(defn decode-pgpoint
|
||||
[^PGpoint v]
|
||||
(gpt/point (.-x v) (.-y v)))
|
||||
@@ -339,12 +374,18 @@
|
||||
(.setType "inet")
|
||||
(.setValue (str ip-addr))))
|
||||
|
||||
(defn decode-inet
|
||||
[^PGobject o]
|
||||
(if (= "inet" (.getType o))
|
||||
(.getValue o)
|
||||
nil))
|
||||
|
||||
(defn tjson
|
||||
"Encode as transit json."
|
||||
[data]
|
||||
(doto (org.postgresql.util.PGobject.)
|
||||
(.setType "jsonb")
|
||||
(.setValue (t/encode-verbose-str data))))
|
||||
(.setValue (t/encode-str data {:type :json-verbose}))))
|
||||
|
||||
(defn json
|
||||
"Encode as plain json."
|
||||
@@ -353,10 +394,23 @@
|
||||
(.setType "jsonb")
|
||||
(.setValue (json/encode-str data))))
|
||||
|
||||
(defn pgarray->set
|
||||
[v]
|
||||
(set (.getArray ^PgArray v)))
|
||||
;; --- Locks
|
||||
|
||||
(defn pgarray->vector
|
||||
[v]
|
||||
(vec (.getArray ^PgArray v)))
|
||||
(defn- xact-check-param
|
||||
[n]
|
||||
(cond
|
||||
(uuid? n) (uuid/get-word-high n)
|
||||
(int? n) n
|
||||
:else (throw (IllegalArgumentException. "uuid or number allowed"))))
|
||||
|
||||
(defn xact-lock!
|
||||
[conn n]
|
||||
(let [n (xact-check-param n)]
|
||||
(exec-one! conn ["select pg_advisory_xact_lock(?::bigint) as lock" n])
|
||||
true))
|
||||
|
||||
(defn xact-try-lock!
|
||||
[conn n]
|
||||
(let [n (xact-check-param n)
|
||||
row (exec-one! conn ["select pg_try_advisory_xact_lock(?::bigint) as lock" n])]
|
||||
(:lock row)))
|
||||
|
||||
@@ -43,8 +43,8 @@
|
||||
([table where-params opts]
|
||||
(let [opts (merge default-opts opts)
|
||||
opts (cond-> opts
|
||||
(:for-update opts)
|
||||
(assoc :suffix "FOR UPDATE"))]
|
||||
(:for-update opts) (assoc :suffix "FOR UPDATE")
|
||||
(:for-key-share opts) (assoc :suffix "FOR KEY SHARE"))]
|
||||
(sql/for-query table where-params opts))))
|
||||
|
||||
(defn update
|
||||
|
||||
@@ -7,12 +7,12 @@
|
||||
(ns app.emails
|
||||
"Main api for send emails."
|
||||
(:require
|
||||
[app.common.logging :as l]
|
||||
[app.common.spec :as us]
|
||||
[app.config :as cfg]
|
||||
[app.config :as cf]
|
||||
[app.db :as db]
|
||||
[app.db.sql :as sql]
|
||||
[app.util.emails :as emails]
|
||||
[app.util.logging :as l]
|
||||
[app.worker :as wrk]
|
||||
[clojure.spec.alpha :as s]
|
||||
[integrant.core :as ig]))
|
||||
@@ -54,10 +54,10 @@
|
||||
(defn allow-send-emails?
|
||||
[conn profile]
|
||||
(when-not (:is-muted profile false)
|
||||
(let [complaint-threshold (cfg/get :profile-complaint-threshold)
|
||||
complaint-max-age (cfg/get :profile-complaint-max-age)
|
||||
bounce-threshold (cfg/get :profile-bounce-threshold)
|
||||
bounce-max-age (cfg/get :profile-bounce-max-age)
|
||||
(let [complaint-threshold (cf/get :profile-complaint-threshold)
|
||||
complaint-max-age (cf/get :profile-complaint-max-age)
|
||||
bounce-threshold (cf/get :profile-bounce-threshold)
|
||||
bounce-max-age (cf/get :profile-bounce-max-age)
|
||||
|
||||
{:keys [complaints bounces] :as result}
|
||||
(db/exec-one! conn [sql:profile-complaint-report
|
||||
@@ -127,9 +127,9 @@
|
||||
(s/def :internal.emails.invite-to-team/token ::us/string)
|
||||
|
||||
(s/def ::invite-to-team
|
||||
(s/keys :keys [:internal.emails.invite-to-team/invited-by
|
||||
:internal.emails.invite-to-team/token
|
||||
:internal.emails.invite-to-team/team]))
|
||||
(s/keys :req-un [:internal.emails.invite-to-team/invited-by
|
||||
:internal.emails.invite-to-team/token
|
||||
:internal.emails.invite-to-team/team]))
|
||||
|
||||
(def invite-to-team
|
||||
"Teams member invitation email."
|
||||
@@ -140,19 +140,17 @@
|
||||
|
||||
(declare send-console!)
|
||||
|
||||
(s/def ::username ::cfg/smtp-username)
|
||||
(s/def ::password ::cfg/smtp-password)
|
||||
(s/def ::tls ::cfg/smtp-tls)
|
||||
(s/def ::ssl ::cfg/smtp-ssl)
|
||||
(s/def ::host ::cfg/smtp-host)
|
||||
(s/def ::port ::cfg/smtp-port)
|
||||
(s/def ::default-reply-to ::cfg/smtp-default-reply-to)
|
||||
(s/def ::default-from ::cfg/smtp-default-from)
|
||||
(s/def ::enabled ::cfg/smtp-enabled)
|
||||
(s/def ::username ::cf/smtp-username)
|
||||
(s/def ::password ::cf/smtp-password)
|
||||
(s/def ::tls ::cf/smtp-tls)
|
||||
(s/def ::ssl ::cf/smtp-ssl)
|
||||
(s/def ::host ::cf/smtp-host)
|
||||
(s/def ::port ::cf/smtp-port)
|
||||
(s/def ::default-reply-to ::cf/smtp-default-reply-to)
|
||||
(s/def ::default-from ::cf/smtp-default-from)
|
||||
|
||||
(defmethod ig/pre-init-spec ::sendmail-handler [_]
|
||||
(s/keys :req-un [::enabled]
|
||||
:opt-un [::username
|
||||
(s/keys :opt-un [::username
|
||||
::password
|
||||
::tls
|
||||
::ssl
|
||||
@@ -164,9 +162,12 @@
|
||||
(defmethod ig/init-key ::sendmail-handler
|
||||
[_ cfg]
|
||||
(fn [{:keys [props] :as task}]
|
||||
(if (:enabled cfg)
|
||||
(emails/send! cfg props)
|
||||
(send-console! cfg props))))
|
||||
(let [enabled? (or (contains? cf/flags :smtp)
|
||||
(cf/get :smtp-enabled)
|
||||
(:enabled task))]
|
||||
(if enabled?
|
||||
(emails/send! cfg props)
|
||||
(send-console! cfg props)))))
|
||||
|
||||
(defn- send-console!
|
||||
[cfg email]
|
||||
|
||||
@@ -8,11 +8,12 @@
|
||||
(:require
|
||||
[app.common.data :as d]
|
||||
[app.common.exceptions :as ex]
|
||||
[app.common.logging :as l]
|
||||
[app.common.spec :as us]
|
||||
[app.http.doc :as doc]
|
||||
[app.http.errors :as errors]
|
||||
[app.http.middleware :as middleware]
|
||||
[app.metrics :as mtx]
|
||||
[app.util.logging :as l]
|
||||
[clojure.spec.alpha :as s]
|
||||
[integrant.core :as ig]
|
||||
[reitit.ring :as rr]
|
||||
@@ -114,9 +115,14 @@
|
||||
(s/def ::storage map?)
|
||||
(s/def ::assets map?)
|
||||
(s/def ::feedback fn?)
|
||||
(s/def ::error-report-handler fn?)
|
||||
(s/def ::audit-http-handler fn?)
|
||||
|
||||
(defmethod ig/pre-init-spec ::router [_]
|
||||
(s/keys :req-un [::rpc ::session ::mtx/metrics ::oauth ::storage ::assets ::feedback]))
|
||||
(s/keys :req-un [::rpc ::session ::mtx/metrics
|
||||
::oauth ::storage ::assets ::feedback
|
||||
::error-report-handler
|
||||
::audit-http-handler]))
|
||||
|
||||
(defmethod ig/init-key ::router
|
||||
[_ {:keys [session rpc oauth metrics assets feedback] :as cfg}]
|
||||
@@ -136,7 +142,8 @@
|
||||
["/webhooks"
|
||||
["/sns" {:post (:sns-webhook cfg)}]]
|
||||
|
||||
["/api" {:middleware [[middleware/etag]
|
||||
["/api" {:middleware [[middleware/cors]
|
||||
[middleware/etag]
|
||||
[middleware/format-response-body]
|
||||
[middleware/params]
|
||||
[middleware/multipart-params]
|
||||
@@ -145,12 +152,16 @@
|
||||
[middleware/errors errors/handle]
|
||||
[middleware/cookies]]}
|
||||
|
||||
["/_doc" {:get (doc/handler rpc)}]
|
||||
|
||||
["/feedback" {:middleware [(:middleware session)]
|
||||
:post feedback}]
|
||||
|
||||
["/auth/oauth/:provider" {:post (:handler oauth)}]
|
||||
["/auth/oauth/:provider/callback" {:get (:callback-handler oauth)}]
|
||||
|
||||
["/audit/events" {:middleware [(:middleware session)]
|
||||
:post (:audit-http-handler cfg)}]
|
||||
|
||||
["/rpc" {:middleware [(:middleware session)]}
|
||||
["/query/:type" {:get (:query-handler rpc)
|
||||
:post (:query-handler rpc)}]
|
||||
|
||||
@@ -49,7 +49,7 @@
|
||||
{:status 200
|
||||
:headers {"content-type" (:content-type mdata)
|
||||
"cache-control" (str "max-age=" (inst-ms cache-max-age))}
|
||||
:body (sto/get-object-data storage obj)}
|
||||
:body (sto/get-object-bytes storage obj)}
|
||||
|
||||
:s3
|
||||
(let [url (sto/get-object-url storage obj {:max-age signature-max-age})]
|
||||
|
||||
@@ -8,10 +8,10 @@
|
||||
"AWS SNS webhook handler for bounces."
|
||||
(:require
|
||||
[app.common.exceptions :as ex]
|
||||
[app.common.logging :as l]
|
||||
[app.db :as db]
|
||||
[app.db.sql :as sql]
|
||||
[app.util.http :as http]
|
||||
[app.util.logging :as l]
|
||||
[clojure.spec.alpha :as s]
|
||||
[cuerdas.core :as str]
|
||||
[integrant.core :as ig]
|
||||
|
||||
53
backend/src/app/http/doc.clj
Normal file
53
backend/src/app/http/doc.clj
Normal file
@@ -0,0 +1,53 @@
|
||||
;; This Source Code Form is subject to the terms of the Mozilla Public
|
||||
;; License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
;;
|
||||
;; Copyright (c) UXBOX Labs SL
|
||||
|
||||
(ns app.http.doc
|
||||
"API autogenerated documentation."
|
||||
(:require
|
||||
[app.common.data :as d]
|
||||
[app.config :as cf]
|
||||
[app.util.services :as sv]
|
||||
[app.util.template :as tmpl]
|
||||
[clojure.java.io :as io]
|
||||
[clojure.spec.alpha :as s]
|
||||
[pretty-spec.core :as ps]))
|
||||
|
||||
(defn get-spec-str
|
||||
[k]
|
||||
(with-out-str
|
||||
(ps/pprint (s/form k)
|
||||
{:ns-aliases {"clojure.spec.alpha" "s"
|
||||
"clojure.core.specs.alpha" "score"
|
||||
"clojure.core" nil}})))
|
||||
|
||||
(defn prepare-context
|
||||
[rpc]
|
||||
(letfn [(gen-doc [type [name f]]
|
||||
(let [mdata (meta f)]
|
||||
;; (prn name mdata)
|
||||
{:type (d/name type)
|
||||
:name (d/name name)
|
||||
:auth (:auth mdata true)
|
||||
:docs (::sv/docs mdata)
|
||||
:spec (get-spec-str (::sv/spec mdata))}))]
|
||||
{:query-methods
|
||||
(into []
|
||||
(map (partial gen-doc :query))
|
||||
(->> rpc :methods :query (sort-by first)))
|
||||
:mutation-methods
|
||||
(into []
|
||||
(map (partial gen-doc :mutation))
|
||||
(->> rpc :methods :mutation (sort-by first)))}))
|
||||
|
||||
(defn handler
|
||||
[rpc]
|
||||
(let [context (prepare-context rpc)]
|
||||
(if (contains? cf/flags :backend-api-doc)
|
||||
(fn [_]
|
||||
{:status 200
|
||||
:body (-> (io/resource "api-doc.tmpl")
|
||||
(tmpl/render context))})
|
||||
(constantly {:status 404 :body ""}))))
|
||||
@@ -7,31 +7,57 @@
|
||||
(ns app.http.errors
|
||||
"A errors handling for the http server."
|
||||
(:require
|
||||
[app.common.data :as d]
|
||||
[app.common.exceptions :as ex]
|
||||
[app.common.logging :as l]
|
||||
[app.common.uuid :as uuid]
|
||||
[app.util.logging :as l]
|
||||
[cuerdas.core :as str]
|
||||
[expound.alpha :as expound]))
|
||||
[clojure.pprint]
|
||||
[cuerdas.core :as str]))
|
||||
|
||||
(defn- explain-error
|
||||
[error]
|
||||
(with-out-str
|
||||
(expound/printer (:data error))))
|
||||
(defn- parse-client-ip
|
||||
[{:keys [headers] :as request}]
|
||||
(or (some-> (get headers "x-forwarded-for") (str/split ",") first)
|
||||
(get headers "x-real-ip")
|
||||
(get request :remote-addr)))
|
||||
|
||||
|
||||
(defn- simple-prune
|
||||
([s] (simple-prune s (* 1024 1024)))
|
||||
([s max-length]
|
||||
(if (> (count s) max-length)
|
||||
(str (subs s 0 max-length) " [...]")
|
||||
s)))
|
||||
|
||||
(defn- stringify-data
|
||||
[data]
|
||||
(binding [clojure.pprint/*print-right-margin* 200]
|
||||
(let [result (with-out-str (clojure.pprint/pprint data))]
|
||||
(simple-prune result (* 1024 1024)))))
|
||||
|
||||
(defn get-error-context
|
||||
[request error]
|
||||
(let [edata (ex-data error)]
|
||||
(merge
|
||||
{:id (uuid/next)
|
||||
:path (:uri request)
|
||||
:method (:request-method request)
|
||||
:params (:params request)
|
||||
:data edata}
|
||||
(let [data (ex-data error)]
|
||||
(d/without-nils
|
||||
(merge
|
||||
{:id (str (uuid/next))
|
||||
:path (str (:uri request))
|
||||
:method (name (:request-method request))
|
||||
:hint (or (:hint data) (ex-message error))
|
||||
:params (stringify-data (:params request))
|
||||
:data (stringify-data (dissoc data :explain))
|
||||
:ip-addr (parse-client-ip request)
|
||||
:explain (str/prune (:explain data) (* 1024 1024) "[...]")}
|
||||
|
||||
(when-let [id (:profile-id request)]
|
||||
{:profile-id id})
|
||||
|
||||
(let [headers (:headers request)]
|
||||
{:user-agent (get headers "user-agent")
|
||||
:frontend-version (get headers "x-frontend-version" "unknown")})
|
||||
(when (and (map? edata) (:data edata))
|
||||
{:explain (explain-error edata)}))))
|
||||
|
||||
(when (map? data)
|
||||
{:error-type (:type data)
|
||||
:error-code (:code data)})))))
|
||||
|
||||
(defmulti handle-exception
|
||||
(fn [err & _rest]
|
||||
@@ -43,7 +69,6 @@
|
||||
[err _]
|
||||
{:status 401 :body (ex-data err)})
|
||||
|
||||
|
||||
(defmethod handle-exception :restriction
|
||||
[err _]
|
||||
{:status 400 :body (ex-data err)})
|
||||
@@ -57,13 +82,10 @@
|
||||
{:status 400
|
||||
:headers {"content-type" "text/html"}
|
||||
:body (str "<pre style='font-size:16px'>"
|
||||
(explain-error edata)
|
||||
(:explain edata)
|
||||
"</pre>\n")}
|
||||
{:status 400
|
||||
:body (cond-> edata
|
||||
(map? (:data edata))
|
||||
(-> (assoc :explain (explain-error edata))
|
||||
(dissoc :data)))})))
|
||||
:body (dissoc edata :data)})))
|
||||
|
||||
(defmethod handle-exception :assertion
|
||||
[error request]
|
||||
@@ -77,9 +99,7 @@
|
||||
{:status 500
|
||||
:body {:type :server-error
|
||||
:code :assertion
|
||||
:data (-> edata
|
||||
(assoc :explain (explain-error edata))
|
||||
(dissoc :data))}}))
|
||||
:data (dissoc edata :data)}}))
|
||||
|
||||
(defmethod handle-exception :not-found
|
||||
[err _]
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
[app.common.data :as d]
|
||||
[app.common.exceptions :as ex]
|
||||
[app.common.spec :as us]
|
||||
[app.config :as cfg]
|
||||
[app.config :as cf]
|
||||
[app.db :as db]
|
||||
[app.emails :as eml]
|
||||
[app.rpc.queries.profile :as profile]
|
||||
@@ -24,8 +24,8 @@
|
||||
|
||||
(defmethod ig/init-key ::handler
|
||||
[_ {:keys [pool] :as scfg}]
|
||||
(let [ftoken (cfg/get :feedback-token ::no-token)
|
||||
enabled (cfg/get :feedback-enabled)]
|
||||
(let [ftoken (cf/get :feedback-token ::no-token)
|
||||
enabled (contains? cf/flags :user-feedback)]
|
||||
(fn [{:keys [profile-id] :as request}]
|
||||
(let [token (get-in request [:headers "x-feedback-token"])
|
||||
params (d/merge (:params request)
|
||||
@@ -58,9 +58,10 @@
|
||||
(defn send-feedback
|
||||
[pool profile params]
|
||||
(let [params (us/conform ::feedback params)
|
||||
destination (cfg/get :feedback-destination)]
|
||||
destination (cf/get :feedback-destination)]
|
||||
(eml/send! {::eml/conn pool
|
||||
::eml/factory eml/feedback
|
||||
:from destination
|
||||
:to destination
|
||||
:profile profile
|
||||
:reply-to (:from params)
|
||||
|
||||
@@ -6,10 +6,11 @@
|
||||
|
||||
(ns app.http.middleware
|
||||
(:require
|
||||
[app.common.logging :as l]
|
||||
[app.common.transit :as t]
|
||||
[app.config :as cf]
|
||||
[app.metrics :as mtx]
|
||||
[app.util.json :as json]
|
||||
[app.util.logging :as l]
|
||||
[app.util.transit :as t]
|
||||
[buddy.core.codecs :as bc]
|
||||
[buddy.core.hash :as bh]
|
||||
[clojure.java.io :as io]
|
||||
@@ -74,16 +75,14 @@
|
||||
:compile (constantly wrap-parse-request-body)})
|
||||
|
||||
(defn- impl-format-response-body
|
||||
[response]
|
||||
[response _request]
|
||||
(let [body (:body response)
|
||||
type :json-verbose]
|
||||
opts {:type :json}]
|
||||
(cond
|
||||
(coll? body)
|
||||
(-> response
|
||||
(assoc :body (t/encode body {:type type}))
|
||||
(update :headers assoc
|
||||
"content-type"
|
||||
"application/transit+json"))
|
||||
(update :headers assoc "content-type" "application/transit+json")
|
||||
(assoc :body (t/encode body opts)))
|
||||
|
||||
(nil? body)
|
||||
(assoc response :status 204 :body "")
|
||||
@@ -96,7 +95,7 @@
|
||||
(fn [request]
|
||||
(let [response (handler request)]
|
||||
(cond-> response
|
||||
(map? response) (impl-format-response-body)))))
|
||||
(map? response) (impl-format-response-body request)))))
|
||||
|
||||
(def format-response-body
|
||||
{:name ::format-response-body
|
||||
@@ -178,3 +177,29 @@
|
||||
:uri (str (:uri request) (when qstring (str "?" qstring)))
|
||||
:method (name (:request-method request)))
|
||||
(handler request)))))
|
||||
|
||||
(defn- wrap-cors
|
||||
[handler]
|
||||
(if-not (contains? cf/flags :cors)
|
||||
handler
|
||||
(letfn [(add-cors-headers [response request]
|
||||
(-> response
|
||||
(update
|
||||
:headers
|
||||
(fn [headers]
|
||||
(-> headers
|
||||
(assoc "access-control-allow-origin" (get-in request [:headers "origin"]))
|
||||
(assoc "access-control-allow-methods" "GET,POST,DELETE,OPTIONS,PUT,HEAD,PATCH")
|
||||
(assoc "access-control-allow-credentials" "true")
|
||||
(assoc "access-control-expose-headers" "x-requested-with, content-type, cookie")
|
||||
(assoc "access-control-allow-headers" "x-frontend-version, content-type, accept, x-requested-width"))))))]
|
||||
(fn [request]
|
||||
(if (= (:request-method request) :options)
|
||||
(-> {:status 200 :body ""}
|
||||
(add-cors-headers request))
|
||||
(let [response (handler request)]
|
||||
(add-cors-headers response request)))))))
|
||||
|
||||
(def cors
|
||||
{:name ::cors
|
||||
:compile (constantly wrap-cors)})
|
||||
|
||||
@@ -6,12 +6,16 @@
|
||||
|
||||
(ns app.http.oauth
|
||||
(:require
|
||||
[app.common.data :as d]
|
||||
[app.common.exceptions :as ex]
|
||||
[app.common.logging :as l]
|
||||
[app.common.spec :as us]
|
||||
[app.common.uri :as u]
|
||||
[app.config :as cf]
|
||||
[app.db :as db]
|
||||
[app.loggers.audit :as audit]
|
||||
[app.rpc.queries.profile :as profile]
|
||||
[app.util.http :as http]
|
||||
[app.util.logging :as l]
|
||||
[app.util.time :as dt]
|
||||
[clojure.data.json :as json]
|
||||
[clojure.set :as set]
|
||||
@@ -19,36 +23,6 @@
|
||||
[cuerdas.core :as str]
|
||||
[integrant.core :as ig]))
|
||||
|
||||
(defn redirect-response
|
||||
[uri]
|
||||
{:status 302
|
||||
:headers {"location" (str uri)}
|
||||
:body ""})
|
||||
|
||||
(defn generate-error-redirect-uri
|
||||
[cfg]
|
||||
(-> (u/uri (:public-uri cfg))
|
||||
(assoc :path "/#/auth/login")
|
||||
(assoc :query (u/map->query-string {:error "unable-to-auth"}))))
|
||||
|
||||
(defn register-profile
|
||||
[{:keys [rpc] :as cfg} info]
|
||||
(let [method-fn (get-in rpc [:methods :mutation :login-or-register])
|
||||
profile (method-fn info)]
|
||||
(cond-> profile
|
||||
(some? (:invitation-token info))
|
||||
(assoc :invitation-token (:invitation-token info)))))
|
||||
|
||||
(defn generate-redirect-uri
|
||||
[{:keys [tokens] :as cfg} profile]
|
||||
(let [token (or (:invitation-token profile)
|
||||
(tokens :generate {:iss :auth
|
||||
:exp (dt/in-future "15m")
|
||||
:profile-id (:id profile)}))]
|
||||
(-> (u/uri (:public-uri cfg))
|
||||
(assoc :path "/#/auth/verify-token")
|
||||
(assoc :query (u/map->query-string {:token token})))))
|
||||
|
||||
(defn- build-redirect-uri
|
||||
[{:keys [provider] :as cfg}]
|
||||
(let [public (u/uri (:public-uri cfg))]
|
||||
@@ -84,10 +58,16 @@
|
||||
{:token (get data "access_token")
|
||||
:type (get data "token_type")})))
|
||||
(catch Exception e
|
||||
(l/error :hint "unexpected error on retrieve-access-token"
|
||||
:cause e)
|
||||
(l/warn :hint "unexpected error on retrieve-access-token" :cause e)
|
||||
nil)))
|
||||
|
||||
(defn- qualify-props
|
||||
[provider props]
|
||||
(reduce-kv (fn [result k v]
|
||||
(assoc result (keyword (:name provider) (name k)) v))
|
||||
{}
|
||||
props))
|
||||
|
||||
(defn- retrieve-user-info
|
||||
[{:keys [provider] :as cfg} tdata]
|
||||
(try
|
||||
@@ -102,11 +82,10 @@
|
||||
{:backend (:name provider)
|
||||
:email (:email info)
|
||||
:fullname (:name info)
|
||||
:props (dissoc info :name :email)})))
|
||||
|
||||
:props (->> (dissoc info :name :email)
|
||||
(qualify-props provider))})))
|
||||
(catch Exception e
|
||||
(l/error :hint "unexpected exception on retrieve-user-info"
|
||||
:cause e)
|
||||
(l/warn :hint "unexpected exception on retrieve-user-info" :cause e)
|
||||
nil)))
|
||||
|
||||
(s/def ::backend ::us/not-empty-string)
|
||||
@@ -146,6 +125,7 @@
|
||||
(string? roles) (into #{} (str/words roles))
|
||||
(vector? roles) (into #{} roles)
|
||||
:else #{}))]
|
||||
|
||||
;; check if profile has a configured set of roles
|
||||
(when-not (set/subset? provider-roles profile-roles)
|
||||
(ex/raise :type :internal
|
||||
@@ -163,22 +143,79 @@
|
||||
|
||||
;; --- HTTP HANDLERS
|
||||
|
||||
(defn extract-props
|
||||
(defn extract-utm-props
|
||||
"Extracts additional data from user params."
|
||||
[params]
|
||||
(reduce-kv (fn [params k v]
|
||||
(let [sk (name k)]
|
||||
(cond-> params
|
||||
(or (str/starts-with? sk "pm_")
|
||||
(str/starts-with? sk "pm-")
|
||||
(str/starts-with? sk "utm_"))
|
||||
(assoc (-> sk str/kebab keyword) v))))
|
||||
(str/starts-with? sk "utm_")
|
||||
(assoc (->> sk str/kebab (keyword "penpot")) v))))
|
||||
{}
|
||||
params))
|
||||
|
||||
(defn- retrieve-profile
|
||||
[{:keys [pool] :as cfg} info]
|
||||
(with-open [conn (db/open pool)]
|
||||
(some->> (:email info)
|
||||
(profile/retrieve-profile-data-by-email conn)
|
||||
(profile/populate-additional-data conn)
|
||||
(profile/decode-profile-row))))
|
||||
|
||||
(defn- redirect-response
|
||||
[uri]
|
||||
{:status 302
|
||||
:headers {"location" (str uri)}
|
||||
:body ""})
|
||||
|
||||
(defn- generate-error-redirect
|
||||
[cfg error]
|
||||
(let [uri (-> (u/uri (:public-uri cfg))
|
||||
(assoc :path "/#/auth/login")
|
||||
(assoc :query (u/map->query-string {:error "unable-to-auth" :hint (ex-message error)})))]
|
||||
(redirect-response uri)))
|
||||
|
||||
(defn- generate-redirect
|
||||
[{:keys [tokens session audit] :as cfg} request info profile]
|
||||
(if profile
|
||||
(let [sxf ((:create session) (:id profile))
|
||||
token (or (:invitation-token info)
|
||||
(tokens :generate {:iss :auth
|
||||
:exp (dt/in-future "15m")
|
||||
:profile-id (:id profile)}))
|
||||
params {:token token}
|
||||
|
||||
uri (-> (u/uri (:public-uri cfg))
|
||||
(assoc :path "/#/auth/verify-token")
|
||||
(assoc :query (u/map->query-string params)))]
|
||||
|
||||
(when (fn? audit)
|
||||
(audit :cmd :submit
|
||||
:type "mutation"
|
||||
:name "login"
|
||||
:profile-id (:id profile)
|
||||
:ip-addr (audit/parse-client-ip request)
|
||||
:props (audit/profile->props profile)))
|
||||
|
||||
(->> (redirect-response uri)
|
||||
(sxf request)))
|
||||
(let [info (assoc info
|
||||
:iss :prepared-register
|
||||
:is-active true
|
||||
:exp (dt/in-future {:hours 48}))
|
||||
token (tokens :generate info)
|
||||
params (d/without-nils
|
||||
{:token token
|
||||
:fullname (:fullname info)})
|
||||
uri (-> (u/uri (:public-uri cfg))
|
||||
(assoc :path "/#/auth/register/validate")
|
||||
(assoc :query (u/map->query-string params)))]
|
||||
(redirect-response uri))))
|
||||
|
||||
(defn- auth-handler
|
||||
[{:keys [tokens] :as cfg} {:keys [params] :as request}]
|
||||
(let [invitation (:invitation-token params)
|
||||
props (extract-props params)
|
||||
props (extract-utm-props params)
|
||||
state (tokens :generate
|
||||
{:iss :oauth
|
||||
:invitation-token invitation
|
||||
@@ -189,17 +226,15 @@
|
||||
:body {:redirect-uri uri}}))
|
||||
|
||||
(defn- callback-handler
|
||||
[{:keys [session] :as cfg} request]
|
||||
[cfg request]
|
||||
(try
|
||||
(let [info (retrieve-info cfg request)
|
||||
profile (register-profile cfg info)
|
||||
uri (generate-redirect-uri cfg profile)
|
||||
sxf ((:create session) (:id profile))]
|
||||
(->> (redirect-response uri)
|
||||
(sxf request)))
|
||||
(catch Exception _e
|
||||
(-> (generate-error-redirect-uri cfg)
|
||||
(redirect-response)))))
|
||||
(let [info (retrieve-info cfg request)
|
||||
profile (retrieve-profile cfg info)]
|
||||
(generate-redirect cfg request info profile))
|
||||
(catch Exception e
|
||||
(l/warn :hint "error on oauth process"
|
||||
:cause e)
|
||||
(generate-error-redirect cfg e))))
|
||||
|
||||
;; --- INIT
|
||||
|
||||
@@ -210,8 +245,8 @@
|
||||
(s/def ::tokens fn?)
|
||||
(s/def ::rpc map?)
|
||||
|
||||
(defmethod ig/pre-init-spec :app.http.oauth/handlers [_]
|
||||
(s/keys :req-un [::public-uri ::session ::tokens ::rpc]))
|
||||
(defmethod ig/pre-init-spec ::handler [_]
|
||||
(s/keys :req-un [::public-uri ::session ::tokens ::rpc ::db/pool]))
|
||||
|
||||
(defn wrap-handler
|
||||
[cfg handler]
|
||||
@@ -225,7 +260,7 @@
|
||||
(-> (assoc @cfg :provider provider)
|
||||
(handler request)))))
|
||||
|
||||
(defmethod ig/init-key :app.http.oauth/handlers
|
||||
(defmethod ig/init-key ::handler
|
||||
[_ cfg]
|
||||
(let [cfg (initialize cfg)]
|
||||
{:handler (wrap-handler cfg auth-handler)
|
||||
|
||||
@@ -8,11 +8,11 @@
|
||||
(:require
|
||||
[app.common.data :as d]
|
||||
[app.common.exceptions :as ex]
|
||||
[app.common.logging :as l]
|
||||
[app.config :as cfg]
|
||||
[app.db :as db]
|
||||
[app.metrics :as mtx]
|
||||
[app.util.async :as aa]
|
||||
[app.util.logging :as l]
|
||||
[app.util.time :as dt]
|
||||
[app.worker :as wrk]
|
||||
[clojure.core.async :as a]
|
||||
@@ -53,7 +53,13 @@
|
||||
|
||||
(defn- add-cookies
|
||||
[response {:keys [id] :as session}]
|
||||
(assoc response :cookies {cookie-name {:path "/" :http-only true :value id}}))
|
||||
(let [cors? (contains? cfg/flags :cors)
|
||||
secure? (contains? cfg/flags :secure-session-cookies)]
|
||||
(assoc response :cookies {cookie-name {:path "/"
|
||||
:http-only true
|
||||
:value id
|
||||
:same-site (if cors? :none :strict)
|
||||
:secure secure?}})))
|
||||
|
||||
(defn- clear-cookies
|
||||
[response]
|
||||
@@ -140,10 +146,13 @@
|
||||
(when-let [[reason batch] (a/<! input)]
|
||||
(let [result (a/<! (update-sessions cfg batch))]
|
||||
(mcnt :inc)
|
||||
(if (ex/exception? result)
|
||||
(cond
|
||||
(ex/exception? result)
|
||||
(l/error :task "updater"
|
||||
:hint "unexpected error on update sessions"
|
||||
:cause result)
|
||||
|
||||
(= :size reason)
|
||||
(l/debug :task "updater"
|
||||
:action "update sessions"
|
||||
:reason (name reason)
|
||||
|
||||
@@ -7,21 +7,37 @@
|
||||
(ns app.loggers.audit
|
||||
"Services related to the user activity (audit log)."
|
||||
(:require
|
||||
[app.common.data :as d]
|
||||
[app.common.exceptions :as ex]
|
||||
[app.common.logging :as l]
|
||||
[app.common.spec :as us]
|
||||
[app.common.transit :as t]
|
||||
[app.common.uuid :as uuid]
|
||||
[app.config :as cf]
|
||||
[app.db :as db]
|
||||
[app.util.async :as aa]
|
||||
[app.util.http :as http]
|
||||
[app.util.logging :as l]
|
||||
[app.util.time :as dt]
|
||||
[app.util.transit :as t]
|
||||
[app.worker :as wrk]
|
||||
[clojure.core.async :as a]
|
||||
[clojure.spec.alpha :as s]
|
||||
[cuerdas.core :as str]
|
||||
[integrant.core :as ig]
|
||||
[lambdaisland.uri :as u]))
|
||||
[lambdaisland.uri :as u]
|
||||
[promesa.exec :as px]))
|
||||
|
||||
(defn parse-client-ip
|
||||
[{:keys [headers] :as request}]
|
||||
(or (some-> (get headers "x-forwarded-for") (str/split ",") first)
|
||||
(get headers "x-real-ip")
|
||||
(get request :remote-addr)))
|
||||
|
||||
(defn profile->props
|
||||
[profile]
|
||||
(-> profile
|
||||
(select-keys [:is-active :is-muted :auth-backend :email :default-team-id :default-project-id :fullname :lang])
|
||||
(merge (:props profile))
|
||||
(d/without-nils)))
|
||||
|
||||
(defn clean-props
|
||||
[{:keys [profile-id] :as event}]
|
||||
@@ -50,8 +66,69 @@
|
||||
(assoc k (name v))))
|
||||
{}
|
||||
props))]
|
||||
|
||||
(update event :props #(-> % clean-common clean-profile-id clean-complex-data))))
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;; HTTP Handler
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
|
||||
(declare persist-http-events)
|
||||
|
||||
(s/def ::profile-id ::us/uuid)
|
||||
(s/def ::name ::us/string)
|
||||
(s/def ::type ::us/string)
|
||||
(s/def ::props (s/map-of ::us/keyword any?))
|
||||
(s/def ::timestamp dt/instant?)
|
||||
(s/def ::context (s/map-of ::us/keyword any?))
|
||||
|
||||
(s/def ::event
|
||||
(s/keys :req-un [::type ::name ::props ::timestamp ::profile-id]
|
||||
:opt-un [::context]))
|
||||
|
||||
(s/def ::events (s/every ::event))
|
||||
|
||||
(defmethod ig/init-key ::http-handler
|
||||
[_ {:keys [executor] :as cfg}]
|
||||
(fn [{:keys [params profile-id] :as request}]
|
||||
(when (contains? cf/flags :audit-log)
|
||||
(let [events (->> (:events params)
|
||||
(remove #(not= profile-id (:profile-id %)))
|
||||
(us/conform ::events))
|
||||
ip-addr (parse-client-ip request)
|
||||
cfg (-> cfg
|
||||
(assoc :source "frontend")
|
||||
(assoc :events events)
|
||||
(assoc :ip-addr ip-addr))]
|
||||
(px/run! executor #(persist-http-events cfg))))
|
||||
{:status 204 :body ""}))
|
||||
|
||||
(defn- persist-http-events
|
||||
[{:keys [pool events ip-addr source] :as cfg}]
|
||||
(try
|
||||
(let [columns [:id :name :source :type :tracked-at :profile-id :ip-addr :props :context]
|
||||
prepare-xf (map (fn [event]
|
||||
[(uuid/next)
|
||||
(:name event)
|
||||
source
|
||||
(:type event)
|
||||
(:timestamp event)
|
||||
(:profile-id event)
|
||||
(db/inet ip-addr)
|
||||
(db/tjson (:props event))
|
||||
(db/tjson (d/without-nils (:context event)))]))
|
||||
events (us/conform ::events events)]
|
||||
(when (seq events)
|
||||
(->> (into [] prepare-xf events)
|
||||
(db/insert-multi! pool :audit-log columns))))
|
||||
(catch Throwable e
|
||||
(let [xdata (ex-data e)]
|
||||
(if (= :spec-validation (:code xdata))
|
||||
(l/error ::l/raw (str "spec validation on persist-events:\n"
|
||||
(:explain xdata)))
|
||||
(l/error :hint "error on persist-events"
|
||||
:cause e))))))
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;; Collector
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
@@ -61,10 +138,9 @@
|
||||
;; an external storage and data cleared.
|
||||
|
||||
(declare persist-events)
|
||||
(s/def ::enabled ::us/boolean)
|
||||
|
||||
(defmethod ig/pre-init-spec ::collector [_]
|
||||
(s/keys :req-un [::db/pool ::wrk/executor ::enabled]))
|
||||
(s/keys :req-un [::db/pool ::wrk/executor]))
|
||||
|
||||
(def event-xform
|
||||
(comp
|
||||
@@ -72,29 +148,29 @@
|
||||
(map clean-props)))
|
||||
|
||||
(defmethod ig/init-key ::collector
|
||||
[_ {:keys [enabled] :as cfg}]
|
||||
(when enabled
|
||||
(l/info :msg "intializing audit collector")
|
||||
(let [input (a/chan 1 event-xform)
|
||||
[_ cfg]
|
||||
(when (contains? cf/flags :audit-log)
|
||||
(l/info :msg "intializing audit log collector")
|
||||
(let [input (a/chan 512 event-xform)
|
||||
buffer (aa/batch input {:max-batch-size 100
|
||||
:max-batch-age (* 5 1000)
|
||||
:max-batch-age (* 10 1000) ; 10s
|
||||
:init []})]
|
||||
(a/go-loop []
|
||||
(when-let [[type events] (a/<! buffer)]
|
||||
(l/debug :action "persist-events (batch)"
|
||||
:reason (name type)
|
||||
:count (count events))
|
||||
(when-let [[_type events] (a/<! buffer)]
|
||||
(let [res (a/<! (persist-events cfg events))]
|
||||
(when (ex/exception? res)
|
||||
(l/error :hint "error on persiting events"
|
||||
:cause res)))
|
||||
(recur)))
|
||||
|
||||
(fn [& [cmd & params]]
|
||||
(case cmd
|
||||
:stop (a/close! input)
|
||||
:submit (when-not (a/offer! input (first params))
|
||||
(l/warn :msg "activity channel is full")))))))
|
||||
(fn [& {:keys [cmd] :as params}]
|
||||
(let [params (-> params
|
||||
(dissoc :cmd)
|
||||
(assoc :tracked-at (dt/now)))]
|
||||
(case cmd
|
||||
:stop (a/close! input)
|
||||
:submit (when-not (a/offer! input params)
|
||||
(l/warn :msg "activity channel is full"))))))))
|
||||
|
||||
|
||||
(defn- persist-events
|
||||
@@ -104,13 +180,16 @@
|
||||
(:name event)
|
||||
(:type event)
|
||||
(:profile-id event)
|
||||
(db/tjson (:props event))])]
|
||||
|
||||
(:tracked-at event)
|
||||
(some-> (:ip-addr event) db/inet)
|
||||
(db/tjson (:props event))
|
||||
"backend"])]
|
||||
(aa/with-thread executor
|
||||
(db/with-atomic [conn pool]
|
||||
(db/insert-multi! conn :audit-log
|
||||
[:id :name :type :profile-id :props]
|
||||
(sequence (map event->row) events))))))
|
||||
(when (seq events)
|
||||
(db/with-atomic [conn pool]
|
||||
(db/insert-multi! conn :audit-log
|
||||
[:id :name :type :profile-id :tracked-at :ip-addr :props :source]
|
||||
(sequence (map event->row) events)))))))
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;; Archive Task
|
||||
@@ -125,42 +204,59 @@
|
||||
(s/def ::tokens fn?)
|
||||
|
||||
(defmethod ig/pre-init-spec ::archive-task [_]
|
||||
(s/keys :req-un [::db/pool ::tokens ::enabled]
|
||||
(s/keys :req-un [::db/pool ::tokens]
|
||||
:opt-un [::uri]))
|
||||
|
||||
(defmethod ig/init-key ::archive-task
|
||||
[_ {:keys [uri enabled] :as cfg}]
|
||||
(fn [_]
|
||||
(when (and enabled (not uri))
|
||||
(ex/raise :type :internal
|
||||
:code :task-not-configured
|
||||
:hint "archive task not configured, missing uri"))
|
||||
(loop []
|
||||
(let [res (archive-events cfg)]
|
||||
(when (= res :continue)
|
||||
(aa/thread-sleep 200)
|
||||
(recur))))))
|
||||
[_ {:keys [uri] :as cfg}]
|
||||
(fn [props]
|
||||
;; NOTE: this let allows overwrite default configured values from
|
||||
;; the repl, when manually invoking the task.
|
||||
(let [enabled (or (contains? cf/flags :audit-log-archive)
|
||||
(:enabled props false))
|
||||
uri (or uri (:uri props))
|
||||
cfg (assoc cfg :uri uri)]
|
||||
(when (and enabled (not uri))
|
||||
(ex/raise :type :internal
|
||||
:code :task-not-configured
|
||||
:hint "archive task not configured, missing uri"))
|
||||
(when enabled
|
||||
(loop []
|
||||
(let [res (archive-events cfg)]
|
||||
(when (= res :continue)
|
||||
(aa/thread-sleep 200)
|
||||
(recur))))))))
|
||||
|
||||
(def sql:retrieve-batch-of-audit-log
|
||||
"select * from audit_log
|
||||
where archived_at is null
|
||||
order by created_at asc
|
||||
limit 100
|
||||
limit 1000
|
||||
for update skip locked;")
|
||||
|
||||
(defn archive-events
|
||||
[{:keys [pool uri tokens] :as cfg}]
|
||||
(letfn [(decode-row [{:keys [props] :as row}]
|
||||
(letfn [(decode-row [{:keys [props ip-addr context] :as row}]
|
||||
(cond-> row
|
||||
(db/pgobject? props)
|
||||
(assoc :props (db/decode-transit-pgobject props))))
|
||||
(assoc :props (db/decode-transit-pgobject props))
|
||||
|
||||
(row->event [{:keys [name type created-at profile-id props]}]
|
||||
{:type type
|
||||
:name name
|
||||
:timestamp created-at
|
||||
:profile-id profile-id
|
||||
:props props})
|
||||
(db/pgobject? context)
|
||||
(assoc :context (db/decode-transit-pgobject context))
|
||||
|
||||
(db/pgobject? ip-addr "inet")
|
||||
(assoc :ip-addr (db/decode-inet ip-addr))))
|
||||
|
||||
(row->event [row]
|
||||
(select-keys row [:type
|
||||
:name
|
||||
:source
|
||||
:created-at
|
||||
:tracked-at
|
||||
:profile-id
|
||||
:ip-addr
|
||||
:props
|
||||
:context]))
|
||||
|
||||
(send [events]
|
||||
(let [token (tokens :generate {:iss "authentication"
|
||||
@@ -171,16 +267,17 @@
|
||||
"origin" (cf/get :public-uri)
|
||||
"cookie" (u/map->query-string {:auth-token token})}
|
||||
params {:uri uri
|
||||
:timeout 5000
|
||||
:timeout 6000
|
||||
:method :post
|
||||
:headers headers
|
||||
:body body}
|
||||
resp (http/send! params)]
|
||||
(when (not= (:status resp) 204)
|
||||
(ex/raise :type :internal
|
||||
:code :unable-to-send-events
|
||||
:hint "unable to send events"
|
||||
:context resp))))
|
||||
(if (= (:status resp) 204)
|
||||
true
|
||||
(do
|
||||
(l/warn :hint "unable to archive events"
|
||||
:resp-status (:status resp))
|
||||
false))))
|
||||
|
||||
(mark-as-archived [conn rows]
|
||||
(db/exec-one! conn ["update audit_log set archived_at=now() where id = ANY(?)"
|
||||
@@ -190,15 +287,12 @@
|
||||
|
||||
(db/with-atomic [conn pool]
|
||||
(let [rows (db/exec! conn [sql:retrieve-batch-of-audit-log])
|
||||
|
||||
xform (comp (map decode-row)
|
||||
(map row->event))
|
||||
events (into [] xform rows)]
|
||||
(l/debug :action "archive-events" :uri uri :events (count events))
|
||||
(if (empty? events)
|
||||
:empty
|
||||
(do
|
||||
(send events)
|
||||
(when-not (empty? events)
|
||||
(l/debug :action "archive-events" :uri uri :events (count events))
|
||||
(when (send events)
|
||||
(mark-as-archived conn rows)
|
||||
:continue))))))
|
||||
|
||||
@@ -206,18 +300,6 @@
|
||||
;; GC Task
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
|
||||
(declare clean-archived)
|
||||
|
||||
(s/def ::max-age ::cf/audit-archive-gc-max-age)
|
||||
|
||||
(defmethod ig/pre-init-spec ::archive-gc-task [_]
|
||||
(s/keys :req-un [::db/pool ::enabled ::max-age]))
|
||||
|
||||
(defmethod ig/init-key ::archive-gc-task
|
||||
[_ cfg]
|
||||
(fn [_]
|
||||
(clean-archived cfg)))
|
||||
|
||||
(def sql:clean-archived
|
||||
"delete from audit_log
|
||||
where archived_at is not null
|
||||
@@ -230,3 +312,13 @@
|
||||
result (:next.jdbc/update-count result)]
|
||||
(l/debug :action "clean archived audit log" :removed result)
|
||||
result))
|
||||
|
||||
(s/def ::max-age ::cf/audit-log-gc-max-age)
|
||||
|
||||
(defmethod ig/pre-init-spec ::gc-task [_]
|
||||
(s/keys :req-un [::db/pool ::max-age]))
|
||||
|
||||
(defmethod ig/init-key ::gc-task
|
||||
[_ cfg]
|
||||
(fn [_]
|
||||
(clean-archived cfg)))
|
||||
|
||||
126
backend/src/app/loggers/database.clj
Normal file
126
backend/src/app/loggers/database.clj
Normal file
@@ -0,0 +1,126 @@
|
||||
;; This Source Code Form is subject to the terms of the Mozilla Public
|
||||
;; License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
;;
|
||||
;; Copyright (c) UXBOX Labs SL
|
||||
|
||||
(ns app.loggers.database
|
||||
"A specific logger impl that persists errors on the database."
|
||||
(:require
|
||||
[app.common.exceptions :as ex]
|
||||
[app.common.logging :as l]
|
||||
[app.common.spec :as us]
|
||||
[app.common.uuid :as uuid]
|
||||
[app.config :as cf]
|
||||
[app.db :as db]
|
||||
[app.util.async :as aa]
|
||||
[app.util.template :as tmpl]
|
||||
[app.worker :as wrk]
|
||||
[clojure.core.async :as a]
|
||||
[clojure.java.io :as io]
|
||||
[clojure.spec.alpha :as s]
|
||||
[cuerdas.core :as str]
|
||||
[integrant.core :as ig]))
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;; Error Listener
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
|
||||
(declare handle-event)
|
||||
|
||||
(defonce enabled (atom true))
|
||||
|
||||
(defn- persist-on-database!
|
||||
[{:keys [pool] :as cfg} {:keys [id] :as event}]
|
||||
(db/with-atomic [conn pool]
|
||||
(db/insert! conn :server-error-report
|
||||
{:id id :content (db/tjson event)})))
|
||||
|
||||
(defn- parse-context
|
||||
[event]
|
||||
(reduce-kv
|
||||
(fn [acc k v]
|
||||
(cond
|
||||
(= k :id) (assoc acc k (uuid/uuid v))
|
||||
(= k :profile-id) (assoc acc k (uuid/uuid v))
|
||||
(str/blank? v) acc
|
||||
:else (assoc acc k v)))
|
||||
{}
|
||||
(:context event)))
|
||||
|
||||
(defn parse-event
|
||||
[event]
|
||||
(-> (parse-context event)
|
||||
(merge (dissoc event :context))
|
||||
(assoc :tenant (cf/get :tenant))
|
||||
(assoc :host (cf/get :host))
|
||||
(assoc :public-uri (cf/get :public-uri))
|
||||
(assoc :version (:full cf/version))))
|
||||
|
||||
(defn handle-event
|
||||
[{:keys [executor] :as cfg} event]
|
||||
(aa/with-thread executor
|
||||
(try
|
||||
(let [event (parse-event event)]
|
||||
(persist-on-database! cfg event))
|
||||
(catch Exception e
|
||||
(l/warn :hint "unexpected exception on database error logger"
|
||||
:cause e)))))
|
||||
|
||||
(defmethod ig/pre-init-spec ::reporter [_]
|
||||
(s/keys :req-un [::wrk/executor ::db/pool ::receiver]))
|
||||
|
||||
(defmethod ig/init-key ::reporter
|
||||
[_ {:keys [receiver] :as cfg}]
|
||||
(l/info :msg "initializing database error persistence")
|
||||
(let [output (a/chan (a/sliding-buffer 128)
|
||||
(filter #(= (:level %) "error")))]
|
||||
(receiver :sub output)
|
||||
(a/go-loop []
|
||||
(let [msg (a/<! output)]
|
||||
(if (nil? msg)
|
||||
(l/info :msg "stoping error reporting loop")
|
||||
(do
|
||||
(a/<! (handle-event cfg msg))
|
||||
(recur)))))
|
||||
output))
|
||||
|
||||
(defmethod ig/halt-key! ::reporter
|
||||
[_ output]
|
||||
(a/close! output))
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;; Http Handler
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
|
||||
(defmethod ig/pre-init-spec ::handler [_]
|
||||
(s/keys :req-un [::db/pool]))
|
||||
|
||||
(defmethod ig/init-key ::handler
|
||||
[_ {:keys [pool] :as cfg}]
|
||||
(letfn [(parse-id [request]
|
||||
(let [id (get-in request [:path-params :id])
|
||||
id (us/uuid-conformer id)]
|
||||
(when (uuid? id)
|
||||
id)))
|
||||
(retrieve-report [id]
|
||||
(ex/ignoring
|
||||
(when-let [{:keys [content] :as row} (db/get-by-id pool :server-error-report id)]
|
||||
(assoc row :content (db/decode-transit-pgobject content)))))
|
||||
|
||||
(render-template [{:keys [content] :as report}]
|
||||
(some-> (io/resource "error-report.tmpl")
|
||||
(tmpl/render content)))]
|
||||
|
||||
|
||||
(fn [request]
|
||||
(let [result (some-> (parse-id request)
|
||||
(retrieve-report)
|
||||
(render-template))]
|
||||
(if result
|
||||
{:status 200
|
||||
:headers {"content-type" "text/html; charset=utf-8"
|
||||
"x-robots-tag" "noindex"}
|
||||
:body result}
|
||||
{:status 404
|
||||
:body "not found"})))))
|
||||
@@ -7,12 +7,12 @@
|
||||
(ns app.loggers.loki
|
||||
"A Loki integration."
|
||||
(:require
|
||||
[app.common.logging :as l]
|
||||
[app.common.spec :as us]
|
||||
[app.config :as cfg]
|
||||
[app.util.async :as aa]
|
||||
[app.util.http :as http]
|
||||
[app.util.json :as json]
|
||||
[app.util.logging :as l]
|
||||
[app.worker :as wrk]
|
||||
[clojure.core.async :as a]
|
||||
[clojure.spec.alpha :as s]
|
||||
@@ -31,7 +31,7 @@
|
||||
[_ {:keys [receiver uri] :as cfg}]
|
||||
(when uri
|
||||
(l/info :msg "intializing loki reporter" :uri uri)
|
||||
(let [input (a/chan (a/sliding-buffer 1024))]
|
||||
(let [input (a/chan (a/dropping-buffer 512))]
|
||||
(receiver :sub input)
|
||||
(a/go-loop []
|
||||
(let [msg (a/<! input)]
|
||||
@@ -69,17 +69,23 @@
|
||||
:method :post
|
||||
:headers {"content-type" "application/json"}
|
||||
:body (json/encode payload)})]
|
||||
(if (= (:status response) 204)
|
||||
(cond
|
||||
(= (:status response) 204)
|
||||
true
|
||||
|
||||
(= (:status response) 400)
|
||||
(do
|
||||
(l/error :hint "error on sending log to loki"
|
||||
:try i
|
||||
(l/error :hint "error on sending log to loki (no retry)"
|
||||
:rsp (pr-str response))
|
||||
true)
|
||||
|
||||
:else
|
||||
(do
|
||||
(l/error :hint "error on sending log to loki" :try i
|
||||
:rsp (pr-str response))
|
||||
false)))
|
||||
(catch Exception e
|
||||
(l/error :hint "error on sending message to loki"
|
||||
:cause e
|
||||
:try i)
|
||||
(l/error :hint "error on sending message to loki" :cause e :try i)
|
||||
false)))
|
||||
|
||||
(defn- handle-event
|
||||
|
||||
@@ -7,69 +7,31 @@
|
||||
(ns app.loggers.mattermost
|
||||
"A mattermost integration for error reporting."
|
||||
(:require
|
||||
[app.common.exceptions :as ex]
|
||||
[app.common.spec :as us]
|
||||
[app.common.uuid :as uuid]
|
||||
[app.config :as cfg]
|
||||
[app.common.logging :as l]
|
||||
[app.config :as cf]
|
||||
[app.db :as db]
|
||||
[app.loggers.database :as ldb]
|
||||
[app.util.async :as aa]
|
||||
[app.util.http :as http]
|
||||
[app.util.json :as json]
|
||||
[app.util.logging :as l]
|
||||
[app.util.template :as tmpl]
|
||||
[app.worker :as wrk]
|
||||
[clojure.core.async :as a]
|
||||
[clojure.java.io :as io]
|
||||
[clojure.spec.alpha :as s]
|
||||
[cuerdas.core :as str]
|
||||
[integrant.core :as ig]))
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;; Error Listener
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
|
||||
(declare handle-event)
|
||||
|
||||
(defonce enabled-mattermost (atom true))
|
||||
|
||||
(s/def ::uri ::us/string)
|
||||
|
||||
(defmethod ig/pre-init-spec ::reporter [_]
|
||||
(s/keys :req-un [::wrk/executor ::db/pool ::receiver]
|
||||
:opt-un [::uri]))
|
||||
|
||||
(defmethod ig/init-key ::reporter
|
||||
[_ {:keys [receiver uri] :as cfg}]
|
||||
(l/info :msg "intializing mattermost error reporter" :uri uri)
|
||||
(let [output (a/chan (a/sliding-buffer 128)
|
||||
(filter #(= (:level %) "error")))]
|
||||
(receiver :sub output)
|
||||
(a/go-loop []
|
||||
(let [msg (a/<! output)]
|
||||
(if (nil? msg)
|
||||
(l/info :msg "stoping error reporting loop")
|
||||
(do
|
||||
(a/<! (handle-event cfg msg))
|
||||
(recur)))))
|
||||
output))
|
||||
|
||||
(defmethod ig/halt-key! ::reporter
|
||||
[_ output]
|
||||
(a/close! output))
|
||||
(defonce enabled (atom true))
|
||||
|
||||
(defn- send-mattermost-notification!
|
||||
[cfg {:keys [host version id] :as cdata}]
|
||||
[cfg {:keys [host id public-uri] :as event}]
|
||||
(try
|
||||
(let [uri (:uri cfg)
|
||||
text (str "Unhandled exception:\n"
|
||||
"- detail: " (cfg/get :public-uri) "/dbg/error-by-id/" id "\n"
|
||||
"- profile-id: `" (:profile-id cdata) "`\n"
|
||||
"- host: `" host "`\n"
|
||||
"- version: `" version "`\n")
|
||||
rsp (http/send! {:uri uri
|
||||
:method :post
|
||||
:headers {"content-type" "application/json"}
|
||||
:body (json/encode-str {:text text})})]
|
||||
text (str "Exception on (host: " host ", url: " public-uri "/dbg/error-by-id/" id ")\n"
|
||||
(when-let [pid (:profile-id event)]
|
||||
(str "- profile-id: #uuid-" pid "\n")))
|
||||
rsp (http/send! {:uri uri
|
||||
:method :post
|
||||
:headers {"content-type" "application/json"}
|
||||
:body (json/encode-str {:text text})})]
|
||||
(when (not= (:status rsp) 200)
|
||||
(l/error :hint "error on sending data to mattermost"
|
||||
:response (pr-str rsp))))
|
||||
@@ -78,76 +40,40 @@
|
||||
(l/error :hint "unexpected exception on error reporter"
|
||||
:cause e))))
|
||||
|
||||
(defn- persist-on-database!
|
||||
[{:keys [pool] :as cfg} {:keys [id] :as cdata}]
|
||||
(db/with-atomic [conn pool]
|
||||
(db/insert! conn :server-error-report
|
||||
{:id id :content (db/tjson cdata)})))
|
||||
|
||||
(defn- parse-context
|
||||
[event]
|
||||
(reduce-kv
|
||||
(fn [acc k v]
|
||||
(cond
|
||||
(= k :id) (assoc acc k (uuid/uuid v))
|
||||
(= k :profile-id) (assoc acc k (uuid/uuid v))
|
||||
(str/blank? v) acc
|
||||
:else (assoc acc k v)))
|
||||
{:id (uuid/next)}
|
||||
(:context event)))
|
||||
|
||||
(defn- parse-event
|
||||
[event]
|
||||
(-> (parse-context event)
|
||||
(merge (dissoc event :context))
|
||||
(assoc :tenant (cfg/get :tenant))
|
||||
(assoc :host (cfg/get :host))
|
||||
(assoc :public-uri (cfg/get :public-uri))
|
||||
(assoc :version (:full cfg/version))))
|
||||
|
||||
(defn handle-event
|
||||
[{:keys [executor] :as cfg} event]
|
||||
(aa/with-thread executor
|
||||
(try
|
||||
(let [cdata (parse-event event)]
|
||||
(when (and (:uri cfg) @enabled-mattermost)
|
||||
(send-mattermost-notification! cfg cdata))
|
||||
(persist-on-database! cfg cdata))
|
||||
(let [event (ldb/parse-event event)]
|
||||
(when @enabled
|
||||
(send-mattermost-notification! cfg event)))
|
||||
(catch Exception e
|
||||
(l/error :hint "unexpected exception on error reporter"
|
||||
:cause e)))))
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;; Http Handler
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
|
||||
(defmethod ig/pre-init-spec ::handler [_]
|
||||
(s/keys :req-un [::db/pool]))
|
||||
|
||||
(defmethod ig/init-key ::handler
|
||||
[_ {:keys [pool] :as cfg}]
|
||||
(letfn [(parse-id [request]
|
||||
(let [id (get-in request [:path-params :id])
|
||||
id (us/uuid-conformer id)]
|
||||
(when (uuid? id)
|
||||
id)))
|
||||
(retrieve-report [id]
|
||||
(ex/ignoring
|
||||
(when-let [{:keys [content] :as row} (db/get-by-id pool :server-error-report id)]
|
||||
(assoc row :content (db/decode-transit-pgobject content)))))
|
||||
|
||||
(render-template [{:keys [content] :as report}]
|
||||
(some-> (io/resource "error-report.tmpl")
|
||||
(tmpl/render content)))]
|
||||
(l/warn :hint "unexpected exception on error reporter" :cause e)))))
|
||||
|
||||
|
||||
(fn [request]
|
||||
(let [result (some-> (parse-id request)
|
||||
(retrieve-report)
|
||||
(render-template))]
|
||||
(if result
|
||||
{:status 200
|
||||
:headers {"content-type" "text/html; charset=utf-8"}
|
||||
:body result}
|
||||
{:status 404
|
||||
:body "not found"})))))
|
||||
(s/def ::uri ::cf/error-report-webhook)
|
||||
|
||||
(defmethod ig/pre-init-spec ::reporter [_]
|
||||
(s/keys :req-un [::wrk/executor ::db/pool ::receiver]
|
||||
:opt-un [::uri]))
|
||||
|
||||
(defmethod ig/init-key ::reporter
|
||||
[_ {:keys [receiver uri] :as cfg}]
|
||||
(when uri
|
||||
(l/info :msg "initializing mattermost error reporter" :uri uri)
|
||||
(let [output (a/chan (a/sliding-buffer 128)
|
||||
(filter #(= (:level %) "error")))]
|
||||
(receiver :sub output)
|
||||
(a/go-loop []
|
||||
(let [msg (a/<! output)]
|
||||
(if (nil? msg)
|
||||
(l/info :msg "stoping error reporting loop")
|
||||
(do
|
||||
(a/<! (handle-event cfg msg))
|
||||
(recur)))))
|
||||
output)))
|
||||
|
||||
(defmethod ig/halt-key! ::reporter
|
||||
[_ output]
|
||||
(when output
|
||||
(a/close! output)))
|
||||
|
||||
172
backend/src/app/loggers/sentry.clj
Normal file
172
backend/src/app/loggers/sentry.clj
Normal file
@@ -0,0 +1,172 @@
|
||||
;; This Source Code Form is subject to the terms of the Mozilla Public
|
||||
;; License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
;;
|
||||
;; Copyright (c) UXBOX Labs SL
|
||||
|
||||
(ns app.loggers.sentry
|
||||
"A mattermost integration for error reporting."
|
||||
(:require
|
||||
[app.common.logging :as l]
|
||||
[app.common.uuid :as uuid]
|
||||
[app.config :as cf]
|
||||
[app.db :as db]
|
||||
[app.util.async :as aa]
|
||||
[app.worker :as wrk]
|
||||
[clojure.core.async :as a]
|
||||
[clojure.spec.alpha :as s]
|
||||
[cuerdas.core :as str]
|
||||
[integrant.core :as ig])
|
||||
(:import
|
||||
io.sentry.Scope
|
||||
io.sentry.IHub
|
||||
io.sentry.Hub
|
||||
io.sentry.NoOpHub
|
||||
io.sentry.protocol.User
|
||||
io.sentry.SentryOptions
|
||||
io.sentry.SentryLevel
|
||||
io.sentry.ScopeCallback))
|
||||
|
||||
(defonce enabled (atom true))
|
||||
|
||||
(defn- parse-context
|
||||
[event]
|
||||
(reduce-kv
|
||||
(fn [acc k v]
|
||||
(cond
|
||||
(= k :id) (assoc acc k (uuid/uuid v))
|
||||
(= k :profile-id) (assoc acc k (uuid/uuid v))
|
||||
(str/blank? v) acc
|
||||
:else (assoc acc k v)))
|
||||
{}
|
||||
(:context event)))
|
||||
|
||||
(defn- parse-event
|
||||
[event]
|
||||
(assoc event :context (parse-context event)))
|
||||
|
||||
(defn- build-sentry-options
|
||||
[cfg]
|
||||
(let [version (:base cf/version)]
|
||||
(doto (SentryOptions.)
|
||||
(.setDebug (:debug cfg false))
|
||||
(.setTracesSampleRate (:traces-sample-rate cfg 1.0))
|
||||
(.setDsn (:dsn cfg))
|
||||
(.setServerName (cf/get :host))
|
||||
(.setEnvironment (cf/get :tenant))
|
||||
(.setAttachServerName true)
|
||||
(.setAttachStacktrace (:attach-stack-trace cfg false))
|
||||
(.setRelease (str "backend@" (if (= version "0.0.0") "develop" version))))))
|
||||
|
||||
(defn handle-event
|
||||
[^IHub shub event]
|
||||
(letfn [(set-user! [^Scope scope {:keys [context] :as event}]
|
||||
(let [user (User.)]
|
||||
(.setIpAddress ^User user ^String (:ip-addr context))
|
||||
(when-let [pid (:profile-id context)]
|
||||
(.setId ^User user ^String (str pid)))
|
||||
(.setUser scope ^User user)))
|
||||
|
||||
(set-level! [^Scope scope]
|
||||
(.setLevel scope SentryLevel/ERROR))
|
||||
|
||||
(set-context! [^Scope scope {:keys [context] :as event}]
|
||||
(let [uri (str (cf/get :public-uri) "/dbg/error-by-id/" (:id context))]
|
||||
(.setContexts scope "detailed_error_uri" ^String uri))
|
||||
(when-let [vers (:frontend-version event)]
|
||||
(.setContexts scope "frontend_version" ^String vers))
|
||||
(when-let [puri (:public-uri event)]
|
||||
(.setContexts scope "public_uri" ^String (str puri)))
|
||||
(when-let [uagent (:user-agent context)]
|
||||
(.setContexts scope "user_agent" ^String uagent))
|
||||
(when-let [tenant (:tenant event)]
|
||||
(.setTag scope "tenant" ^String tenant))
|
||||
(when-let [type (:error-type context)]
|
||||
(.setTag scope "error_type" ^String (str type)))
|
||||
(when-let [code (:error-code context)]
|
||||
(.setTag scope "error_code" ^String (str code)))
|
||||
)
|
||||
|
||||
(capture [^Scope scope {:keys [context error] :as event}]
|
||||
(let [msg (str (:message error) "\n\n"
|
||||
|
||||
"======================================================\n"
|
||||
"=================== Params ===========================\n"
|
||||
"======================================================\n"
|
||||
|
||||
(:params context) "\n"
|
||||
|
||||
(when (:explain context)
|
||||
(str "======================================================\n"
|
||||
"=================== Explain ==========================\n"
|
||||
"======================================================\n"
|
||||
(:explain context) "\n"))
|
||||
|
||||
(when (:data context)
|
||||
(str "======================================================\n"
|
||||
"=================== Error Data =======================\n"
|
||||
"======================================================\n"
|
||||
(:data context) "\n"))
|
||||
|
||||
(str "======================================================\n"
|
||||
"=================== Stack Trace ======================\n"
|
||||
"======================================================\n"
|
||||
(:trace error))
|
||||
|
||||
"\n")]
|
||||
(set-user! scope event)
|
||||
(set-level! scope)
|
||||
(set-context! scope event)
|
||||
(.captureMessage ^IHub shub msg)
|
||||
))
|
||||
]
|
||||
;; (clojure.pprint/pprint event)
|
||||
|
||||
(when @enabled
|
||||
(.withScope ^IHub shub (reify ScopeCallback
|
||||
(run [_ scope]
|
||||
(->> event
|
||||
(parse-event)
|
||||
(capture scope))))))
|
||||
|
||||
))
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;; Error Listener
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
|
||||
(s/def ::receiver any?)
|
||||
(s/def ::dsn ::cf/sentry-dsn)
|
||||
(s/def ::trace-sample-rate ::cf/sentry-trace-sample-rate)
|
||||
(s/def ::attach-stack-trace ::cf/sentry-attach-stack-trace)
|
||||
(s/def ::debug ::cf/sentry-debug)
|
||||
|
||||
(defmethod ig/pre-init-spec ::reporter [_]
|
||||
(s/keys :req-un [::wrk/executor ::db/pool ::receiver]
|
||||
:opt-un [::dsn ::trace-sample-rate ::attach-stack-trace]))
|
||||
|
||||
(defmethod ig/init-key ::reporter
|
||||
[_ {:keys [receiver dsn executor] :as cfg}]
|
||||
(l/info :msg "initializing sentry reporter" :dsn dsn)
|
||||
(let [opts (build-sentry-options cfg)
|
||||
shub (if dsn
|
||||
(Hub. ^SentryOptions opts)
|
||||
(NoOpHub/getInstance))
|
||||
output (a/chan (a/sliding-buffer 128)
|
||||
(filter #(= (:level %) "error")))]
|
||||
(receiver :sub output)
|
||||
(a/go-loop []
|
||||
(let [event (a/<! output)]
|
||||
(if (nil? event)
|
||||
(do
|
||||
(l/info :msg "stoping error reporting loop")
|
||||
(.close ^IHub shub))
|
||||
(do
|
||||
(a/<! (aa/with-thread executor (handle-event shub event)))
|
||||
(recur)))))
|
||||
output))
|
||||
|
||||
(defmethod ig/halt-key! ::reporter
|
||||
[_ output]
|
||||
(when output
|
||||
(a/close! output)))
|
||||
@@ -7,10 +7,9 @@
|
||||
(ns app.loggers.zmq
|
||||
"A generic ZMQ listener."
|
||||
(:require
|
||||
[app.common.data :as d]
|
||||
[app.common.logging :as l]
|
||||
[app.common.spec :as us]
|
||||
[app.util.json :as json]
|
||||
[app.util.logging :as l]
|
||||
[app.util.time :as dt]
|
||||
[clojure.core.async :as a]
|
||||
[clojure.spec.alpha :as s]
|
||||
@@ -74,7 +73,7 @@
|
||||
|
||||
(defn- prepare
|
||||
[event]
|
||||
(d/merge
|
||||
(merge
|
||||
{:logger (:loggerName event)
|
||||
:level (str/lower (:level event))
|
||||
:thread (:thread event)
|
||||
|
||||
@@ -6,8 +6,8 @@
|
||||
|
||||
(ns app.main
|
||||
(:require
|
||||
[app.common.logging :as l]
|
||||
[app.config :as cf]
|
||||
[app.util.logging :as l]
|
||||
[app.util.time :as dt]
|
||||
[integrant.core :as ig]))
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
:migrations (ig/ref :app.migrations/all)
|
||||
:name :main
|
||||
:min-pool-size 0
|
||||
:max-pool-size 20}
|
||||
:max-pool-size 30}
|
||||
|
||||
:app.metrics/metrics
|
||||
{:definitions
|
||||
@@ -28,9 +28,20 @@
|
||||
{:name "actions_profile_register_count"
|
||||
:help "A global counter of user registrations."
|
||||
:type :counter}
|
||||
|
||||
:profile-activation
|
||||
{:name "actions_profile_activation_count"
|
||||
:help "A global counter of profile activations"
|
||||
:type :counter}
|
||||
|
||||
:update-file-changes
|
||||
{:name "rpc_update_file_changes_total"
|
||||
:help "A total number of changes submitted to update-file."
|
||||
:type :counter}
|
||||
|
||||
:update-file-bytes-processed
|
||||
{:name "rpc_update_file_bytes_processed_total"
|
||||
:help "A total number of bytes processed by update-file."
|
||||
:type :counter}}}
|
||||
|
||||
:app.migrations/all
|
||||
@@ -44,7 +55,7 @@
|
||||
:redis-uri (cf/get :redis-uri)}
|
||||
|
||||
:app.tokens/tokens
|
||||
{:props (ig/ref :app.setup/props)}
|
||||
{:keys (ig/ref :app.setup/keys)}
|
||||
|
||||
:app.storage/gc-deleted-task
|
||||
{:pool (ig/ref :app.db/pool)
|
||||
@@ -90,12 +101,13 @@
|
||||
:tokens (ig/ref :app.tokens/tokens)
|
||||
:public-uri (cf/get :public-uri)
|
||||
:metrics (ig/ref :app.metrics/metrics)
|
||||
:oauth (ig/ref :app.http.oauth/handlers)
|
||||
:oauth (ig/ref :app.http.oauth/handler)
|
||||
:assets (ig/ref :app.http.assets/handlers)
|
||||
:storage (ig/ref :app.storage/storage)
|
||||
:sns-webhook (ig/ref :app.http.awsns/handler)
|
||||
:feedback (ig/ref :app.http.feedback/handler)
|
||||
:error-report-handler (ig/ref :app.loggers.mattermost/handler)}
|
||||
:audit-http-handler (ig/ref :app.loggers.audit/http-handler)
|
||||
:error-report-handler (ig/ref :app.loggers.database/handler)}
|
||||
|
||||
:app.http.assets/handlers
|
||||
{:metrics (ig/ref :app.metrics/metrics)
|
||||
@@ -107,30 +119,14 @@
|
||||
:app.http.feedback/handler
|
||||
{:pool (ig/ref :app.db/pool)}
|
||||
|
||||
:app.http.oauth/handlers
|
||||
:app.http.oauth/handler
|
||||
{:rpc (ig/ref :app.rpc/rpc)
|
||||
:session (ig/ref :app.http.session/session)
|
||||
:pool (ig/ref :app.db/pool)
|
||||
:tokens (ig/ref :app.tokens/tokens)
|
||||
:audit (ig/ref :app.loggers.audit/collector)
|
||||
:public-uri (cf/get :public-uri)}
|
||||
|
||||
;; RLimit definition for password hashing
|
||||
:app.rlimits/password
|
||||
(cf/get :rlimits-password)
|
||||
|
||||
;; RLimit definition for image processing
|
||||
:app.rlimits/image
|
||||
(cf/get :rlimits-image)
|
||||
|
||||
;; RLimit definition for font processing
|
||||
:app.rlimits/font
|
||||
(cf/get :rlimits-font 2)
|
||||
|
||||
;; A collection of rlimits as hash-map.
|
||||
:app.rlimits/all
|
||||
{:password (ig/ref :app.rlimits/password)
|
||||
:image (ig/ref :app.rlimits/image)
|
||||
:font (ig/ref :app.rlimits/font)}
|
||||
|
||||
:app.rpc/rpc
|
||||
{:pool (ig/ref :app.db/pool)
|
||||
:session (ig/ref :app.http.session/session)
|
||||
@@ -138,7 +134,6 @@
|
||||
:metrics (ig/ref :app.metrics/metrics)
|
||||
:storage (ig/ref :app.storage/storage)
|
||||
:msgbus (ig/ref :app.msgbus/msgbus)
|
||||
:rlimits (ig/ref :app.rlimits/all)
|
||||
:public-uri (cf/get :public-uri)
|
||||
:audit (ig/ref :app.loggers.audit/collector)}
|
||||
|
||||
@@ -166,36 +161,44 @@
|
||||
:tasks (ig/ref :app.worker/registry)
|
||||
:pool (ig/ref :app.db/pool)
|
||||
:schedule
|
||||
[{:cron #app/cron "0 0 0 */1 * ? *" ;; daily
|
||||
[{:cron #app/cron "0 0 0 * * ?" ;; daily
|
||||
:task :file-media-gc}
|
||||
|
||||
{:cron #app/cron "0 0 */1 * * ?" ;; hourly
|
||||
{:cron #app/cron "0 0 * * * ?" ;; hourly
|
||||
:task :file-xlog-gc}
|
||||
|
||||
{:cron #app/cron "0 0 1 */1 * ?" ;; daily (1 hour shift)
|
||||
{:cron #app/cron "0 0 0 * * ?" ;; daily
|
||||
:task :storage-deleted-gc}
|
||||
|
||||
{:cron #app/cron "0 0 2 */1 * ?" ;; daily (2 hour shift)
|
||||
{:cron #app/cron "0 0 0 * * ?" ;; daily
|
||||
:task :storage-touched-gc}
|
||||
|
||||
{:cron #app/cron "0 0 3 */1 * ?" ;; daily (3 hour shift)
|
||||
{:cron #app/cron "0 0 0 * * ?" ;; daily
|
||||
:task :session-gc}
|
||||
|
||||
{:cron #app/cron "0 0 */1 * * ?" ;; hourly
|
||||
{:cron #app/cron "0 0 * * * ?" ;; hourly
|
||||
:task :storage-recheck}
|
||||
|
||||
{:cron #app/cron "0 0 0 */1 * ?" ;; daily
|
||||
{:cron #app/cron "0 0 0 * * ?" ;; daily
|
||||
:task :objects-gc}
|
||||
|
||||
{:cron #app/cron "0 0 0 * * ?" ;; daily
|
||||
:task :tasks-gc}
|
||||
|
||||
(when (cf/get :audit-archive-enabled)
|
||||
{:cron #app/cron "0 0 * * * ?" ;; every 1h
|
||||
:task :audit-archive})
|
||||
(when (cf/get :fdata-storage-backed)
|
||||
{:cron #app/cron "0 0 * * * ?" ;; hourly
|
||||
:task :file-offload})
|
||||
|
||||
(when (cf/get :audit-archive-gc-enabled)
|
||||
{:cron #app/cron "0 0 * * * ?" ;; every 1h
|
||||
:task :audit-archive-gc})
|
||||
(when (contains? cf/flags :audit-log-archive)
|
||||
{:cron #app/cron "0 */3 * * * ?" ;; every 3m
|
||||
:task :audit-log-archive})
|
||||
|
||||
(when (cf/get :telemetry-enabled)
|
||||
(when (contains? cf/flags :audit-log-gc)
|
||||
{:cron #app/cron "0 0 0 * * ?" ;; daily
|
||||
:task :audit-log-gc})
|
||||
|
||||
(when (or (contains? cf/flags :telemetry)
|
||||
(cf/get :telemetry-enabled))
|
||||
{:cron #app/cron "0 0 */6 * * ?" ;; every 6h
|
||||
:task :telemetry})]}
|
||||
|
||||
@@ -203,8 +206,7 @@
|
||||
{:metrics (ig/ref :app.metrics/metrics)
|
||||
:tasks
|
||||
{:sendmail (ig/ref :app.emails/sendmail-handler)
|
||||
:delete-object (ig/ref :app.tasks.delete-object/handler)
|
||||
:delete-profile (ig/ref :app.tasks.delete-profile/handler)
|
||||
:objects-gc (ig/ref :app.tasks.objects-gc/handler)
|
||||
:file-media-gc (ig/ref :app.tasks.file-media-gc/handler)
|
||||
:file-xlog-gc (ig/ref :app.tasks.file-xlog-gc/handler)
|
||||
:storage-deleted-gc (ig/ref :app.storage/gc-deleted-task)
|
||||
@@ -213,15 +215,15 @@
|
||||
:tasks-gc (ig/ref :app.tasks.tasks-gc/handler)
|
||||
:telemetry (ig/ref :app.tasks.telemetry/handler)
|
||||
:session-gc (ig/ref :app.http.session/gc-task)
|
||||
:audit-archive (ig/ref :app.loggers.audit/archive-task)
|
||||
:audit-archive-gc (ig/ref :app.loggers.audit/archive-gc-task)}}
|
||||
:file-offload (ig/ref :app.tasks.file-offload/handler)
|
||||
:audit-log-archive (ig/ref :app.loggers.audit/archive-task)
|
||||
:audit-log-gc (ig/ref :app.loggers.audit/gc-task)}}
|
||||
|
||||
:app.emails/sendmail-handler
|
||||
{:host (cf/get :smtp-host)
|
||||
:port (cf/get :smtp-port)
|
||||
:ssl (cf/get :smtp-ssl)
|
||||
:tls (cf/get :smtp-tls)
|
||||
:enabled (cf/get :smtp-enabled)
|
||||
:username (cf/get :smtp-username)
|
||||
:password (cf/get :smtp-password)
|
||||
:metrics (ig/ref :app.metrics/metrics)
|
||||
@@ -230,31 +232,26 @@
|
||||
|
||||
:app.tasks.tasks-gc/handler
|
||||
{:pool (ig/ref :app.db/pool)
|
||||
:max-age cf/deletion-delay
|
||||
:metrics (ig/ref :app.metrics/metrics)}
|
||||
:max-age cf/deletion-delay}
|
||||
|
||||
:app.tasks.delete-object/handler
|
||||
{:pool (ig/ref :app.db/pool)
|
||||
:metrics (ig/ref :app.metrics/metrics)}
|
||||
|
||||
:app.tasks.delete-storage-object/handler
|
||||
:app.tasks.objects-gc/handler
|
||||
{:pool (ig/ref :app.db/pool)
|
||||
:storage (ig/ref :app.storage/storage)
|
||||
:metrics (ig/ref :app.metrics/metrics)}
|
||||
|
||||
:app.tasks.delete-profile/handler
|
||||
{:pool (ig/ref :app.db/pool)
|
||||
:metrics (ig/ref :app.metrics/metrics)}
|
||||
:max-age cf/deletion-delay}
|
||||
|
||||
:app.tasks.file-media-gc/handler
|
||||
{:pool (ig/ref :app.db/pool)
|
||||
:metrics (ig/ref :app.metrics/metrics)
|
||||
:max-age cf/deletion-delay}
|
||||
|
||||
:app.tasks.file-xlog-gc/handler
|
||||
{:pool (ig/ref :app.db/pool)
|
||||
:metrics (ig/ref :app.metrics/metrics)
|
||||
:max-age cf/deletion-delay}
|
||||
:max-age (dt/duration {:hours 72})}
|
||||
|
||||
:app.tasks.file-offload/handler
|
||||
{:pool (ig/ref :app.db/pool)
|
||||
:max-age (dt/duration {:seconds 5})
|
||||
:storage (ig/ref :app.storage/storage)
|
||||
:backend (cf/get :fdata-storage-backed :fdata-s3)}
|
||||
|
||||
:app.tasks.telemetry/handler
|
||||
{:pool (ig/ref :app.db/pool)
|
||||
@@ -270,23 +267,27 @@
|
||||
{:pool (ig/ref :app.db/pool)
|
||||
:key (cf/get :secret-key)}
|
||||
|
||||
:app.setup/keys
|
||||
{:props (ig/ref :app.setup/props)}
|
||||
|
||||
:app.loggers.zmq/receiver
|
||||
{:endpoint (cf/get :loggers-zmq-uri)}
|
||||
|
||||
:app.loggers.audit/http-handler
|
||||
{:pool (ig/ref :app.db/pool)
|
||||
:executor (ig/ref :app.worker/executor)}
|
||||
|
||||
:app.loggers.audit/collector
|
||||
{:enabled (cf/get :audit-enabled false)
|
||||
:pool (ig/ref :app.db/pool)
|
||||
{:pool (ig/ref :app.db/pool)
|
||||
:executor (ig/ref :app.worker/executor)}
|
||||
|
||||
:app.loggers.audit/archive-task
|
||||
{:uri (cf/get :audit-archive-uri)
|
||||
:enabled (cf/get :audit-archive-enabled false)
|
||||
{:uri (cf/get :audit-log-archive-uri)
|
||||
:tokens (ig/ref :app.tokens/tokens)
|
||||
:pool (ig/ref :app.db/pool)}
|
||||
|
||||
:app.loggers.audit/archive-gc-task
|
||||
{:enabled (cf/get :audit-archive-gc-enabled false)
|
||||
:max-age (cf/get :audit-archive-gc-max-age cf/deletion-delay)
|
||||
:app.loggers.audit/gc-task
|
||||
{:max-age (cf/get :audit-log-gc-max-age cf/deletion-delay)
|
||||
:pool (ig/ref :app.db/pool)}
|
||||
|
||||
:app.loggers.loki/reporter
|
||||
@@ -300,29 +301,54 @@
|
||||
:pool (ig/ref :app.db/pool)
|
||||
:executor (ig/ref :app.worker/executor)}
|
||||
|
||||
:app.loggers.mattermost/handler
|
||||
:app.loggers.database/reporter
|
||||
{:receiver (ig/ref :app.loggers.zmq/receiver)
|
||||
:pool (ig/ref :app.db/pool)
|
||||
:executor (ig/ref :app.worker/executor)}
|
||||
|
||||
:app.loggers.database/handler
|
||||
{:pool (ig/ref :app.db/pool)}
|
||||
|
||||
:app.loggers.sentry/reporter
|
||||
{:dsn (cf/get :sentry-dsn)
|
||||
:trace-sample-rate (cf/get :sentry-trace-sample-rate 1.0)
|
||||
:attach-stack-trace (cf/get :sentry-attach-stack-trace false)
|
||||
:debug (cf/get :sentry-debug false)
|
||||
:receiver (ig/ref :app.loggers.zmq/receiver)
|
||||
:pool (ig/ref :app.db/pool)
|
||||
:executor (ig/ref :app.worker/executor)}
|
||||
|
||||
:app.storage/storage
|
||||
{:pool (ig/ref :app.db/pool)
|
||||
:executor (ig/ref :app.worker/executor)
|
||||
:backend (cf/get :storage-backend :fs)
|
||||
:backends {:s3 (ig/ref [::main :app.storage.s3/backend])
|
||||
:db (ig/ref [::main :app.storage.db/backend])
|
||||
:fs (ig/ref [::main :app.storage.fs/backend])
|
||||
:tmp (ig/ref [::tmp :app.storage.fs/backend])}}
|
||||
|
||||
[::main :app.storage.s3/backend]
|
||||
{:region (cf/get :storage-s3-region)
|
||||
:bucket (cf/get :storage-s3-bucket)}
|
||||
:backends {
|
||||
:assets-s3 (ig/ref [::assets :app.storage.s3/backend])
|
||||
:assets-db (ig/ref [::assets :app.storage.db/backend])
|
||||
:assets-fs (ig/ref [::assets :app.storage.fs/backend])
|
||||
:tmp (ig/ref [::tmp :app.storage.fs/backend])
|
||||
:fdata-s3 (ig/ref [::fdata :app.storage.s3/backend])
|
||||
|
||||
[::main :app.storage.fs/backend]
|
||||
{:directory (cf/get :storage-fs-directory)}
|
||||
;; keep this for backward compatibility
|
||||
:s3 (ig/ref [::assets :app.storage.s3/backend])
|
||||
:fs (ig/ref [::assets :app.storage.fs/backend])}}
|
||||
|
||||
[::fdata :app.storage.s3/backend]
|
||||
{:region (cf/get :storage-fdata-s3-region)
|
||||
:bucket (cf/get :storage-fdata-s3-bucket)
|
||||
:prefix (cf/get :storage-fdata-s3-prefix)}
|
||||
|
||||
[::assets :app.storage.s3/backend]
|
||||
{:region (cf/get :storage-assets-s3-region)
|
||||
:bucket (cf/get :storage-assets-s3-bucket)}
|
||||
|
||||
[::assets :app.storage.fs/backend]
|
||||
{:directory (cf/get :storage-assets-fs-directory)}
|
||||
|
||||
[::tmp :app.storage.fs/backend]
|
||||
{:directory "/tmp/penpot"}
|
||||
|
||||
[::main :app.storage.db/backend]
|
||||
[::assets :app.storage.db/backend]
|
||||
{:pool (ig/ref :app.db/pool)}})
|
||||
|
||||
(def system nil)
|
||||
|
||||
@@ -11,8 +11,8 @@
|
||||
[app.common.exceptions :as ex]
|
||||
[app.common.media :as cm]
|
||||
[app.common.spec :as us]
|
||||
[app.rlimits :as rlm]
|
||||
[app.rpc.queries.svg :as svg]
|
||||
[app.config :as cf]
|
||||
[app.util.svg :as svg]
|
||||
[buddy.core.bytes :as bb]
|
||||
[buddy.core.codecs :as bc]
|
||||
[clojure.java.io :as io]
|
||||
@@ -28,10 +28,6 @@
|
||||
org.im4java.core.IMOperation
|
||||
org.im4java.core.Info))
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;; --- Utility functions
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
|
||||
(s/def ::image-content-type cm/valid-image-types)
|
||||
(s/def ::font-content-type cm/valid-font-types)
|
||||
|
||||
@@ -54,7 +50,6 @@
|
||||
:code :media-type-not-allowed
|
||||
:hint "Seems like you are uploading an invalid media object"))))
|
||||
|
||||
|
||||
(defmulti process :cmd)
|
||||
(defmulti process-error class)
|
||||
|
||||
@@ -69,17 +64,11 @@
|
||||
(throw error))
|
||||
|
||||
(defn run
|
||||
[{:keys [rlimits] :as cfg} {:keys [rlimit] :or {rlimit :image} :as params}]
|
||||
(us/assert map? rlimits)
|
||||
(let [rlimit (get rlimits rlimit)]
|
||||
(when-not rlimit
|
||||
(ex/raise :type :internal
|
||||
:code :rlimit-not-configured
|
||||
:hint ":image rlimit not configured"))
|
||||
(try
|
||||
(rlm/execute rlimit (process params))
|
||||
(catch Throwable e
|
||||
(process-error e)))))
|
||||
[params]
|
||||
(try
|
||||
(process params)
|
||||
(catch Throwable e
|
||||
(process-error e))))
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;; --- Thumbnails Generation
|
||||
@@ -183,7 +172,7 @@
|
||||
(us/assert ::input input)
|
||||
(let [{:keys [path mtype]} input]
|
||||
(if (= mtype "image/svg+xml")
|
||||
(let [info (some-> path slurp svg/parse get-basic-info-from-svg)]
|
||||
(let [info (some-> path slurp svg/pre-process svg/parse get-basic-info-from-svg)]
|
||||
(when-not info
|
||||
(ex/raise :type :validation
|
||||
:code :invalid-svg-file
|
||||
@@ -330,3 +319,17 @@
|
||||
(= 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 apropriate
|
||||
backend for assets."
|
||||
[storage conn]
|
||||
(-> storage
|
||||
(assoc :conn conn)
|
||||
(assoc :backend (cf/get :assets-storage-backend :assets-fs))))
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
(ns app.metrics
|
||||
(:require
|
||||
[app.common.exceptions :as ex]
|
||||
[app.util.logging :as l]
|
||||
[app.common.logging :as l]
|
||||
[clojure.spec.alpha :as s]
|
||||
[integrant.core :as ig])
|
||||
(:import
|
||||
@@ -92,18 +92,14 @@
|
||||
_ (when (seq labels)
|
||||
(.labelNames instance (into-array String labels)))
|
||||
instance (.register instance registry)]
|
||||
(reify
|
||||
clojure.lang.IDeref
|
||||
(deref [_] instance)
|
||||
|
||||
clojure.lang.IFn
|
||||
(invoke [_ cmd]
|
||||
(.inc ^Counter instance))
|
||||
|
||||
(invoke [_ cmd labels]
|
||||
(.. ^Counter instance
|
||||
(labels (into-array String labels))
|
||||
(inc))))))
|
||||
{::instance instance
|
||||
::fn (fn [{:keys [by labels] :or {by 1}}]
|
||||
(if labels
|
||||
(.. ^Counter instance
|
||||
(labels (into-array String labels))
|
||||
(inc by))
|
||||
(.inc ^Counter instance by)))}))
|
||||
|
||||
(defn make-gauge
|
||||
[{:keys [name help registry reg labels] :as props}]
|
||||
@@ -115,21 +111,16 @@
|
||||
(.labelNames instance (into-array String labels)))
|
||||
instance (.register instance registry)]
|
||||
|
||||
(reify
|
||||
clojure.lang.IDeref
|
||||
(deref [_] instance)
|
||||
|
||||
clojure.lang.IFn
|
||||
(invoke [_ cmd]
|
||||
(case cmd
|
||||
:inc (.inc ^Gauge instance)
|
||||
:dec (.dec ^Gauge instance)))
|
||||
|
||||
(invoke [_ cmd labels]
|
||||
(let [labels (into-array String [labels])]
|
||||
(case cmd
|
||||
:inc (.. ^Gauge instance (labels labels) (inc))
|
||||
:dec (.. ^Gauge instance (labels labels) (dec))))))))
|
||||
{::instance instance
|
||||
::fn (fn [{:keys [cmd by labels] :or {by 1}}]
|
||||
(if labels
|
||||
(let [labels (into-array String [labels])]
|
||||
(case cmd
|
||||
:inc (.. ^Gauge instance (labels labels) (inc by))
|
||||
:dec (.. ^Gauge instance (labels labels) (dec by))))
|
||||
(case cmd
|
||||
:inc (.inc ^Gauge instance by)
|
||||
:dec (.dec ^Gauge instance by))))}))
|
||||
|
||||
(def default-quantiles
|
||||
[[0.75 0.02]
|
||||
@@ -150,18 +141,14 @@
|
||||
_ (when (seq labels)
|
||||
(.labelNames instance (into-array String labels)))
|
||||
instance (.register instance registry)]
|
||||
(reify
|
||||
clojure.lang.IDeref
|
||||
(deref [_] instance)
|
||||
|
||||
clojure.lang.IFn
|
||||
(invoke [_ cmd val]
|
||||
(.observe ^Summary instance val))
|
||||
|
||||
(invoke [_ cmd val labels]
|
||||
(.. ^Summary instance
|
||||
(labels (into-array String labels))
|
||||
(observe val))))))
|
||||
{::instance instance
|
||||
::fn (fn [{:keys [val labels]}]
|
||||
(if labels
|
||||
(.. ^Summary instance
|
||||
(labels (into-array String labels))
|
||||
(observe val))
|
||||
(.observe ^Summary instance val)))}))
|
||||
|
||||
(def default-histogram-buckets
|
||||
[1 5 10 25 50 75 100 250 500 750 1000 2500 5000 7500])
|
||||
@@ -177,18 +164,14 @@
|
||||
_ (when (seq labels)
|
||||
(.labelNames instance (into-array String labels)))
|
||||
instance (.register instance registry)]
|
||||
(reify
|
||||
clojure.lang.IDeref
|
||||
(deref [_] instance)
|
||||
|
||||
clojure.lang.IFn
|
||||
(invoke [_ cmd val]
|
||||
(.observe ^Histogram instance val))
|
||||
|
||||
(invoke [_ cmd val labels]
|
||||
(.. ^Histogram instance
|
||||
(labels (into-array String labels))
|
||||
(observe val))))))
|
||||
{::instance instance
|
||||
::fn (fn [{:keys [val labels]}]
|
||||
(if labels
|
||||
(.. ^Histogram instance
|
||||
(labels (into-array String labels))
|
||||
(observe val))
|
||||
(.observe ^Histogram instance val)))}))
|
||||
|
||||
(defn create
|
||||
[{:keys [type] :as props}]
|
||||
@@ -205,14 +188,20 @@
|
||||
(with-meta
|
||||
(fn
|
||||
([a]
|
||||
(mobj :inc)
|
||||
((::fn mobj) nil)
|
||||
(origf a))
|
||||
([a b]
|
||||
(mobj :inc)
|
||||
((::fn mobj) nil)
|
||||
(origf a b))
|
||||
([a b & more]
|
||||
(mobj :inc)
|
||||
(apply origf a b more)))
|
||||
([a b c]
|
||||
((::fn mobj) nil)
|
||||
(origf a b c))
|
||||
([a b c d]
|
||||
((::fn mobj) nil)
|
||||
(origf a b c d))
|
||||
([a b c d & more]
|
||||
((::fn mobj) nil)
|
||||
(apply origf a b c d more)))
|
||||
(assoc mdata ::original origf))))
|
||||
([rootf mobj labels]
|
||||
(let [mdata (meta rootf)
|
||||
@@ -220,13 +209,13 @@
|
||||
(with-meta
|
||||
(fn
|
||||
([a]
|
||||
(mobj :inc labels)
|
||||
((::fn mobj) {:labels labels})
|
||||
(origf a))
|
||||
([a b]
|
||||
(mobj :inc labels)
|
||||
((::fn mobj) {:labels labels})
|
||||
(origf a b))
|
||||
([a b & more]
|
||||
(mobj :inc labels)
|
||||
((::fn mobj) {:labels labels})
|
||||
(apply origf a b more)))
|
||||
(assoc mdata ::original origf)))))
|
||||
|
||||
@@ -239,15 +228,15 @@
|
||||
([a]
|
||||
(with-measure
|
||||
:expr (origf a)
|
||||
:cb #(mobj :observe %)))
|
||||
:cb #((::fn mobj) {:val %})))
|
||||
([a b]
|
||||
(with-measure
|
||||
:expr (origf a b)
|
||||
:cb #(mobj :observe %)))
|
||||
:cb #((::fn mobj) {:val %})))
|
||||
([a b & more]
|
||||
(with-measure
|
||||
:expr (apply origf a b more)
|
||||
:cb #(mobj :observe %))))
|
||||
:cb #((::fn mobj) {:val %}))))
|
||||
(assoc mdata ::original origf))))
|
||||
|
||||
([rootf mobj labels]
|
||||
@@ -258,26 +247,26 @@
|
||||
([a]
|
||||
(with-measure
|
||||
:expr (origf a)
|
||||
:cb #(mobj :observe % labels)))
|
||||
:cb #((::fn mobj) {:val % :labels labels})))
|
||||
([a b]
|
||||
(with-measure
|
||||
:expr (origf a b)
|
||||
:cb #(mobj :observe % labels)))
|
||||
:cb #((::fn mobj) {:val % :labels labels})))
|
||||
([a b & more]
|
||||
(with-measure
|
||||
:expr (apply origf a b more)
|
||||
:cb #(mobj :observe % labels))))
|
||||
:cb #((::fn mobj) {:val % :labels labels}))))
|
||||
(assoc mdata ::original origf)))))
|
||||
|
||||
(defn instrument-vars!
|
||||
[vars {:keys [wrap] :as props}]
|
||||
(let [obj (create props)]
|
||||
(cond
|
||||
(instance? Counter @obj)
|
||||
(instance? Counter (::instance obj))
|
||||
(doseq [var vars]
|
||||
(alter-var-root var (or wrap wrap-counter) obj))
|
||||
|
||||
(instance? Summary @obj)
|
||||
(instance? Summary (::instance obj))
|
||||
(doseq [var vars]
|
||||
(alter-var-root var (or wrap wrap-summary) obj))
|
||||
|
||||
@@ -288,13 +277,13 @@
|
||||
[f {:keys [wrap] :as props}]
|
||||
(let [obj (create props)]
|
||||
(cond
|
||||
(instance? Counter @obj)
|
||||
(instance? Counter (::instance obj))
|
||||
((or wrap wrap-counter) f obj)
|
||||
|
||||
(instance? Summary @obj)
|
||||
(instance? Summary (::instance obj))
|
||||
((or wrap wrap-summary) f obj)
|
||||
|
||||
(instance? Histogram @obj)
|
||||
(instance? Histogram (::instance obj))
|
||||
((or wrap wrap-summary) f obj)
|
||||
|
||||
:else
|
||||
|
||||
@@ -175,6 +175,33 @@
|
||||
|
||||
{:name "0055-mod-file-media-object-table"
|
||||
:fn (mg/resource "app/migrations/sql/0055-mod-file-media-object-table.sql")}
|
||||
|
||||
{:name "0056-add-missing-index-on-deleted-at"
|
||||
:fn (mg/resource "app/migrations/sql/0056-add-missing-index-on-deleted-at.sql")}
|
||||
|
||||
{:name "0057-del-profile-on-delete-trigger"
|
||||
:fn (mg/resource "app/migrations/sql/0057-del-profile-on-delete-trigger.sql")}
|
||||
|
||||
{:name "0058-del-team-on-delete-trigger"
|
||||
:fn (mg/resource "app/migrations/sql/0058-del-team-on-delete-trigger.sql")}
|
||||
|
||||
{:name "0059-mod-audit-log-table"
|
||||
:fn (mg/resource "app/migrations/sql/0059-mod-audit-log-table.sql")}
|
||||
|
||||
{:name "0060-mod-file-change-table"
|
||||
:fn (mg/resource "app/migrations/sql/0060-mod-file-change-table.sql")}
|
||||
|
||||
{:name "0061-mod-file-table"
|
||||
:fn (mg/resource "app/migrations/sql/0061-mod-file-table.sql")}
|
||||
|
||||
{:name "0062-fix-metadata-media"
|
||||
:fn (mg/resource "app/migrations/sql/0062-fix-metadata-media.sql")}
|
||||
|
||||
{:name "0063-add-share-link-table"
|
||||
:fn (mg/resource "app/migrations/sql/0063-add-share-link-table.sql")}
|
||||
|
||||
{:name "0064-mod-audit-log-table"
|
||||
:fn (mg/resource "app/migrations/sql/0064-mod-audit-log-table.sql")}
|
||||
])
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
CREATE INDEX profile_deleted_at_idx
|
||||
ON profile(deleted_at, id)
|
||||
WHERE deleted_at IS NOT NULL;
|
||||
|
||||
CREATE INDEX project_deleted_at_idx
|
||||
ON project(deleted_at, id)
|
||||
WHERE deleted_at IS NOT NULL;
|
||||
|
||||
CREATE INDEX team_deleted_at_idx
|
||||
ON team(deleted_at, id)
|
||||
WHERE deleted_at IS NOT NULL;
|
||||
|
||||
CREATE INDEX team_font_variant_deleted_at_idx
|
||||
ON team_font_variant(deleted_at, id)
|
||||
WHERE deleted_at IS NOT NULL;
|
||||
@@ -0,0 +1,2 @@
|
||||
DROP TRIGGER profile__on_delete__tgr ON profile CASCADE;
|
||||
DROP FUNCTION on_delete_profile ();
|
||||
@@ -0,0 +1,2 @@
|
||||
DROP TRIGGER team__on_delete__tgr ON team CASCADE;
|
||||
DROP FUNCTION on_delete_team ();
|
||||
@@ -0,0 +1,2 @@
|
||||
ALTER TABLE audit_log
|
||||
ADD COLUMN ip_addr inet NULL;
|
||||
@@ -0,0 +1,2 @@
|
||||
ALTER TABLE file_change
|
||||
ALTER COLUMN data DROP NOT NULL;
|
||||
10
backend/src/app/migrations/sql/0061-mod-file-table.sql
Normal file
10
backend/src/app/migrations/sql/0061-mod-file-table.sql
Normal file
@@ -0,0 +1,10 @@
|
||||
CREATE INDEX IF NOT EXISTS file__modified_at__with__data__idx
|
||||
ON file (modified_at, id)
|
||||
WHERE data IS NOT NULL;
|
||||
|
||||
ALTER TABLE file
|
||||
ADD COLUMN data_backend text NULL,
|
||||
ALTER COLUMN data_backend SET STORAGE EXTERNAL;
|
||||
|
||||
DROP TRIGGER file_on_update_tgr ON file;
|
||||
DROP FUNCTION handle_file_update ();
|
||||
@@ -0,0 +1,8 @@
|
||||
-- Fix problem with content-type inconherence
|
||||
|
||||
UPDATE storage_object so
|
||||
SET metadata = jsonb_set(metadata, '{~:content-type}', to_jsonb(fmo.mtype))
|
||||
FROM file_media_object fmo
|
||||
WHERE so.id = fmo.media_id and
|
||||
so.metadata->>'~:content-type' != fmo.mtype;
|
||||
|
||||
12
backend/src/app/migrations/sql/0063-add-share-link-table.sql
Normal file
12
backend/src/app/migrations/sql/0063-add-share-link-table.sql
Normal file
@@ -0,0 +1,12 @@
|
||||
CREATE TABLE share_link (
|
||||
id uuid PRIMARY KEY DEFAULT uuid_generate_v4(),
|
||||
file_id uuid NOT NULL REFERENCES file(id) ON DELETE CASCADE DEFERRABLE,
|
||||
owner_id uuid NULL REFERENCES profile(id) ON DELETE SET NULL DEFERRABLE,
|
||||
created_at timestamptz NOT NULL DEFAULT clock_timestamp(),
|
||||
|
||||
pages uuid[],
|
||||
flags text[]
|
||||
);
|
||||
|
||||
CREATE INDEX share_link_file_id_idx ON share_link(file_id);
|
||||
CREATE INDEX share_link_owner_id_idx ON share_link(owner_id);
|
||||
13
backend/src/app/migrations/sql/0064-mod-audit-log-table.sql
Normal file
13
backend/src/app/migrations/sql/0064-mod-audit-log-table.sql
Normal file
@@ -0,0 +1,13 @@
|
||||
ALTER TABLE audit_log
|
||||
ADD COLUMN tracked_at timestamptz NULL DEFAULT clock_timestamp(),
|
||||
ADD COLUMN source text NULL,
|
||||
ADD COLUMN context jsonb NULL;
|
||||
|
||||
ALTER TABLE audit_log
|
||||
ALTER COLUMN source SET STORAGE external,
|
||||
ALTER COLUMN context SET STORAGE external;
|
||||
|
||||
UPDATE audit_log SET source = 'backend', tracked_at=created_at;
|
||||
|
||||
-- ALTER TABLE audit_log ALTER COLUMN source SET NOT NULL;
|
||||
-- ALTER TABLE audit_log ALTER COLUMN tracked_at SET NOT NULL;
|
||||
@@ -8,10 +8,10 @@
|
||||
"The msgbus abstraction implemented using redis as underlying backend."
|
||||
(:require
|
||||
[app.common.exceptions :as ex]
|
||||
[app.common.logging :as l]
|
||||
[app.common.spec :as us]
|
||||
[app.config :as cfg]
|
||||
[app.util.blob :as blob]
|
||||
[app.util.logging :as l]
|
||||
[app.util.time :as dt]
|
||||
[clojure.core.async :as a]
|
||||
[clojure.spec.alpha :as s]
|
||||
|
||||
@@ -7,13 +7,13 @@
|
||||
(ns app.notifications
|
||||
"A websocket based notifications mechanism."
|
||||
(:require
|
||||
[app.common.logging :as l]
|
||||
[app.common.spec :as us]
|
||||
[app.common.transit :as t]
|
||||
[app.db :as db]
|
||||
[app.metrics :as mtx]
|
||||
[app.util.async :as aa]
|
||||
[app.util.logging :as l]
|
||||
[app.util.time :as dt]
|
||||
[app.util.transit :as t]
|
||||
[app.worker :as wrk]
|
||||
[clojure.core.async :as a]
|
||||
[clojure.spec.alpha :as s]
|
||||
@@ -69,6 +69,7 @@
|
||||
:mtx-messages mtx-messages
|
||||
:mtx-sessions mtx-sessions
|
||||
)]
|
||||
|
||||
(-> #(handler cfg %)
|
||||
(wrap-session)
|
||||
(wrap-keyword-params)
|
||||
@@ -135,7 +136,7 @@
|
||||
ws-send (mtx/wrap-counter ws-send mtx-messages ["send"])]
|
||||
|
||||
(letfn [(on-connect [conn]
|
||||
(mtx-aconn :inc)
|
||||
((::mtx/fn mtx-aconn) {:cmd :inc :by 1})
|
||||
;; A subscription channel should use a lossy buffer
|
||||
;; because we can't penalize normal clients when one
|
||||
;; slow client is connected to the room.
|
||||
@@ -162,8 +163,8 @@
|
||||
(a/<! (handle-connect cfg))
|
||||
|
||||
;; when connection is closed
|
||||
(mtx-aconn :dec)
|
||||
(mtx-sessions :observe (/ (inst-ms (dt/duration-between created-at (dt/now))) 1000.0))
|
||||
((::mtx/fn mtx-aconn) {:cmd :dec :by 1})
|
||||
((::mtx/fn mtx-sessions) {:val (/ (inst-ms (dt/diff created-at (dt/now))) 1000.0)})
|
||||
|
||||
;; close subscription
|
||||
(a/close! sub-ch))))
|
||||
|
||||
@@ -1,45 +0,0 @@
|
||||
;; This Source Code Form is subject to the terms of the Mozilla Public
|
||||
;; License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
;;
|
||||
;; Copyright (c) UXBOX Labs SL
|
||||
|
||||
(ns app.rlimits
|
||||
"Resource usage limits (in other words: semaphores)."
|
||||
(:require
|
||||
[app.common.spec :as us]
|
||||
[clojure.spec.alpha :as s]
|
||||
[integrant.core :as ig])
|
||||
(:import
|
||||
java.util.concurrent.Semaphore))
|
||||
|
||||
(s/def ::rlimit #(instance? Semaphore %))
|
||||
(s/def ::rlimits (s/map-of ::us/keyword ::rlimit))
|
||||
|
||||
(derive ::password ::instance)
|
||||
(derive ::image ::instance)
|
||||
(derive ::font ::instance)
|
||||
|
||||
(defmethod ig/pre-init-spec ::instance [_]
|
||||
(s/spec int?))
|
||||
|
||||
(defmethod ig/init-key ::instance
|
||||
[_ permits]
|
||||
(Semaphore. (int permits)))
|
||||
|
||||
(defn acquire!
|
||||
[sem]
|
||||
(.acquire ^Semaphore sem))
|
||||
|
||||
(defn release!
|
||||
[sem]
|
||||
(.release ^Semaphore sem))
|
||||
|
||||
(defmacro execute
|
||||
[rlinst & body]
|
||||
`(try
|
||||
(acquire! ~rlinst)
|
||||
~@body
|
||||
(finally
|
||||
(release! ~rlinst))))
|
||||
|
||||
@@ -8,15 +8,15 @@
|
||||
(:require
|
||||
[app.common.data :as d]
|
||||
[app.common.exceptions :as ex]
|
||||
[app.common.logging :as l]
|
||||
[app.common.spec :as us]
|
||||
[app.db :as db]
|
||||
[app.loggers.audit :as audit]
|
||||
[app.metrics :as mtx]
|
||||
[app.rlimits :as rlm]
|
||||
[app.util.logging :as l]
|
||||
[app.util.retry :as retry]
|
||||
[app.util.rlimit :as rlimit]
|
||||
[app.util.services :as sv]
|
||||
[clojure.spec.alpha :as s]
|
||||
[cuerdas.core :as str]
|
||||
[integrant.core :as ig]))
|
||||
|
||||
(defn- default-handler
|
||||
@@ -32,9 +32,10 @@
|
||||
[methods {:keys [profile-id] :as request}]
|
||||
(let [type (keyword (get-in request [:path-params :type]))
|
||||
|
||||
data (d/merge (:params request)
|
||||
(:body-params request)
|
||||
(:uploads request))
|
||||
data (merge (:params request)
|
||||
(:body-params request)
|
||||
(:uploads request)
|
||||
{::request request})
|
||||
|
||||
data (if profile-id
|
||||
(assoc data :profile-id profile-id)
|
||||
@@ -50,12 +51,15 @@
|
||||
(defn- rpc-mutation-handler
|
||||
[methods {:keys [profile-id] :as request}]
|
||||
(let [type (keyword (get-in request [:path-params :type]))
|
||||
data (d/merge (:params request)
|
||||
(:body-params request)
|
||||
(:uploads request))
|
||||
data (merge (:params request)
|
||||
(:body-params request)
|
||||
(:uploads request)
|
||||
{::request request})
|
||||
|
||||
data (if profile-id
|
||||
(assoc data :profile-id profile-id)
|
||||
(dissoc data :profile-id))
|
||||
|
||||
result ((get methods type default-handler) data)
|
||||
mdata (meta result)]
|
||||
(cond->> {:status 200 :body result}
|
||||
@@ -69,50 +73,49 @@
|
||||
[cfg f mdata]
|
||||
(mtx/wrap-summary f (::mobj cfg) [(::sv/name mdata)]))
|
||||
|
||||
;; Wrap the rpc handler with a semaphore if it is specified in the
|
||||
;; metadata asocciated with the handler.
|
||||
(defn- wrap-with-rlimits
|
||||
[cfg f mdata]
|
||||
(if-let [key (:rlimit mdata)]
|
||||
(let [rlinst (get-in cfg [:rlimits key])]
|
||||
(when-not rlinst
|
||||
(ex/raise :type :internal
|
||||
:code :rlimit-not-configured
|
||||
:hint (str/fmt "%s rlimit not configured" key)))
|
||||
(l/trace :action "add rlimit"
|
||||
:handler (::sv/name mdata))
|
||||
(fn [cfg params]
|
||||
(rlm/execute rlinst (f cfg params))))
|
||||
f))
|
||||
|
||||
|
||||
(defn- wrap-impl
|
||||
[{:keys [audit] :as cfg} f mdata]
|
||||
(let [f (wrap-with-rlimits cfg f mdata)
|
||||
f (wrap-with-metrics cfg f mdata)
|
||||
spec (or (::sv/spec mdata) (s/spec any?))
|
||||
auth? (:auth mdata true)]
|
||||
(let [f (as-> f $
|
||||
(rlimit/wrap-rlimit cfg $ mdata)
|
||||
(retry/wrap-retry cfg $ mdata)
|
||||
(wrap-with-metrics cfg $ mdata))
|
||||
|
||||
spec (or (::sv/spec mdata) (s/spec any?))
|
||||
auth? (:auth mdata true)]
|
||||
|
||||
(l/trace :action "register" :name (::sv/name mdata))
|
||||
(fn [params]
|
||||
(when (and auth? (not (uuid? (:profile-id params))))
|
||||
(ex/raise :type :authentication
|
||||
:code :authentication-required
|
||||
:hint "authentication required for this endpoint"))
|
||||
(let [params (us/conform spec params)
|
||||
result (f cfg params)
|
||||
resultm (meta result)]
|
||||
(when (and (::type cfg) (fn? audit))
|
||||
(let [profile-id (or (:profile-id params)
|
||||
(:profile-id result)
|
||||
(::audit/profile-id resultm))
|
||||
props (d/merge params (::audit/props resultm))]
|
||||
(audit :submit {:type (::type cfg)
|
||||
:name (or (::audit/name resultm)
|
||||
(::sv/name mdata))
|
||||
:profile-id profile-id
|
||||
:props props})))
|
||||
result))))
|
||||
(with-meta
|
||||
(fn [params]
|
||||
;; Raise authentication error when rpc method requires auth but
|
||||
;; no profile-id is found in the request.
|
||||
(when (and auth? (not (uuid? (:profile-id params))))
|
||||
(ex/raise :type :authentication
|
||||
:code :authentication-required
|
||||
:hint "authentication required for this endpoint"))
|
||||
|
||||
(let [params' (dissoc params ::request)
|
||||
params' (us/conform spec params')
|
||||
result (f cfg params')]
|
||||
|
||||
;; When audit log is enabled (default false).
|
||||
(when (fn? audit)
|
||||
(let [resultm (meta result)
|
||||
request (::request params)
|
||||
profile-id (or (:profile-id params')
|
||||
(:profile-id result)
|
||||
(::audit/profile-id resultm))
|
||||
props (d/merge params' (::audit/props resultm))]
|
||||
(audit :cmd :submit
|
||||
:type (or (::audit/type resultm)
|
||||
(::type cfg))
|
||||
:name (or (::audit/name resultm)
|
||||
(::sv/name mdata))
|
||||
:profile-id profile-id
|
||||
:ip-addr (audit/parse-client-ip request)
|
||||
:props props)))
|
||||
|
||||
result))
|
||||
mdata)))
|
||||
|
||||
(defn- process-method
|
||||
[cfg vfn]
|
||||
@@ -134,10 +137,8 @@
|
||||
'app.rpc.queries.teams
|
||||
'app.rpc.queries.comments
|
||||
'app.rpc.queries.profile
|
||||
'app.rpc.queries.recent-files
|
||||
'app.rpc.queries.viewer
|
||||
'app.rpc.queries.fonts
|
||||
'app.rpc.queries.svg)
|
||||
'app.rpc.queries.fonts)
|
||||
(map (partial process-method cfg))
|
||||
(into {}))))
|
||||
|
||||
@@ -156,11 +157,11 @@
|
||||
'app.rpc.mutations.files
|
||||
'app.rpc.mutations.comments
|
||||
'app.rpc.mutations.projects
|
||||
'app.rpc.mutations.viewer
|
||||
'app.rpc.mutations.teams
|
||||
'app.rpc.mutations.management
|
||||
'app.rpc.mutations.ldap
|
||||
'app.rpc.mutations.fonts
|
||||
'app.rpc.mutations.share-link
|
||||
'app.rpc.mutations.verify-token)
|
||||
(map (partial process-method cfg))
|
||||
(into {}))))
|
||||
@@ -172,7 +173,7 @@
|
||||
|
||||
(defmethod ig/pre-init-spec ::rpc [_]
|
||||
(s/keys :req-un [::storage ::session ::tokens ::audit
|
||||
::mtx/metrics ::rlm/rlimits ::db/pool]))
|
||||
::mtx/metrics ::db/pool]))
|
||||
|
||||
(defmethod ig/init-key ::rpc
|
||||
[_ cfg]
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
[app.rpc.queries.comments :as comments]
|
||||
[app.rpc.queries.files :as files]
|
||||
[app.util.blob :as blob]
|
||||
[app.util.retry :as retry]
|
||||
[app.util.services :as sv]
|
||||
[app.util.time :as dt]
|
||||
[clojure.spec.alpha :as s]))
|
||||
@@ -32,6 +33,9 @@
|
||||
(s/keys :req-un [::profile-id ::file-id ::position ::content ::page-id]))
|
||||
|
||||
(sv/defmethod ::create-comment-thread
|
||||
{::retry/enabled true
|
||||
::retry/max-retries 3
|
||||
::retry/matches retry/conflict-db-insert?}
|
||||
[{:keys [pool] :as cfg} {:keys [profile-id file-id] :as params}]
|
||||
(db/with-atomic [conn pool]
|
||||
(files/check-read-permissions! conn profile-id file-id)
|
||||
@@ -43,7 +47,7 @@
|
||||
res (db/exec-one! conn [sql file-id])]
|
||||
(:next-seqn res)))
|
||||
|
||||
(defn- create-comment-thread*
|
||||
(defn- create-comment-thread
|
||||
[conn {:keys [profile-id file-id page-id position content] :as params}]
|
||||
(let [seqn (retrieve-next-seqn conn file-id)
|
||||
now (dt/now)
|
||||
@@ -78,24 +82,6 @@
|
||||
|
||||
(select-keys thread [:id :file-id :page-id])))
|
||||
|
||||
(defn- create-comment-thread
|
||||
[conn params]
|
||||
(loop [sp (db/savepoint conn)
|
||||
rc 0]
|
||||
(let [res (ex/try (create-comment-thread* conn params))]
|
||||
(cond
|
||||
(and (instance? Throwable res)
|
||||
(< rc 3))
|
||||
(do
|
||||
(db/rollback! conn sp)
|
||||
(recur (db/savepoint conn)
|
||||
(inc rc)))
|
||||
|
||||
(instance? Throwable res)
|
||||
(throw res)
|
||||
|
||||
:else res))))
|
||||
|
||||
(defn- retrieve-page-name
|
||||
[conn {:keys [file-id page-id]}]
|
||||
(let [{:keys [data]} (db/get-by-id conn :file file-id)
|
||||
|
||||
@@ -9,13 +9,12 @@
|
||||
(:require
|
||||
[app.common.exceptions :as ex]
|
||||
[app.common.uuid :as uuid]
|
||||
[app.config :as cfg]
|
||||
[app.config :as cf]
|
||||
[app.db :as db]
|
||||
[app.loggers.audit :as audit]
|
||||
[app.rpc.mutations.profile :as profile]
|
||||
[app.setup.initial-data :as sid]
|
||||
[app.util.services :as sv]
|
||||
[app.worker :as wrk]
|
||||
[app.util.time :as dt]
|
||||
[buddy.core.codecs :as bc]
|
||||
[buddy.core.nonce :as bn]
|
||||
[clojure.spec.alpha :as s]))
|
||||
@@ -34,25 +33,19 @@
|
||||
params {:id id
|
||||
:email email
|
||||
:fullname fullname
|
||||
:is-demo true
|
||||
:is-active true
|
||||
:deleted-at (dt/in-future cf/deletion-delay)
|
||||
:password password
|
||||
:props {:onboarding-viewed true}}]
|
||||
|
||||
(when-not (cfg/get :allow-demo-users)
|
||||
(when-not (contains? cf/flags :demo-users)
|
||||
(ex/raise :type :validation
|
||||
:code :demo-users-not-allowed
|
||||
:hint "Demo users are disabled by config."))
|
||||
|
||||
(db/with-atomic [conn pool]
|
||||
(->> (#'profile/create-profile conn params)
|
||||
(#'profile/create-profile-relations conn)
|
||||
(sid/load-initial-project! conn))
|
||||
|
||||
;; Schedule deletion of the demo profile
|
||||
(wrk/submit! {::wrk/task :delete-profile
|
||||
::wrk/delay cfg/deletion-delay
|
||||
::wrk/conn conn
|
||||
:profile-id id})
|
||||
(#'profile/create-profile-relations conn))
|
||||
|
||||
(with-meta {:email email
|
||||
:password password}
|
||||
|
||||
@@ -11,17 +11,19 @@
|
||||
[app.common.pages.migrations :as pmg]
|
||||
[app.common.spec :as us]
|
||||
[app.common.uuid :as uuid]
|
||||
[app.config :as cfg]
|
||||
[app.db :as db]
|
||||
[app.metrics :as mtx]
|
||||
[app.rpc.permissions :as perms]
|
||||
[app.rpc.queries.files :as files]
|
||||
[app.rpc.queries.projects :as proj]
|
||||
[app.storage.impl :as simpl]
|
||||
[app.util.blob :as blob]
|
||||
[app.util.services :as sv]
|
||||
[app.util.time :as dt]
|
||||
[app.worker :as wrk]
|
||||
[clojure.spec.alpha :as s]))
|
||||
|
||||
(declare create-file)
|
||||
|
||||
;; --- Helpers & Specs
|
||||
|
||||
(s/def ::id ::us/uuid)
|
||||
@@ -32,8 +34,6 @@
|
||||
|
||||
;; --- Mutation: Create File
|
||||
|
||||
(declare create-file)
|
||||
|
||||
(s/def ::is-shared ::us/boolean)
|
||||
(s/def ::create-file
|
||||
(s/keys :req-un [::profile-id ::name ::project-id]
|
||||
@@ -45,7 +45,6 @@
|
||||
(proj/check-edition-permissions! conn profile-id project-id)
|
||||
(create-file conn params)))
|
||||
|
||||
|
||||
(defn create-file-role
|
||||
[conn {:keys [file-id profile-id role]}]
|
||||
(let [params {:file-id file-id
|
||||
@@ -54,21 +53,24 @@
|
||||
(db/insert! conn :file-profile-rel))))
|
||||
|
||||
(defn create-file
|
||||
[conn {:keys [id name project-id is-shared]
|
||||
:or {is-shared false}
|
||||
[conn {:keys [id name project-id is-shared data deleted-at]
|
||||
:or {is-shared false
|
||||
deleted-at nil}
|
||||
:as params}]
|
||||
(let [id (or id (uuid/next))
|
||||
data (cp/make-file-data id)
|
||||
(let [id (or id (:id data) (uuid/next))
|
||||
data (or data (cp/make-file-data id))
|
||||
file (db/insert! conn :file
|
||||
{:id id
|
||||
:project-id project-id
|
||||
:name name
|
||||
:is-shared is-shared
|
||||
:data (blob/encode data)})]
|
||||
:data (blob/encode data)
|
||||
:deleted-at deleted-at})]
|
||||
|
||||
(->> (assoc params :file-id id :role :owner)
|
||||
(create-file-role conn))
|
||||
(assoc file :data data)))
|
||||
|
||||
(assoc file :data data)))
|
||||
|
||||
;; --- Mutation: Rename File
|
||||
|
||||
@@ -109,7 +111,6 @@
|
||||
{:is-shared is-shared}
|
||||
{:id id}))
|
||||
|
||||
|
||||
;; --- Mutation: Delete File
|
||||
|
||||
(declare mark-file-deleted)
|
||||
@@ -122,13 +123,6 @@
|
||||
(db/with-atomic [conn pool]
|
||||
(files/check-edition-permissions! conn profile-id id)
|
||||
|
||||
;; Schedule object deletion
|
||||
(wrk/submit! {::wrk/task :delete-object
|
||||
::wrk/delay cfg/deletion-delay
|
||||
::wrk/conn conn
|
||||
:id id
|
||||
:type :file})
|
||||
|
||||
(mark-file-deleted conn params)))
|
||||
|
||||
(defn mark-file-deleted
|
||||
@@ -175,7 +169,7 @@
|
||||
(s/keys :req-un [::profile-id ::file-id ::library-id]))
|
||||
|
||||
(sv/defmethod ::unlink-file-from-library
|
||||
[{:keys [pool] :as cfg} {:keys [profile-id file-id library-id] :as params}]
|
||||
[{:keys [pool] :as cfg} {:keys [profile-id file-id] :as params}]
|
||||
(db/with-atomic [conn pool]
|
||||
(files/check-edition-permissions! conn profile-id file-id)
|
||||
(unlink-file-from-library conn params)))
|
||||
@@ -195,7 +189,7 @@
|
||||
(s/keys :req-un [::profile-id ::file-id ::library-id]))
|
||||
|
||||
(sv/defmethod ::update-sync
|
||||
[{:keys [pool] :as cfg} {:keys [profile-id file-id library-id] :as params}]
|
||||
[{:keys [pool] :as cfg} {:keys [profile-id file-id] :as params}]
|
||||
(db/with-atomic [conn pool]
|
||||
(files/check-edition-permissions! conn profile-id file-id)
|
||||
(update-sync conn params)))
|
||||
@@ -207,7 +201,6 @@
|
||||
{:file-id file-id
|
||||
:library-file-id library-id}))
|
||||
|
||||
|
||||
;; --- Mutation: Ignore updates in linked files
|
||||
|
||||
(declare ignore-sync)
|
||||
@@ -216,7 +209,7 @@
|
||||
(s/keys :req-un [::profile-id ::file-id ::date]))
|
||||
|
||||
(sv/defmethod ::ignore-sync
|
||||
[{:keys [pool] :as cfg} {:keys [profile-id file-id date] :as params}]
|
||||
[{:keys [pool] :as cfg} {:keys [profile-id file-id] :as params}]
|
||||
(db/with-atomic [conn pool]
|
||||
(files/check-edition-permissions! conn profile-id file-id)
|
||||
(ignore-sync conn params)))
|
||||
@@ -278,28 +271,53 @@
|
||||
(sv/defmethod ::update-file
|
||||
[{:keys [pool] :as cfg} {:keys [id profile-id] :as params}]
|
||||
(db/with-atomic [conn pool]
|
||||
(let [{:keys [id] :as file} (db/get-by-id conn :file id {:for-update true})]
|
||||
(db/xact-lock! conn id)
|
||||
(let [{:keys [id] :as file} (db/get-by-id conn :file id {:for-key-share true})]
|
||||
(files/check-edition-permissions! conn profile-id id)
|
||||
(update-file (assoc cfg :conn conn)
|
||||
(assoc params :file file)))))
|
||||
|
||||
(defn- take-snapshot?
|
||||
"Defines the rule when file `data` snapshot should be saved."
|
||||
[{:keys [revn modified-at] :as file}]
|
||||
;; The snapshot will be saved every 20 changes or if the last
|
||||
;; modification is older than 3 hour.
|
||||
(or (zero? (mod revn 20))
|
||||
(> (inst-ms (dt/diff modified-at (dt/now)))
|
||||
(inst-ms (dt/duration {:hours 3})))))
|
||||
|
||||
(defn- delete-from-storage
|
||||
[{:keys [storage] :as cfg} file]
|
||||
(when-let [backend (simpl/resolve-backend storage (:data-backend file))]
|
||||
(simpl/del-object backend file)))
|
||||
|
||||
(defn- update-file
|
||||
[{:keys [conn] :as cfg} {:keys [file changes changes-with-metadata session-id profile-id] :as params}]
|
||||
[{:keys [conn metrics] :as cfg} {:keys [file changes changes-with-metadata session-id profile-id] :as params}]
|
||||
(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)}))
|
||||
|
||||
(let [changes (if changes-with-metadata
|
||||
(let [mtx1 (get-in metrics [:definitions :update-file-changes])
|
||||
mtx2 (get-in metrics [:definitions :update-file-bytes-processed])
|
||||
|
||||
changes (if changes-with-metadata
|
||||
(mapcat :changes changes-with-metadata)
|
||||
changes)
|
||||
|
||||
file (-> file
|
||||
;; Trace the number of changes processed
|
||||
_ ((::mtx/fn mtx1) {:by (count changes)})
|
||||
|
||||
ts (dt/now)
|
||||
file (-> (files/retrieve-data cfg file)
|
||||
(update :revn inc)
|
||||
(update :data (fn [data]
|
||||
;; Trace the length of bytes of processed data
|
||||
((::mtx/fn mtx2) {:by (alength data)})
|
||||
(-> data
|
||||
(blob/decode)
|
||||
(assoc :id (:id file))
|
||||
@@ -311,26 +329,55 @@
|
||||
{:id (uuid/next)
|
||||
:session-id session-id
|
||||
:profile-id profile-id
|
||||
:created-at ts
|
||||
:file-id (:id file)
|
||||
:revn (:revn file)
|
||||
:data (:data file)
|
||||
:data (when (take-snapshot? file)
|
||||
(:data file))
|
||||
:changes (blob/encode changes)})
|
||||
|
||||
;; Update file
|
||||
(db/update! conn :file
|
||||
{:revn (:revn file)
|
||||
:data (:data file)
|
||||
:data-backend nil
|
||||
:modified-at ts
|
||||
:has-media-trimmed false}
|
||||
{:id (:id file)})
|
||||
|
||||
(let [params (-> params (assoc :file file
|
||||
:changes changes))]
|
||||
;; We need to delete the data from external storage backend
|
||||
(when-not (nil? (:data-backend file))
|
||||
(delete-from-storage cfg file))
|
||||
|
||||
(db/update! conn :project
|
||||
{:modified-at ts}
|
||||
{:id (:project-id file)})
|
||||
|
||||
(let [params (assoc params :file file :changes changes)]
|
||||
;; Send asynchronous notifications
|
||||
(send-notifications cfg params)
|
||||
|
||||
;; Retrieve and return lagged data
|
||||
(retrieve-lagged-changes conn params))))
|
||||
|
||||
(def ^:private
|
||||
sql:lagged-changes
|
||||
"select s.id, s.revn, s.file_id,
|
||||
s.session_id, s.changes
|
||||
from file_change as s
|
||||
where s.file_id = ?
|
||||
and s.revn > ?
|
||||
order by s.created_at asc")
|
||||
|
||||
(defn- retrieve-lagged-changes
|
||||
[conn params]
|
||||
(->> (db/exec! conn [sql:lagged-changes (:id params) (:revn params)])
|
||||
(into [] (comp (map files/decode-row)
|
||||
(map (fn [row]
|
||||
(cond-> row
|
||||
(= (:revn row) (:revn (:file params)))
|
||||
(assoc :changes []))))))))
|
||||
|
||||
(defn- send-notifications
|
||||
[{:keys [msgbus conn] :as cfg} {:keys [file changes session-id] :as params}]
|
||||
(let [lchanges (filter library-change? changes)]
|
||||
@@ -362,17 +409,24 @@
|
||||
[conn project-id]
|
||||
(:team-id (db/get-by-id conn :project project-id {:columns [:team-id]})))
|
||||
|
||||
(def ^:private
|
||||
sql:lagged-changes
|
||||
"select s.id, s.revn, s.file_id,
|
||||
s.session_id, s.changes
|
||||
from file_change as s
|
||||
where s.file_id = ?
|
||||
and s.revn > ?
|
||||
order by s.created_at asc")
|
||||
|
||||
(defn- retrieve-lagged-changes
|
||||
[conn params]
|
||||
(->> (db/exec! conn [sql:lagged-changes (:id params) (:revn params)])
|
||||
(mapv files/decode-row)))
|
||||
;; TEMPORARY FILE CREATION
|
||||
|
||||
(s/def ::create-temp-file ::create-file)
|
||||
|
||||
(sv/defmethod ::create-temp-file
|
||||
[{:keys [pool] :as cfg} {:keys [profile-id project-id] :as params}]
|
||||
(db/with-atomic [conn pool]
|
||||
(proj/check-edition-permissions! conn profile-id project-id)
|
||||
(create-file conn (assoc params :deleted-at (dt/in-future {:days 1})))))
|
||||
|
||||
(s/def ::persist-temp-file
|
||||
(s/keys :req-un [::id ::profile-id]))
|
||||
|
||||
(sv/defmethod ::persist-temp-file
|
||||
[{:keys [pool] :as cfg} {:keys [id profile-id] :as params}]
|
||||
(db/with-atomic [conn pool]
|
||||
(files/check-edition-permissions! conn profile-id id)
|
||||
(db/update! conn :file
|
||||
{:deleted-at nil}
|
||||
{:id id})))
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
|
||||
(ns app.rpc.mutations.fonts
|
||||
(:require
|
||||
[app.common.exceptions :as ex]
|
||||
[app.common.spec :as us]
|
||||
[app.common.uuid :as uuid]
|
||||
[app.config :as cf]
|
||||
@@ -13,9 +14,9 @@
|
||||
[app.media :as media]
|
||||
[app.rpc.queries.teams :as teams]
|
||||
[app.storage :as sto]
|
||||
[app.util.rlimit :as rlimit]
|
||||
[app.util.services :as sv]
|
||||
[app.util.time :as dt]
|
||||
[app.worker :as wrk]
|
||||
[clojure.spec.alpha :as s]))
|
||||
|
||||
(declare create-font-variant)
|
||||
@@ -38,6 +39,7 @@
|
||||
::font-id ::font-family ::font-weight ::font-style]))
|
||||
|
||||
(sv/defmethod ::create-font-variant
|
||||
{::rlimit/permits (cf/get :rlimit-font)}
|
||||
[{:keys [pool] :as cfg} {:keys [team-id profile-id] :as params}]
|
||||
(db/with-atomic [conn pool]
|
||||
(let [cfg (assoc cfg :conn conn)]
|
||||
@@ -46,8 +48,9 @@
|
||||
|
||||
(defn create-font-variant
|
||||
[{:keys [conn storage] :as cfg} {:keys [data] :as params}]
|
||||
(let [data (media/run cfg {:cmd :generate-fonts :input data :rlimit :font})
|
||||
storage (assoc storage :conn conn)
|
||||
(let [data (media/run {:cmd :generate-fonts :input data})
|
||||
storage (media/configure-assets-storage storage conn)
|
||||
|
||||
otf (when-let [fdata (get data "font/otf")]
|
||||
(sto/put-object storage {:content (sto/content fdata)
|
||||
:content-type "font/otf"}))
|
||||
@@ -64,6 +67,13 @@
|
||||
(sto/put-object storage {:content (sto/content fdata)
|
||||
:content-type "font/woff2"}))]
|
||||
|
||||
(when (and (nil? otf)
|
||||
(nil? ttf)
|
||||
(nil? woff1)
|
||||
(nil? woff2))
|
||||
(ex/raise :type :validation
|
||||
:code :invalid-font-upload))
|
||||
|
||||
(db/insert! conn :team-font-variant
|
||||
{:id (uuid/next)
|
||||
:team-id (:team-id params)
|
||||
@@ -104,21 +114,10 @@
|
||||
(db/with-atomic [conn pool]
|
||||
(teams/check-edition-permissions! conn profile-id team-id)
|
||||
|
||||
(let [items (db/query conn :team-font-variant
|
||||
{:font-id id :team-id team-id}
|
||||
{:for-update true})]
|
||||
(doseq [item items]
|
||||
;; Schedule object deletion
|
||||
(wrk/submit! {::wrk/task :delete-object
|
||||
::wrk/delay cf/deletion-delay
|
||||
::wrk/conn conn
|
||||
:id (:id item)
|
||||
:type :team-font-variant}))
|
||||
|
||||
(db/update! conn :team-font-variant
|
||||
{:deleted-at (dt/now)}
|
||||
{:font-id id :team-id team-id})
|
||||
nil)))
|
||||
(db/update! conn :team-font-variant
|
||||
{:deleted-at (dt/now)}
|
||||
{:font-id id :team-id team-id})
|
||||
nil))
|
||||
|
||||
;; --- DELETE FONT VARIANT
|
||||
|
||||
@@ -130,13 +129,6 @@
|
||||
(db/with-atomic [conn pool]
|
||||
(teams/check-edition-permissions! conn profile-id team-id)
|
||||
|
||||
;; Schedule object deletion
|
||||
(wrk/submit! {::wrk/task :delete-object
|
||||
::wrk/delay cf/deletion-delay
|
||||
::wrk/conn conn
|
||||
:id id
|
||||
:type :team-font-variant})
|
||||
|
||||
(db/update! conn :team-font-variant
|
||||
{:deleted-at (dt/now)}
|
||||
{:id id :team-id team-id})
|
||||
|
||||
@@ -7,14 +7,26 @@
|
||||
(ns app.rpc.mutations.ldap
|
||||
(:require
|
||||
[app.common.exceptions :as ex]
|
||||
[app.common.logging :as l]
|
||||
[app.common.spec :as us]
|
||||
[app.config :as cfg]
|
||||
[app.rpc.mutations.profile :refer [login-or-register]]
|
||||
[app.db :as db]
|
||||
[app.loggers.audit :as audit]
|
||||
[app.rpc.mutations.profile :as profile-m]
|
||||
[app.rpc.queries.profile :as profile-q]
|
||||
[app.util.services :as sv]
|
||||
[clj-ldap.client :as ldap]
|
||||
[clojure.spec.alpha :as s]
|
||||
[clojure.string]))
|
||||
|
||||
|
||||
(s/def ::fullname ::us/not-empty-string)
|
||||
(s/def ::email ::us/email)
|
||||
(s/def ::backend ::us/not-empty-string)
|
||||
|
||||
(s/def ::info-data
|
||||
(s/keys :req-un [::fullname ::email ::backend]))
|
||||
|
||||
(defn ^java.lang.AutoCloseable connect
|
||||
[]
|
||||
(let [params {:ssl? (cfg/get :ldap-ssl)
|
||||
@@ -34,6 +46,7 @@
|
||||
;; --- Mutation: login-with-ldap
|
||||
|
||||
(declare authenticate)
|
||||
(declare login-or-register)
|
||||
|
||||
(s/def ::email ::us/email)
|
||||
(s/def ::password ::us/string)
|
||||
@@ -44,31 +57,44 @@
|
||||
:opt-un [::invitation-token]))
|
||||
|
||||
(sv/defmethod ::login-with-ldap {:auth false :rlimit :password}
|
||||
[{:keys [pool session tokens] :as cfg} {:keys [email password invitation-token] :as params}]
|
||||
(let [info (authenticate params)
|
||||
cfg (assoc cfg :conn pool)]
|
||||
(when-not info
|
||||
(ex/raise :type :validation
|
||||
:code :wrong-credentials))
|
||||
(let [profile (login-or-register cfg {:email (:email info)
|
||||
:backend (:backend info)
|
||||
:fullname (:fullname info)})]
|
||||
(if-let [token (:invitation-token params)]
|
||||
;; If invitation token comes in params, this is because the
|
||||
;; user comes from team-invitation process; in this case,
|
||||
;; regenerate token and send back to the user a new invitation
|
||||
;; token (and mark current session as logged).
|
||||
(let [claims (tokens :verify {:token token :iss :team-invitation})
|
||||
claims (assoc claims
|
||||
:member-id (:id profile)
|
||||
:member-email (:email profile))
|
||||
token (tokens :generate claims)]
|
||||
(with-meta
|
||||
{:invitation-token token}
|
||||
{:transform-response ((:create session) (:id profile))}))
|
||||
[{:keys [pool session tokens] :as cfg} params]
|
||||
(db/with-atomic [conn pool]
|
||||
(let [info (authenticate params)
|
||||
cfg (assoc cfg :conn conn)]
|
||||
|
||||
(with-meta profile
|
||||
{:transform-response ((:create session) (:id profile))})))))
|
||||
(when-not info
|
||||
(ex/raise :type :validation
|
||||
:code :wrong-credentials))
|
||||
|
||||
(when-not (s/valid? ::info-data info)
|
||||
(let [explain (s/explain-str ::info-data info)]
|
||||
(l/warn ::l/raw (str "invalid response from ldap, looks like ldap is not configured correctly\n" explain))
|
||||
(ex/raise :type :restriction
|
||||
:code :wrong-ldap-response
|
||||
:reason explain)))
|
||||
|
||||
(let [profile (login-or-register cfg {:email (:email info)
|
||||
:backend (:backend info)
|
||||
:fullname (:fullname info)})]
|
||||
(if-let [token (:invitation-token params)]
|
||||
;; If invitation token comes in params, this is because the
|
||||
;; user comes from team-invitation process; in this case,
|
||||
;; regenerate token and send back to the user a new invitation
|
||||
;; token (and mark current session as logged).
|
||||
(let [claims (tokens :verify {:token token :iss :team-invitation})
|
||||
claims (assoc claims
|
||||
:member-id (:id profile)
|
||||
:member-email (:email profile))
|
||||
token (tokens :generate claims)]
|
||||
(with-meta {:invitation-token token}
|
||||
{:transform-response ((:create session) (:id profile))
|
||||
::audit/props (:props profile)
|
||||
::audit/profile-id (:id profile)}))
|
||||
|
||||
(with-meta profile
|
||||
{:transform-response ((:create session) (:id profile))
|
||||
::audit/props (:props profile)
|
||||
::audit/profile-id (:id profile)}))))))
|
||||
|
||||
(defn- replace-several [s & {:as replacements}]
|
||||
(reduce-kv clojure.string/replace s replacements))
|
||||
@@ -84,15 +110,31 @@
|
||||
(cfg/get :ldap-attrs-fullname)]
|
||||
|
||||
base-dn (cfg/get :ldap-base-dn)
|
||||
params {:filter query :sizelimit 1 :attributes attrs}]
|
||||
params {:filter query
|
||||
:sizelimit 1
|
||||
:attributes attrs}]
|
||||
(first (ldap/search cpool base-dn params))))
|
||||
|
||||
(defn- authenticate
|
||||
[{:keys [password] :as params}]
|
||||
[{:keys [password email] :as params}]
|
||||
(with-open [conn (connect)]
|
||||
(when-let [{:keys [dn] :as luser} (get-ldap-user conn params)]
|
||||
(when (ldap/bind? conn dn password)
|
||||
{:photo (get luser (keyword (cfg/get :ldap-attrs-photo)))
|
||||
:fullname (get luser (keyword (cfg/get :ldap-attrs-fullname)))
|
||||
:email (get luser (keyword (cfg/get :ldap-attrs-email)))
|
||||
:email email
|
||||
:backend "ldap"}))))
|
||||
|
||||
(defn- login-or-register
|
||||
[{:keys [conn] :as cfg} info]
|
||||
(or (some->> (:email info)
|
||||
(profile-q/retrieve-profile-data-by-email conn)
|
||||
(profile-q/populate-additional-data conn)
|
||||
(profile-q/decode-profile-row))
|
||||
(let [params (-> info
|
||||
(assoc :is-active true)
|
||||
(assoc :is-demo false))]
|
||||
(->> params
|
||||
(profile-m/create-profile conn)
|
||||
(profile-m/create-profile-relations conn)
|
||||
(profile-q/strip-private-attrs)))))
|
||||
|
||||
@@ -39,11 +39,23 @@
|
||||
[file index]
|
||||
(letfn [(process-form [form]
|
||||
(cond-> form
|
||||
;; Relink Components
|
||||
;; Relink library items
|
||||
(and (map? form)
|
||||
(uuid? (:component-file form)))
|
||||
(update :component-file #(get index % %))
|
||||
|
||||
(and (map? form)
|
||||
(uuid? (:fill-color-ref-file form)))
|
||||
(update :fill-color-ref-file #(get index % %))
|
||||
|
||||
(and (map? form)
|
||||
(uuid? (:stroke-color-ref-file form)))
|
||||
(update :stroke-color-ref-file #(get index % %))
|
||||
|
||||
(and (map? form)
|
||||
(uuid? (:typography-ref-file form)))
|
||||
(update :typography-ref-file #(get index % %))
|
||||
|
||||
;; Relink Image Shapes
|
||||
(and (map? form)
|
||||
(map? (:metadata form))
|
||||
@@ -167,7 +179,7 @@
|
||||
:opt-un [::name]))
|
||||
|
||||
(sv/defmethod ::duplicate-file
|
||||
[{:keys [pool] :as cfg} {:keys [profile-id file-id name] :as params}]
|
||||
[{:keys [pool] :as cfg} {:keys [profile-id file-id] :as params}]
|
||||
(db/with-atomic [conn pool]
|
||||
(let [file (db/get-by-id conn :file file-id)
|
||||
index {file-id (uuid/next)}
|
||||
@@ -187,7 +199,7 @@
|
||||
:opt-un [::name]))
|
||||
|
||||
(sv/defmethod ::duplicate-project
|
||||
[{:keys [pool] :as cfg} {:keys [profile-id project-id name] :as params}]
|
||||
[{:keys [pool] :as cfg} {:keys [profile-id project-id] :as params}]
|
||||
(db/with-atomic [conn pool]
|
||||
(let [project (db/get-by-id conn :project project-id)]
|
||||
(teams/check-edition-permissions! conn profile-id (:team-id project))
|
||||
|
||||
@@ -10,11 +10,13 @@
|
||||
[app.common.media :as cm]
|
||||
[app.common.spec :as us]
|
||||
[app.common.uuid :as uuid]
|
||||
[app.config :as cf]
|
||||
[app.db :as db]
|
||||
[app.media :as media]
|
||||
[app.rpc.queries.teams :as teams]
|
||||
[app.storage :as sto]
|
||||
[app.util.http :as http]
|
||||
[app.util.rlimit :as rlimit]
|
||||
[app.util.services :as sv]
|
||||
[app.util.time :as dt]
|
||||
[clojure.spec.alpha :as s]
|
||||
@@ -32,7 +34,6 @@
|
||||
(s/def ::file-id ::us/uuid)
|
||||
(s/def ::team-id ::us/uuid)
|
||||
|
||||
|
||||
;; --- Create File Media object (upload)
|
||||
|
||||
(declare create-file-media-object)
|
||||
@@ -48,6 +49,7 @@
|
||||
:opt-un [::id]))
|
||||
|
||||
(sv/defmethod ::upload-file-media-object
|
||||
{::rlimit/permits (cf/get :rlimit-image)}
|
||||
[{:keys [pool] :as cfg} {:keys [profile-id file-id] :as params}]
|
||||
(db/with-atomic [conn pool]
|
||||
(let [file (select-file conn file-id)]
|
||||
@@ -90,22 +92,30 @@
|
||||
:content-type mtype
|
||||
:expired-at (dt/in-future {:minutes 30})}))))
|
||||
|
||||
;; NOTE: we use the `on conflict do update` instead of `do nothing`
|
||||
;; because postgresql does not returns anything if no update is
|
||||
;; performed, the `do update` does the trick.
|
||||
|
||||
(def sql:create-file-media-object
|
||||
"insert into file_media_object (id, file_id, is_local, name, media_id, thumbnail_id, width, height, mtype)
|
||||
values (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
on conflict (id) do update set created_at=file_media_object.created_at
|
||||
returning *")
|
||||
|
||||
(defn create-file-media-object
|
||||
[{:keys [conn storage] :as cfg} {:keys [file-id is-local name content] :as params}]
|
||||
[{:keys [conn storage] :as cfg} {:keys [id file-id is-local name content] :as params}]
|
||||
(media/validate-media-type (:content-type content))
|
||||
(let [storage (assoc storage :conn conn)
|
||||
(let [storage (media/configure-assets-storage storage conn)
|
||||
source-path (fs/path (:tempfile content))
|
||||
source-mtype (:content-type content)
|
||||
|
||||
source-info (media/run cfg {:cmd :info :input {:path source-path :mtype source-mtype}})
|
||||
source-info (media/run {:cmd :info :input {:path source-path :mtype source-mtype}})
|
||||
|
||||
thumb (when (and (not (svg-image? source-info))
|
||||
(big-enough-for-thumbnail? source-info))
|
||||
(media/run cfg (assoc thumbnail-options
|
||||
:cmd :generic-thumbnail
|
||||
:input {:mtype (:mtype source-info)
|
||||
:path source-path})))
|
||||
(media/run (assoc thumbnail-options
|
||||
:cmd :generic-thumbnail
|
||||
:input {:mtype (:mtype source-info)
|
||||
:path source-path})))
|
||||
|
||||
image (if (= (:mtype source-info) "image/svg+xml")
|
||||
(let [data (slurp source-path)]
|
||||
@@ -117,17 +127,15 @@
|
||||
thumb (when thumb
|
||||
(sto/put-object storage {:content (sto/content (:data thumb) (:size thumb))
|
||||
:content-type (:mtype thumb)}))]
|
||||
(db/insert! conn :file-media-object
|
||||
{:id (uuid/next)
|
||||
:file-id file-id
|
||||
:is-local is-local
|
||||
:name name
|
||||
:media-id (:id image)
|
||||
:thumbnail-id (:id thumb)
|
||||
:width (:width source-info)
|
||||
:height (:height source-info)
|
||||
:mtype source-mtype})))
|
||||
|
||||
(db/exec-one! conn [sql:create-file-media-object
|
||||
(or id (uuid/next))
|
||||
file-id is-local name
|
||||
(:id image)
|
||||
(:id thumb)
|
||||
(:width source-info)
|
||||
(:height source-info)
|
||||
source-mtype])))
|
||||
|
||||
;; --- Create File Media Object (from URL)
|
||||
|
||||
|
||||
@@ -9,20 +9,19 @@
|
||||
[app.common.exceptions :as ex]
|
||||
[app.common.spec :as us]
|
||||
[app.common.uuid :as uuid]
|
||||
[app.config :as cfg]
|
||||
[app.config :as cf]
|
||||
[app.db :as db]
|
||||
[app.emails :as eml]
|
||||
[app.http.oauth :refer [extract-props]]
|
||||
[app.http.oauth :refer [extract-utm-props]]
|
||||
[app.loggers.audit :as audit]
|
||||
[app.media :as media]
|
||||
[app.rpc.mutations.projects :as projects]
|
||||
[app.metrics :as mtx]
|
||||
[app.rpc.mutations.teams :as teams]
|
||||
[app.rpc.queries.profile :as profile]
|
||||
[app.setup.initial-data :as sid]
|
||||
[app.storage :as sto]
|
||||
[app.util.rlimit :as rlimit]
|
||||
[app.util.services :as sv]
|
||||
[app.util.time :as dt]
|
||||
[app.worker :as wrk]
|
||||
[buddy.hashers :as hashers]
|
||||
[clojure.spec.alpha :as s]
|
||||
[cuerdas.core :as str]))
|
||||
@@ -37,106 +36,14 @@
|
||||
(s/def ::password ::us/not-empty-string)
|
||||
(s/def ::old-password ::us/not-empty-string)
|
||||
(s/def ::theme ::us/string)
|
||||
|
||||
;; --- Mutation: Register Profile
|
||||
(s/def ::invitation-token ::us/not-empty-string)
|
||||
|
||||
(declare annotate-profile-register)
|
||||
(declare check-profile-existence!)
|
||||
(declare create-profile)
|
||||
(declare create-profile-relations)
|
||||
(declare email-domain-in-whitelist?)
|
||||
(declare register-profile)
|
||||
|
||||
(s/def ::invitation-token ::us/not-empty-string)
|
||||
(s/def ::terms-privacy ::us/boolean)
|
||||
|
||||
(s/def ::register-profile
|
||||
(s/keys :req-un [::email ::password ::fullname ::terms-privacy]
|
||||
:opt-un [::invitation-token]))
|
||||
|
||||
(sv/defmethod ::register-profile {:auth false :rlimit :password}
|
||||
[{:keys [pool tokens session] :as cfg} params]
|
||||
(when-not (cfg/get :registration-enabled)
|
||||
(ex/raise :type :restriction
|
||||
:code :registration-disabled))
|
||||
|
||||
(when-let [domains (cfg/get :registration-domain-whitelist)]
|
||||
(when-not (email-domain-in-whitelist? domains (:email params))
|
||||
(ex/raise :type :validation
|
||||
:code :email-domain-is-not-allowed)))
|
||||
|
||||
(when-not (:terms-privacy params)
|
||||
(ex/raise :type :validation
|
||||
:code :invalid-terms-and-privacy))
|
||||
|
||||
(db/with-atomic [conn pool]
|
||||
(let [cfg (assoc cfg :conn conn)]
|
||||
(register-profile cfg params))))
|
||||
|
||||
(defn- annotate-profile-register
|
||||
"A helper for properly increase the profile-register metric once the
|
||||
transaction is completed."
|
||||
[metrics profile]
|
||||
(fn []
|
||||
(when (::created profile)
|
||||
((get-in metrics [:definitions :profile-register]) :inc))))
|
||||
|
||||
(defn- register-profile
|
||||
[{:keys [conn tokens session metrics] :as cfg} params]
|
||||
(check-profile-existence! conn params)
|
||||
(let [profile (->> (create-profile conn params)
|
||||
(create-profile-relations conn))
|
||||
profile (assoc profile ::created true)]
|
||||
|
||||
(sid/load-initial-project! conn profile)
|
||||
|
||||
(if-let [token (:invitation-token params)]
|
||||
;; If invitation token comes in params, this is because the
|
||||
;; user comes from team-invitation process; in this case,
|
||||
;; regenerate token and send back to the user a new invitation
|
||||
;; token (and mark current session as logged).
|
||||
(let [claims (tokens :verify {:token token :iss :team-invitation})
|
||||
claims (assoc claims
|
||||
:member-id (:id profile)
|
||||
:member-email (:email profile))
|
||||
token (tokens :generate claims)
|
||||
resp {:invitation-token token}]
|
||||
(with-meta resp
|
||||
{:transform-response ((:create session) (:id profile))
|
||||
:before-complete (annotate-profile-register metrics profile)
|
||||
::audit/props (:props profile)
|
||||
::audit/profile-id (:id profile)}))
|
||||
|
||||
;; If no token is provided, send a verification email
|
||||
(let [vtoken (tokens :generate
|
||||
{:iss :verify-email
|
||||
:exp (dt/in-future "48h")
|
||||
:profile-id (:id profile)
|
||||
:email (:email profile)})
|
||||
ptoken (tokens :generate-predefined
|
||||
{:iss :profile-identity
|
||||
:profile-id (:id profile)})]
|
||||
|
||||
;; Don't allow proceed in register page if the email is
|
||||
;; already reported as permanent bounced
|
||||
(when (eml/has-bounce-reports? conn (:email profile))
|
||||
(ex/raise :type :validation
|
||||
:code :email-has-permanent-bounces
|
||||
:hint "looks like the email has one or many bounces reported"))
|
||||
|
||||
(eml/send! {::eml/conn conn
|
||||
::eml/factory eml/register
|
||||
:public-uri (:public-uri cfg)
|
||||
:to (:email profile)
|
||||
:name (:fullname profile)
|
||||
:token vtoken
|
||||
:extra-data ptoken})
|
||||
|
||||
(with-meta profile
|
||||
{:before-complete (annotate-profile-register metrics profile)
|
||||
::audit/props (:props profile)
|
||||
::audit/profile-id (:id profile)})))))
|
||||
|
||||
(defn email-domain-in-whitelist?
|
||||
"Returns true if email's domain is in the given whitelist or if
|
||||
given whitelist is an empty string."
|
||||
@@ -178,28 +85,177 @@
|
||||
{:update false
|
||||
:valid false})))
|
||||
|
||||
(defn decode-profile-row
|
||||
[{:keys [props] :as profile}]
|
||||
(cond-> profile
|
||||
(db/pgobject? props "jsonb")
|
||||
(assoc :props (db/decode-transit-pgobject props))))
|
||||
|
||||
;; --- MUTATION: Prepare Register
|
||||
|
||||
(s/def ::prepare-register-profile
|
||||
(s/keys :req-un [::email ::password]
|
||||
:opt-un [::invitation-token]))
|
||||
|
||||
(sv/defmethod ::prepare-register-profile {:auth false}
|
||||
[{:keys [pool tokens] :as cfg} params]
|
||||
(when-not (contains? cf/flags :registration)
|
||||
(ex/raise :type :restriction
|
||||
:code :registration-disabled))
|
||||
(when-let [domains (cf/get :registration-domain-whitelist)]
|
||||
(when-not (email-domain-in-whitelist? domains (:email params))
|
||||
(ex/raise :type :validation
|
||||
:code :email-domain-is-not-allowed)))
|
||||
|
||||
;; Don't allow proceed in preparing registration if the profile is
|
||||
;; already reported as spamer.
|
||||
(when (eml/has-bounce-reports? pool (:email params))
|
||||
(ex/raise :type :validation
|
||||
:code :email-has-permanent-bounces
|
||||
:hint "looks like the email has one or many bounces reported"))
|
||||
|
||||
(check-profile-existence! pool params)
|
||||
|
||||
(let [params (assoc params
|
||||
:backend "penpot"
|
||||
:iss :prepared-register
|
||||
:exp (dt/in-future "48h"))
|
||||
token (tokens :generate params)]
|
||||
{:token token}))
|
||||
|
||||
;; --- MUTATION: Register Profile
|
||||
|
||||
(s/def ::token ::us/not-empty-string)
|
||||
(s/def ::register-profile
|
||||
(s/keys :req-un [::token ::fullname]))
|
||||
|
||||
(sv/defmethod ::register-profile
|
||||
{:auth false ::rlimit/permits (cf/get :rlimit-password)}
|
||||
[{:keys [pool] :as cfg} params]
|
||||
(db/with-atomic [conn pool]
|
||||
(-> (assoc cfg :conn conn)
|
||||
(register-profile params))))
|
||||
|
||||
(defn- annotate-profile-register
|
||||
"A helper for properly increase the profile-register metric once the
|
||||
transaction is completed."
|
||||
[metrics]
|
||||
(fn []
|
||||
(let [mobj (get-in metrics [:definitions :profile-register])]
|
||||
((::mtx/fn mobj) {:by 1}))))
|
||||
|
||||
(defn register-profile
|
||||
[{:keys [conn tokens session metrics] :as cfg} {:keys [token] :as params}]
|
||||
(let [claims (tokens :verify {:token token :iss :prepared-register})
|
||||
params (merge params claims)]
|
||||
|
||||
(check-profile-existence! conn params)
|
||||
|
||||
(let [is-active (or (:is-active params)
|
||||
(contains? cf/flags :insecure-register))
|
||||
profile (->> (assoc params :is-active is-active)
|
||||
(create-profile conn)
|
||||
(create-profile-relations conn)
|
||||
(decode-profile-row))]
|
||||
(cond
|
||||
;; If invitation token comes in params, this is because the
|
||||
;; user comes from team-invitation process; in this case,
|
||||
;; regenerate token and send back to the user a new invitation
|
||||
;; token (and mark current session as logged).
|
||||
(some? (:invitation-token params))
|
||||
(let [token (:invitation-token params)
|
||||
claims (tokens :verify {:token token :iss :team-invitation})
|
||||
claims (assoc claims
|
||||
:member-id (:id profile)
|
||||
:member-email (:email profile))
|
||||
token (tokens :generate claims)
|
||||
resp {:invitation-token token}]
|
||||
(with-meta resp
|
||||
{:transform-response ((:create session) (:id profile))
|
||||
:before-complete (annotate-profile-register metrics)
|
||||
::audit/props (audit/profile->props profile)
|
||||
::audit/profile-id (:id profile)}))
|
||||
|
||||
;; If auth backend is different from "penpot" means user is
|
||||
;; registring using third party auth mechanism; in this case
|
||||
;; we need to mark this session as logged.
|
||||
(not= "penpot" (:auth-backend profile))
|
||||
(with-meta (profile/strip-private-attrs profile)
|
||||
{:transform-response ((:create session) (:id profile))
|
||||
:before-complete (annotate-profile-register metrics)
|
||||
::audit/props (audit/profile->props profile)
|
||||
::audit/profile-id (:id profile)})
|
||||
|
||||
;; If the `:enable-insecure-register` flag is set, we proceed
|
||||
;; to sign in the user directly, without email verification.
|
||||
(true? is-active)
|
||||
(with-meta (profile/strip-private-attrs profile)
|
||||
{:transform-response ((:create session) (:id profile))
|
||||
:before-complete (annotate-profile-register metrics)
|
||||
::audit/props (audit/profile->props profile)
|
||||
::audit/profile-id (:id profile)})
|
||||
|
||||
;; In all other cases, send a verification email.
|
||||
:else
|
||||
(let [vtoken (tokens :generate
|
||||
{:iss :verify-email
|
||||
:exp (dt/in-future "48h")
|
||||
:profile-id (:id profile)
|
||||
:email (:email profile)})
|
||||
ptoken (tokens :generate-predefined
|
||||
{:iss :profile-identity
|
||||
:profile-id (:id profile)})]
|
||||
(eml/send! {::eml/conn conn
|
||||
::eml/factory eml/register
|
||||
:public-uri (:public-uri cfg)
|
||||
:to (:email profile)
|
||||
:name (:fullname profile)
|
||||
:token vtoken
|
||||
:extra-data ptoken})
|
||||
|
||||
(with-meta profile
|
||||
{:before-complete (annotate-profile-register metrics)
|
||||
::audit/props (audit/profile->props profile)
|
||||
::audit/profile-id (:id profile)}))))))
|
||||
|
||||
(defn create-profile
|
||||
"Create the profile entry on the database with limited input
|
||||
filling all the other fields with defaults."
|
||||
[conn {:keys [id fullname email password is-active is-muted is-demo opts]
|
||||
:or {is-active false is-muted false is-demo false}
|
||||
:as params}]
|
||||
(let [id (or id (uuid/next))
|
||||
is-active (if is-demo true is-active)
|
||||
props (-> params extract-props db/tjson)
|
||||
password (derive-password password)
|
||||
"Create the profile entry on the database with limited input filling
|
||||
all the other fields with defaults."
|
||||
[conn params]
|
||||
(let [id (or (:id params) (uuid/next))
|
||||
|
||||
props (-> (extract-utm-props params)
|
||||
(merge (:props params))
|
||||
(db/tjson))
|
||||
|
||||
password (if-let [password (:password params)]
|
||||
(derive-password password)
|
||||
"!")
|
||||
|
||||
locale (:locale params)
|
||||
locale (when (and (string? locale) (not (str/blank? locale)))
|
||||
locale)
|
||||
|
||||
backend (:backend params "penpot")
|
||||
is-demo (:is-demo params false)
|
||||
is-muted (:is-muted params false)
|
||||
is-active (:is-active params false)
|
||||
email (str/lower (:email params))
|
||||
|
||||
params {:id id
|
||||
:fullname fullname
|
||||
:email (str/lower email)
|
||||
:auth-backend "penpot"
|
||||
:fullname (:fullname params)
|
||||
:email email
|
||||
:auth-backend backend
|
||||
:lang locale
|
||||
:password password
|
||||
:deleted-at (:deleted-at params)
|
||||
:props props
|
||||
:is-active is-active
|
||||
:is-muted is-muted
|
||||
:is-demo is-demo}]
|
||||
(try
|
||||
(-> (db/insert! conn :profile params opts)
|
||||
(update :props db/decode-transit-pgobject))
|
||||
(-> (db/insert! conn :profile params)
|
||||
(decode-profile-row))
|
||||
(catch org.postgresql.util.PSQLException e
|
||||
(let [state (.getSQLState e)]
|
||||
(if (not= state "23505")
|
||||
@@ -208,30 +264,17 @@
|
||||
:code :email-already-exists
|
||||
:cause e)))))))
|
||||
|
||||
|
||||
(defn create-profile-relations
|
||||
[conn profile]
|
||||
(let [team (teams/create-team conn {:profile-id (:id profile)
|
||||
:name "Default"
|
||||
:is-default true})
|
||||
project (projects/create-project conn {:profile-id (:id profile)
|
||||
:team-id (:id team)
|
||||
:name "Drafts"
|
||||
:is-default true})
|
||||
params {:team-id (:id team)
|
||||
:profile-id (:id profile)
|
||||
:project-id (:id project)
|
||||
:role :owner}]
|
||||
|
||||
(teams/create-team-role conn params)
|
||||
(projects/create-project-role conn params)
|
||||
|
||||
(let [team (teams/create-team conn {:profile-id (:id profile)
|
||||
:name "Default"
|
||||
:is-default true})]
|
||||
(-> profile
|
||||
(profile/strip-private-attrs)
|
||||
(assoc :default-team-id (:id team))
|
||||
(assoc :default-project-id (:id project)))))
|
||||
(assoc :default-project-id (:default-project-id team)))))
|
||||
|
||||
;; --- Mutation: Login
|
||||
;; --- MUTATION: Login
|
||||
|
||||
(s/def ::email ::us/email)
|
||||
(s/def ::scope ::us/string)
|
||||
@@ -240,8 +283,9 @@
|
||||
(s/keys :req-un [::email ::password]
|
||||
:opt-un [::scope ::invitation-token]))
|
||||
|
||||
(sv/defmethod ::login {:auth false :rlimit :password}
|
||||
[{:keys [pool session tokens] :as cfg} {:keys [email password scope] :as params}]
|
||||
(sv/defmethod ::login
|
||||
{:auth false ::rlimit/permits (cf/get :rlimit-password)}
|
||||
[{:keys [pool session tokens] :as cfg} {:keys [email password] :as params}]
|
||||
(letfn [(check-password [profile password]
|
||||
(when (= (:password profile) "!")
|
||||
(ex/raise :type :validation
|
||||
@@ -264,7 +308,8 @@
|
||||
(let [profile (->> (profile/retrieve-profile-data-by-email conn email)
|
||||
(validate-profile)
|
||||
(profile/strip-private-attrs)
|
||||
(profile/populate-additional-data conn))]
|
||||
(profile/populate-additional-data conn)
|
||||
(decode-profile-row))]
|
||||
(if-let [token (:invitation-token params)]
|
||||
;; If the request comes with an invitation token, this means
|
||||
;; that user wants to accept it with different user. A very
|
||||
@@ -279,98 +324,36 @@
|
||||
token (tokens :generate claims)]
|
||||
(with-meta {:invitation-token token}
|
||||
{:transform-response ((:create session) (:id profile))
|
||||
::audit/props (audit/profile->props profile)
|
||||
::audit/profile-id (:id profile)}))
|
||||
|
||||
(with-meta profile
|
||||
{:transform-response ((:create session) (:id profile))
|
||||
::audit/props (audit/profile->props profile)
|
||||
::audit/profile-id (:id profile)}))))))
|
||||
|
||||
;; --- Mutation: Logout
|
||||
;; --- MUTATION: Logout
|
||||
|
||||
(s/def ::logout
|
||||
(s/keys :req-un [::profile-id]))
|
||||
|
||||
(sv/defmethod ::logout
|
||||
[{:keys [pool session] :as cfg} {:keys [profile-id] :as params}]
|
||||
[{:keys [session] :as cfg} _]
|
||||
(with-meta {}
|
||||
{:transform-response (:delete session)}))
|
||||
|
||||
|
||||
;; --- Mutation: Register if not exists
|
||||
|
||||
(declare login-or-register)
|
||||
|
||||
(s/def ::backend ::us/string)
|
||||
(s/def ::login-or-register
|
||||
(s/keys :req-un [::email ::fullname ::backend]))
|
||||
|
||||
(sv/defmethod ::login-or-register {:auth false}
|
||||
[{:keys [pool metrics] :as cfg} params]
|
||||
(db/with-atomic [conn pool]
|
||||
(let [profile (-> (assoc cfg :conn conn)
|
||||
(login-or-register params))
|
||||
props (merge
|
||||
(select-keys profile [:backend :fullname :email])
|
||||
(:props profile))]
|
||||
(with-meta profile
|
||||
{:before-complete (annotate-profile-register metrics profile)
|
||||
::audit/name (if (::created profile) "register" "login")
|
||||
::audit/props props
|
||||
::audit/profile-id (:id profile)}))))
|
||||
|
||||
(defn login-or-register
|
||||
[{:keys [conn] :as cfg} {:keys [email] :as params}]
|
||||
(letfn [(info->lang [{:keys [locale] :as info}]
|
||||
(when (and (string? locale)
|
||||
(not (str/blank? locale)))
|
||||
locale))
|
||||
|
||||
(create-profile [conn {:keys [fullname backend email props] :as info}]
|
||||
(let [params {:id (uuid/next)
|
||||
:fullname fullname
|
||||
:email (str/lower email)
|
||||
:lang (info->lang props)
|
||||
:auth-backend backend
|
||||
:is-active true
|
||||
:password "!"
|
||||
:props (db/tjson props)
|
||||
:is-demo false}]
|
||||
(-> (db/insert! conn :profile params)
|
||||
(update :props db/decode-transit-pgobject))))
|
||||
|
||||
(update-profile [conn info profile]
|
||||
(let [props (merge (:props profile)
|
||||
(:props info))]
|
||||
(db/update! conn :profile
|
||||
{:props (db/tjson props)
|
||||
:modified-at (dt/now)}
|
||||
{:id (:id profile)})
|
||||
(assoc profile :props props)))
|
||||
|
||||
(register-profile [conn params]
|
||||
(let [profile (->> (create-profile conn params)
|
||||
(create-profile-relations conn))]
|
||||
(sid/load-initial-project! conn profile)
|
||||
(assoc profile ::created true)))]
|
||||
|
||||
(let [profile (profile/retrieve-profile-data-by-email conn email)
|
||||
profile (if profile
|
||||
(->> profile
|
||||
(update-profile conn params)
|
||||
(profile/populate-additional-data conn))
|
||||
(register-profile conn params))]
|
||||
(profile/strip-private-attrs profile))))
|
||||
|
||||
|
||||
;; --- Mutation: Update Profile (own)
|
||||
;; --- MUTATION: Update Profile (own)
|
||||
|
||||
(defn- update-profile
|
||||
[conn {:keys [id fullname lang theme] :as params}]
|
||||
(db/update! conn :profile
|
||||
{:fullname fullname
|
||||
:lang lang
|
||||
:theme theme}
|
||||
{:id id}))
|
||||
(let [profile (db/update! conn :profile
|
||||
{:fullname fullname
|
||||
:lang lang
|
||||
:theme theme}
|
||||
{:id id})]
|
||||
(-> profile
|
||||
(profile/decode-profile-row)
|
||||
(profile/strip-private-attrs))))
|
||||
|
||||
(s/def ::update-profile
|
||||
(s/keys :req-un [::id ::fullname]
|
||||
@@ -379,10 +362,11 @@
|
||||
(sv/defmethod ::update-profile
|
||||
[{:keys [pool] :as cfg} params]
|
||||
(db/with-atomic [conn pool]
|
||||
(update-profile conn params)
|
||||
nil))
|
||||
(let [profile (update-profile conn params)]
|
||||
(with-meta profile
|
||||
{::audit/props (audit/profile->props profile)}))))
|
||||
|
||||
;; --- Mutation: Update Password
|
||||
;; --- MUTATION: Update Password
|
||||
|
||||
(declare validate-password!)
|
||||
(declare update-profile-password!)
|
||||
@@ -390,8 +374,9 @@
|
||||
(s/def ::update-profile-password
|
||||
(s/keys :req-un [::profile-id ::password ::old-password]))
|
||||
|
||||
(sv/defmethod ::update-profile-password {:rlimit :password}
|
||||
[{:keys [pool] :as cfg} {:keys [password profile-id] :as params}]
|
||||
(sv/defmethod ::update-profile-password
|
||||
{::rlimit/permits (cf/get :rlimit-password)}
|
||||
[{:keys [pool] :as cfg} {:keys [password] :as params}]
|
||||
(db/with-atomic [conn pool]
|
||||
(let [profile (validate-password! conn params)]
|
||||
(update-profile-password! conn (assoc profile :password password))
|
||||
@@ -411,7 +396,8 @@
|
||||
{:password (derive-password password)}
|
||||
{:id id}))
|
||||
|
||||
;; --- Mutation: Update Photo
|
||||
|
||||
;; --- MUTATION: Update Photo
|
||||
|
||||
(declare update-profile-photo)
|
||||
|
||||
@@ -422,14 +408,17 @@
|
||||
(s/keys :req-un [::profile-id ::file]))
|
||||
|
||||
(sv/defmethod ::update-profile-photo
|
||||
{::rlimit/permits (cf/get :rlimit-image)}
|
||||
[{:keys [pool storage] :as cfg} {:keys [profile-id file] :as params}]
|
||||
(db/with-atomic [conn pool]
|
||||
(media/validate-media-type (:content-type file) #{"image/jpeg" "image/png" "image/webp"})
|
||||
(media/run {:cmd :info :input {:path (:tempfile file)
|
||||
:mtype (:content-type file)}})
|
||||
|
||||
(let [profile (db/get-by-id conn :profile profile-id)
|
||||
_ (media/run cfg {:cmd :info :input {:path (:tempfile file)
|
||||
:mtype (:content-type file)}})
|
||||
photo (teams/upload-photo cfg params)
|
||||
storage (assoc storage :conn conn)]
|
||||
storage (media/configure-assets-storage storage conn)
|
||||
cfg (assoc cfg :storage storage)
|
||||
photo (teams/upload-photo cfg params)]
|
||||
|
||||
;; Schedule deletion of old photo
|
||||
(when-let [id (:photo-id profile)]
|
||||
@@ -446,7 +435,7 @@
|
||||
nil)
|
||||
|
||||
|
||||
;; --- Mutation: Request Email Change
|
||||
;; --- MUTATION: Request Email Change
|
||||
|
||||
(declare request-email-change)
|
||||
(declare change-email-inmediatelly)
|
||||
@@ -462,7 +451,8 @@
|
||||
params (assoc params
|
||||
:profile profile
|
||||
:email (str/lower email))]
|
||||
(if (cfg/get :smtp-enabled)
|
||||
(if (or (cf/get :smtp-enabled)
|
||||
(contains? cf/flags :smtp))
|
||||
(request-email-change cfg params)
|
||||
(change-email-inmediatelly cfg params)))))
|
||||
|
||||
@@ -514,7 +504,7 @@
|
||||
[conn id]
|
||||
(db/get-by-id conn :profile id {:for-update true}))
|
||||
|
||||
;; --- Mutation: Request Profile Recovery
|
||||
;; --- MUTATION: Request Profile Recovery
|
||||
|
||||
(s/def ::request-profile-recovery
|
||||
(s/keys :req-un [::email]))
|
||||
@@ -563,13 +553,14 @@
|
||||
(send-email-notification conn))))))
|
||||
|
||||
|
||||
;; --- Mutation: Recover Profile
|
||||
;; --- MUTATION: Recover Profile
|
||||
|
||||
(s/def ::token ::us/not-empty-string)
|
||||
(s/def ::recover-profile
|
||||
(s/keys :req-un [::token ::password]))
|
||||
|
||||
(sv/defmethod ::recover-profile {:auth false :rlimit :password}
|
||||
(sv/defmethod ::recover-profile
|
||||
{:auth false ::rlimit/permits (cf/get :rlimit-password)}
|
||||
[{:keys [pool tokens] :as cfg} {:keys [token password]}]
|
||||
(letfn [(validate-token [token]
|
||||
(let [tdata (tokens :verify {:token token :iss :password-recovery})]
|
||||
@@ -584,7 +575,7 @@
|
||||
(update-password conn))
|
||||
nil)))
|
||||
|
||||
;; --- Mutation: Update Profile Props
|
||||
;; --- MUTATION: Update Profile Props
|
||||
|
||||
(s/def ::props map?)
|
||||
(s/def ::update-profile-props
|
||||
@@ -595,18 +586,22 @@
|
||||
(db/with-atomic [conn pool]
|
||||
(let [profile (profile/retrieve-profile-data conn profile-id)
|
||||
props (reduce-kv (fn [props k v]
|
||||
(if (nil? v)
|
||||
(dissoc props k)
|
||||
(assoc 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})
|
||||
nil)))
|
||||
|
||||
|
||||
;; --- Mutation: Delete Profile
|
||||
;; --- MUTATION: Delete Profile
|
||||
|
||||
(declare check-can-delete-profile!)
|
||||
(declare mark-profile-as-deleted!)
|
||||
@@ -619,12 +614,6 @@
|
||||
(db/with-atomic [conn pool]
|
||||
(check-can-delete-profile! conn profile-id)
|
||||
|
||||
;; Schedule a complete deletion of profile
|
||||
(wrk/submit! {::wrk/task :delete-profile
|
||||
::wrk/delay cfg/deletion-delay
|
||||
::wrk/conn conn
|
||||
:profile-id profile-id})
|
||||
|
||||
(db/update! conn :profile
|
||||
{:deleted-at (dt/now)}
|
||||
{:id profile-id})
|
||||
|
||||
@@ -8,14 +8,12 @@
|
||||
(:require
|
||||
[app.common.spec :as us]
|
||||
[app.common.uuid :as uuid]
|
||||
[app.config :as cfg]
|
||||
[app.db :as db]
|
||||
[app.rpc.permissions :as perms]
|
||||
[app.rpc.queries.projects :as proj]
|
||||
[app.rpc.queries.teams :as teams]
|
||||
[app.util.services :as sv]
|
||||
[app.util.time :as dt]
|
||||
[app.worker :as wrk]
|
||||
[clojure.spec.alpha :as s]))
|
||||
|
||||
;; --- Helpers & Specs
|
||||
@@ -119,19 +117,15 @@
|
||||
(s/def ::delete-project
|
||||
(s/keys :req-un [::id ::profile-id]))
|
||||
|
||||
;; TODO: right now, we just don't allow delete default projects, in a
|
||||
;; future we need to ensure raise a correct exception signaling that
|
||||
;; this is not allowed.
|
||||
|
||||
(sv/defmethod ::delete-project
|
||||
[{:keys [pool] :as cfg} {:keys [id profile-id] :as params}]
|
||||
(db/with-atomic [conn pool]
|
||||
(proj/check-edition-permissions! conn profile-id id)
|
||||
|
||||
;; Schedule object deletion
|
||||
(wrk/submit! {::wrk/task :delete-object
|
||||
::wrk/delay cfg/deletion-delay
|
||||
::wrk/conn conn
|
||||
:id id
|
||||
:type :project})
|
||||
|
||||
(db/update! conn :project
|
||||
{:deleted-at (dt/now)}
|
||||
{:id id})
|
||||
{:id id :is-default false})
|
||||
nil))
|
||||
|
||||
72
backend/src/app/rpc/mutations/share_link.clj
Normal file
72
backend/src/app/rpc/mutations/share_link.clj
Normal file
@@ -0,0 +1,72 @@
|
||||
;; This Source Code Form is subject to the terms of the Mozilla Public
|
||||
;; License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
;;
|
||||
;; Copyright (c) UXBOX Labs SL
|
||||
|
||||
(ns app.rpc.mutations.share-link
|
||||
"Share link related rpc mutation methods."
|
||||
(:require
|
||||
[app.common.spec :as us]
|
||||
[app.common.uuid :as uuid]
|
||||
[app.db :as db]
|
||||
[app.rpc.queries.files :as files]
|
||||
[app.util.services :as sv]
|
||||
[clojure.spec.alpha :as s]))
|
||||
|
||||
;; --- Helpers & Specs
|
||||
|
||||
(s/def ::id ::us/uuid)
|
||||
(s/def ::profile-id ::us/uuid)
|
||||
(s/def ::file-id ::us/uuid)
|
||||
(s/def ::flags (s/every ::us/string :kind set?))
|
||||
(s/def ::pages (s/every ::us/uuid :kind set?))
|
||||
|
||||
;; --- Mutation: Create Share Link
|
||||
|
||||
(declare create-share-link)
|
||||
|
||||
(s/def ::create-share-link
|
||||
(s/keys :req-un [::profile-id ::file-id ::flags]
|
||||
:opt-un [::pages]))
|
||||
|
||||
(sv/defmethod ::create-share-link
|
||||
"Creates a share-link object.
|
||||
|
||||
Share links are resources that allows external users access to
|
||||
specific files with specific permissions (flags)."
|
||||
|
||||
[{:keys [pool] :as cfg} {:keys [profile-id file-id] :as params}]
|
||||
(db/with-atomic [conn pool]
|
||||
(files/check-edition-permissions! conn profile-id file-id)
|
||||
(create-share-link conn params)))
|
||||
|
||||
(defn create-share-link
|
||||
[conn {:keys [profile-id file-id pages flags]}]
|
||||
(let [pages (db/create-array conn "uuid" pages)
|
||||
flags (->> (map name flags)
|
||||
(db/create-array conn "text"))
|
||||
slink (db/insert! conn :share-link
|
||||
{:id (uuid/next)
|
||||
:file-id file-id
|
||||
:flags flags
|
||||
:pages pages
|
||||
:owner-id profile-id})]
|
||||
(-> slink
|
||||
(update :pages db/decode-pgarray #{})
|
||||
(update :flags db/decode-pgarray #{}))))
|
||||
|
||||
;; --- Mutation: Delete Share Link
|
||||
|
||||
(declare delete-share-link)
|
||||
|
||||
(s/def ::delete-share-link
|
||||
(s/keys :req-un [::profile-id ::id]))
|
||||
|
||||
(sv/defmethod ::delete-share-link
|
||||
[{:keys [pool] :as cfg} {:keys [profile-id id] :as params}]
|
||||
(db/with-atomic [conn pool]
|
||||
(let [slink (db/get-by-id conn :share-link id)]
|
||||
(files/check-edition-permissions! conn profile-id (:file-id slink))
|
||||
(db/delete! conn :share-link {:id id})
|
||||
nil)))
|
||||
@@ -10,7 +10,7 @@
|
||||
[app.common.exceptions :as ex]
|
||||
[app.common.spec :as us]
|
||||
[app.common.uuid :as uuid]
|
||||
[app.config :as cfg]
|
||||
[app.config :as cf]
|
||||
[app.db :as db]
|
||||
[app.emails :as eml]
|
||||
[app.media :as media]
|
||||
@@ -19,9 +19,9 @@
|
||||
[app.rpc.queries.profile :as profile]
|
||||
[app.rpc.queries.teams :as teams]
|
||||
[app.storage :as sto]
|
||||
[app.util.rlimit :as rlimit]
|
||||
[app.util.services :as sv]
|
||||
[app.util.time :as dt]
|
||||
[app.worker :as wrk]
|
||||
[clojure.spec.alpha :as s]
|
||||
[datoteka.core :as fs]))
|
||||
|
||||
@@ -34,6 +34,7 @@
|
||||
;; --- Mutation: Create Team
|
||||
|
||||
(declare create-team)
|
||||
(declare create-team-entry)
|
||||
(declare create-team-role)
|
||||
(declare create-team-default-project)
|
||||
|
||||
@@ -44,15 +45,21 @@
|
||||
(sv/defmethod ::create-team
|
||||
[{:keys [pool] :as cfg} params]
|
||||
(db/with-atomic [conn pool]
|
||||
(let [team (create-team conn params)
|
||||
params (assoc params
|
||||
:team-id (:id team)
|
||||
:role :owner)]
|
||||
(create-team-role conn params)
|
||||
(create-team-default-project conn params)
|
||||
team)))
|
||||
(create-team conn params)))
|
||||
|
||||
(defn create-team
|
||||
"This is a complete team creation process, it creates the team
|
||||
object and all related objects (default role and default project)."
|
||||
[conn params]
|
||||
(let [team (create-team-entry conn params)
|
||||
params (assoc params
|
||||
:team-id (:id team)
|
||||
:role :owner)
|
||||
project (create-team-default-project conn params)]
|
||||
(create-team-role conn params)
|
||||
(assoc team :default-project-id (:id project))))
|
||||
|
||||
(defn- create-team-entry
|
||||
[conn {:keys [id name is-default] :as params}]
|
||||
(let [id (or id (uuid/next))
|
||||
is-default (if (boolean? is-default) is-default false)]
|
||||
@@ -61,23 +68,24 @@
|
||||
:name name
|
||||
:is-default is-default})))
|
||||
|
||||
(defn create-team-role
|
||||
(defn- create-team-role
|
||||
[conn {:keys [team-id profile-id role] :as params}]
|
||||
(let [params {:team-id team-id
|
||||
:profile-id profile-id}]
|
||||
(->> (perms/assign-role-flags params role)
|
||||
(db/insert! conn :team-profile-rel))))
|
||||
|
||||
(defn create-team-default-project
|
||||
(defn- create-team-default-project
|
||||
[conn {:keys [team-id profile-id] :as params}]
|
||||
(let [project {:id (uuid/next)
|
||||
:team-id team-id
|
||||
:name "Drafts"
|
||||
:is-default true}]
|
||||
(projects/create-project conn project)
|
||||
:is-default true}
|
||||
project (projects/create-project conn project)]
|
||||
(projects/create-project-role conn {:project-id (:id project)
|
||||
:profile-id profile-id
|
||||
:role :owner})))
|
||||
:role :owner})
|
||||
project))
|
||||
|
||||
;; --- Mutation: Update Team
|
||||
|
||||
@@ -102,10 +110,10 @@
|
||||
(sv/defmethod ::leave-team
|
||||
[{:keys [pool] :as cfg} {:keys [id profile-id] :as params}]
|
||||
(db/with-atomic [conn pool]
|
||||
(let [perms (teams/check-read-permissions! conn profile-id id)
|
||||
(let [perms (teams/get-permissions conn profile-id id)
|
||||
members (teams/retrieve-team-members conn id)]
|
||||
|
||||
(when (some :is-owner perms)
|
||||
(when (:is-owner perms)
|
||||
(ex/raise :type :validation
|
||||
:code :owner-cant-leave-team
|
||||
:hint "reasing owner before leave"))
|
||||
@@ -127,24 +135,21 @@
|
||||
(s/def ::delete-team
|
||||
(s/keys :req-un [::profile-id ::id]))
|
||||
|
||||
;; TODO: right now just don't allow delete default team, in future it
|
||||
;; should raise a speific exception for signal that this acction is
|
||||
;; not allowed.
|
||||
|
||||
(sv/defmethod ::delete-team
|
||||
[{:keys [pool] :as cfg} {:keys [id profile-id] :as params}]
|
||||
(db/with-atomic [conn pool]
|
||||
(let [perms (teams/check-edition-permissions! conn profile-id id)]
|
||||
(when-not (some :is-owner perms)
|
||||
(let [perms (teams/get-permissions conn profile-id id)]
|
||||
(when-not (:is-owner perms)
|
||||
(ex/raise :type :validation
|
||||
:code :only-owner-can-delete-team))
|
||||
|
||||
;; Schedule object deletion
|
||||
(wrk/submit! {::wrk/task :delete-object
|
||||
::wrk/delay cfg/deletion-delay
|
||||
::wrk/conn conn
|
||||
:id id
|
||||
:type :team})
|
||||
|
||||
(db/update! conn :team
|
||||
{:deleted-at (dt/now)}
|
||||
{:id id})
|
||||
{:id id :is-default false})
|
||||
nil)))
|
||||
|
||||
|
||||
@@ -166,8 +171,7 @@
|
||||
(sv/defmethod ::update-team-member-role
|
||||
[{:keys [pool] :as cfg} {:keys [team-id profile-id member-id role] :as params}]
|
||||
(db/with-atomic [conn pool]
|
||||
(let [perms (teams/check-read-permissions! conn profile-id team-id)
|
||||
|
||||
(let [perms (teams/get-permissions conn profile-id team-id)
|
||||
;; We retrieve all team members instead of query the
|
||||
;; database for a single member. This is just for
|
||||
;; convenience, if this bocomes a bottleneck or problematic,
|
||||
@@ -175,8 +179,8 @@
|
||||
members (teams/retrieve-team-members conn team-id)
|
||||
member (d/seek #(= member-id (:id %)) members)
|
||||
|
||||
is-owner? (some :is-owner perms)
|
||||
is-admin? (some :is-admin perms)]
|
||||
is-owner? (:is-owner perms)
|
||||
is-admin? (:is-admin perms)]
|
||||
|
||||
;; If no member is found, just 404
|
||||
(when-not member
|
||||
@@ -229,9 +233,9 @@
|
||||
(sv/defmethod ::delete-team-member
|
||||
[{:keys [pool] :as cfg} {:keys [team-id profile-id member-id] :as params}]
|
||||
(db/with-atomic [conn pool]
|
||||
(let [perms (teams/check-read-permissions! conn profile-id team-id)]
|
||||
(when-not (or (some :is-owner perms)
|
||||
(some :is-admin perms))
|
||||
(let [perms (teams/get-permissions conn profile-id team-id)]
|
||||
(when-not (or (:is-owner perms)
|
||||
(:is-admin perms))
|
||||
(ex/raise :type :validation
|
||||
:code :insufficient-permissions))
|
||||
|
||||
@@ -256,14 +260,17 @@
|
||||
(s/keys :req-un [::profile-id ::team-id ::file]))
|
||||
|
||||
(sv/defmethod ::update-team-photo
|
||||
{::rlimit/permits (cf/get :rlimit-image)}
|
||||
[{:keys [pool storage] :as cfg} {:keys [profile-id file team-id] :as params}]
|
||||
(db/with-atomic [conn pool]
|
||||
(teams/check-edition-permissions! conn profile-id team-id)
|
||||
(media/validate-media-type (:content-type file) #{"image/jpeg" "image/png" "image/webp"})
|
||||
(media/run {:cmd :info :input {:path (:tempfile file)
|
||||
:mtype (:content-type file)}})
|
||||
|
||||
(let [team (teams/retrieve-team conn profile-id team-id)
|
||||
_ (media/run cfg {:cmd :info :input {:path (:tempfile file)
|
||||
:mtype (:content-type file)}})
|
||||
storage (media/configure-assets-storage storage conn)
|
||||
cfg (assoc cfg :storage storage)
|
||||
photo (upload-photo cfg params)]
|
||||
|
||||
;; Schedule deletion of old photo
|
||||
@@ -272,23 +279,20 @@
|
||||
|
||||
;; Save new photo
|
||||
(db/update! conn :team
|
||||
{:photo-id (:id photo)}
|
||||
{:id team-id})
|
||||
{:photo-id (:id photo)}
|
||||
{:id team-id})
|
||||
|
||||
(assoc team :photo-id (:id photo)))))
|
||||
|
||||
(defn upload-photo
|
||||
[{:keys [storage] :as cfg} {:keys [file]}]
|
||||
(let [thumb (media/run cfg
|
||||
{:cmd :profile-thumbnail
|
||||
:format :jpeg
|
||||
:quality 85
|
||||
:width 256
|
||||
:height 256
|
||||
:input {:path (fs/path (:tempfile file))
|
||||
:mtype (:content-type file)}})]
|
||||
|
||||
|
||||
(let [thumb (media/run {:cmd :profile-thumbnail
|
||||
:format :jpeg
|
||||
:quality 85
|
||||
:width 256
|
||||
:height 256
|
||||
:input {:path (fs/path (:tempfile file))
|
||||
:mtype (:content-type file)}})]
|
||||
(sto/put-object storage
|
||||
{:content (sto/content (:data thumb) (:size thumb))
|
||||
:content-type (:mtype thumb)})))
|
||||
@@ -296,30 +300,20 @@
|
||||
|
||||
;; --- Mutation: Invite Member
|
||||
|
||||
(declare create-team-invitation)
|
||||
|
||||
(s/def ::email ::us/email)
|
||||
(s/def ::invite-team-member
|
||||
(s/keys :req-un [::profile-id ::team-id ::email ::role]))
|
||||
|
||||
(sv/defmethod ::invite-team-member
|
||||
[{:keys [pool tokens] :as cfg} {:keys [profile-id team-id email role] :as params}]
|
||||
[{:keys [pool] :as cfg} {:keys [profile-id team-id email role] :as params}]
|
||||
(db/with-atomic [conn pool]
|
||||
(let [perms (teams/check-edition-permissions! conn profile-id team-id)
|
||||
(let [perms (teams/get-permissions conn profile-id team-id)
|
||||
profile (db/get-by-id conn :profile profile-id)
|
||||
member (profile/retrieve-profile-data-by-email conn email)
|
||||
team (db/get-by-id conn :team team-id)
|
||||
itoken (tokens :generate
|
||||
{:iss :team-invitation
|
||||
:exp (dt/in-future "48h")
|
||||
:profile-id (:id profile)
|
||||
:role role
|
||||
:team-id team-id
|
||||
:member-email (:email member email)
|
||||
:member-id (:id member)})
|
||||
ptoken (tokens :generate-predefined
|
||||
{:iss :profile-identity
|
||||
:profile-id (:id profile)})]
|
||||
team (db/get-by-id conn :team team-id)]
|
||||
|
||||
(when-not (some :is-admin perms)
|
||||
(when-not (:is-admin perms)
|
||||
(ex/raise :type :validation
|
||||
:code :insufficient-permissions))
|
||||
|
||||
@@ -329,24 +323,71 @@
|
||||
:code :profile-is-muted
|
||||
:hint "looks like the profile has reported repeatedly as spam or has permanent bounces"))
|
||||
|
||||
(when (and member (not (eml/allow-send-emails? conn member)))
|
||||
(ex/raise :type :validation
|
||||
:code :member-is-muted
|
||||
:hint "looks like the profile has reported repeatedly as spam or has permanent bounces"))
|
||||
|
||||
;; Secondly check if the invited member email is part of the
|
||||
;; global spam/bounce report.
|
||||
(when (eml/has-bounce-reports? conn email)
|
||||
(ex/raise :type :validation
|
||||
:code :email-has-permanent-bounces
|
||||
:hint "looks like the email you invite has been repeatedly reported as spam or permanent bounce"))
|
||||
|
||||
(eml/send! {::eml/conn conn
|
||||
::eml/factory eml/invite-to-team
|
||||
:public-uri (:public-uri cfg)
|
||||
:to email
|
||||
:invited-by (:fullname profile)
|
||||
:team (:name team)
|
||||
:token itoken
|
||||
:extra-data ptoken})
|
||||
(create-team-invitation
|
||||
(assoc cfg
|
||||
:email email
|
||||
:conn conn
|
||||
:team team
|
||||
:profile profile
|
||||
:role role))
|
||||
nil)))
|
||||
|
||||
(defn- create-team-invitation
|
||||
[{:keys [conn tokens team profile role email] :as cfg}]
|
||||
(let [member (profile/retrieve-profile-data-by-email conn email)
|
||||
itoken (tokens :generate
|
||||
{:iss :team-invitation
|
||||
:exp (dt/in-future "48h")
|
||||
:profile-id (:id profile)
|
||||
:role role
|
||||
:team-id (:id team)
|
||||
:member-email (:email member email)
|
||||
:member-id (:id member)})
|
||||
ptoken (tokens :generate-predefined
|
||||
{:iss :profile-identity
|
||||
:profile-id (:id profile)})]
|
||||
|
||||
(when (and member (not (eml/allow-send-emails? conn member)))
|
||||
(ex/raise :type :validation
|
||||
:code :member-is-muted
|
||||
:hint "looks like the profile has reported repeatedly as spam or has permanent bounces"))
|
||||
|
||||
;; Secondly check if the invited member email is part of the
|
||||
;; global spam/bounce report.
|
||||
(when (eml/has-bounce-reports? conn email)
|
||||
(ex/raise :type :validation
|
||||
:code :email-has-permanent-bounces
|
||||
:hint "looks like the email you invite has been repeatedly reported as spam or permanent bounce"))
|
||||
|
||||
(eml/send! {::eml/conn conn
|
||||
::eml/factory eml/invite-to-team
|
||||
:public-uri (:public-uri cfg)
|
||||
:to email
|
||||
:invited-by (:fullname profile)
|
||||
:team (:name team)
|
||||
:token itoken
|
||||
:extra-data ptoken})))
|
||||
|
||||
|
||||
;; --- Mutation: Create Team & Invite Members
|
||||
|
||||
(s/def ::emails ::us/set-of-emails)
|
||||
(s/def ::create-team-and-invite-members
|
||||
(s/and ::create-team (s/keys :req-un [::emails ::role])))
|
||||
|
||||
(sv/defmethod ::create-team-and-invite-members
|
||||
[{:keys [pool] :as cfg} {:keys [profile-id emails role] :as params}]
|
||||
(db/with-atomic [conn pool]
|
||||
(let [team (create-team conn params)
|
||||
profile (db/get-by-id conn :profile profile-id)]
|
||||
|
||||
;; Create invitations for all provided emails.
|
||||
(doseq [email emails]
|
||||
(create-team-invitation
|
||||
(assoc cfg
|
||||
:conn conn
|
||||
:team team
|
||||
:profile profile
|
||||
:email email
|
||||
:role role)))
|
||||
team)))
|
||||
|
||||
@@ -9,6 +9,8 @@
|
||||
[app.common.exceptions :as ex]
|
||||
[app.common.spec :as us]
|
||||
[app.db :as db]
|
||||
[app.loggers.audit :as audit]
|
||||
[app.metrics :as mtx]
|
||||
[app.rpc.mutations.teams :as teams]
|
||||
[app.rpc.queries.profile :as profile]
|
||||
[app.util.services :as sv]
|
||||
@@ -32,17 +34,23 @@
|
||||
(when (profile/retrieve-profile-data-by-email conn email)
|
||||
(ex/raise :type :validation
|
||||
:code :email-already-exists))
|
||||
|
||||
(db/update! conn :profile
|
||||
{:email email}
|
||||
{:id profile-id})
|
||||
claims)
|
||||
|
||||
(with-meta claims
|
||||
{::audit/name "update-profile-email"
|
||||
::audit/props {:email email}
|
||||
::audit/profile-id profile-id}))
|
||||
|
||||
(defn- annotate-profile-activation
|
||||
"A helper for properly increase the profile-activation metric once the
|
||||
transaction is completed."
|
||||
[metrics]
|
||||
(fn []
|
||||
((get-in metrics [:definitions :profile-activation]) :inc)))
|
||||
(let [mobj (get-in metrics [:definitions :profile-activation])]
|
||||
((::mtx/fn mobj) {:by 1}))))
|
||||
|
||||
(defmethod process-token :verify-email
|
||||
[{:keys [conn session metrics] :as cfg} _ {:keys [profile-id] :as claims}]
|
||||
@@ -61,7 +69,10 @@
|
||||
|
||||
(with-meta claims
|
||||
{:transform-response ((:create session) profile-id)
|
||||
:before-complete (annotate-profile-activation metrics)})))
|
||||
:before-complete (annotate-profile-activation metrics)
|
||||
::audit/name "verify-profile-email"
|
||||
::audit/props (audit/profile->props profile)
|
||||
::audit/profile-id (:id profile)})))
|
||||
|
||||
(defmethod process-token :auth
|
||||
[{:keys [conn] :as cfg} _params {:keys [profile-id] :as claims}]
|
||||
@@ -114,8 +125,7 @@
|
||||
;; user is already logged in with some account.
|
||||
(and (uuid? profile-id)
|
||||
(uuid? member-id))
|
||||
(do
|
||||
(accept-invitation cfg claims)
|
||||
(let [profile (accept-invitation cfg claims)]
|
||||
(if (= member-id profile-id)
|
||||
;; If the current session is already matches the invited
|
||||
;; member, then just return the token and leave the frontend
|
||||
@@ -129,27 +139,44 @@
|
||||
;; account.
|
||||
(with-meta
|
||||
(assoc claims :state :created)
|
||||
{:transform-response ((:create session) member-id)})))
|
||||
{:transform-response ((:create session) member-id)
|
||||
::audit/name "accept-team-invitation"
|
||||
::audit/props (merge
|
||||
(audit/profile->props profile)
|
||||
{:team-id (:team-id claims)
|
||||
:role (:role claims)})
|
||||
::audit/profile-id profile-id})))
|
||||
|
||||
;; This happens when member-id is not filled in the invitation but
|
||||
;; the user already has an account (probably with other mail) and
|
||||
;; is already logged-in.
|
||||
(and (uuid? profile-id)
|
||||
(nil? member-id))
|
||||
(do
|
||||
(accept-invitation cfg (assoc claims :member-id profile-id))
|
||||
(assoc claims :state :created))
|
||||
(let [profile (accept-invitation cfg (assoc claims :member-id profile-id))]
|
||||
(with-meta
|
||||
(assoc claims :state :created)
|
||||
{::audit/name "accept-team-invitation"
|
||||
::audit/props (merge
|
||||
(audit/profile->props profile)
|
||||
{:team-id (:team-id claims)
|
||||
:role (:role claims)})
|
||||
::audit/profile-id profile-id}))
|
||||
|
||||
;; This happens when member-id is filled but the accessing user is
|
||||
;; not logged-in. In this case we proceed to accept invitation and
|
||||
;; leave the user logged-in.
|
||||
(and (nil? profile-id)
|
||||
(uuid? member-id))
|
||||
(do
|
||||
(accept-invitation cfg claims)
|
||||
(let [profile (accept-invitation cfg claims)]
|
||||
(with-meta
|
||||
(assoc claims :state :created)
|
||||
{:transform-response ((:create session) member-id)}))
|
||||
{:transform-response ((:create session) member-id)
|
||||
::audit/name "accept-team-invitation"
|
||||
::audit/props (merge
|
||||
(audit/profile->props profile)
|
||||
{:team-id (:team-id claims)
|
||||
:role (:role claims)})
|
||||
::audit/profile-id member-id}))
|
||||
|
||||
;; In this case, we wait until frontend app redirect user to
|
||||
;; registeration page, the user is correctly registered and the
|
||||
|
||||
@@ -1,49 +0,0 @@
|
||||
;; This Source Code Form is subject to the terms of the Mozilla Public
|
||||
;; License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
;;
|
||||
;; Copyright (c) UXBOX Labs SL
|
||||
|
||||
(ns app.rpc.mutations.viewer
|
||||
(:require
|
||||
[app.common.spec :as us]
|
||||
[app.db :as db]
|
||||
[app.rpc.queries.files :as files]
|
||||
[app.util.services :as sv]
|
||||
[buddy.core.codecs :as bc]
|
||||
[buddy.core.nonce :as bn]
|
||||
[clojure.spec.alpha :as s]))
|
||||
|
||||
(s/def ::profile-id ::us/uuid)
|
||||
(s/def ::file-id ::us/uuid)
|
||||
(s/def ::page-id ::us/uuid)
|
||||
|
||||
(s/def ::create-file-share-token
|
||||
(s/keys :req-un [::profile-id ::file-id ::page-id]))
|
||||
|
||||
(sv/defmethod ::create-file-share-token
|
||||
[{:keys [pool] :as cfg} {:keys [profile-id file-id page-id] :as params}]
|
||||
(db/with-atomic [conn pool]
|
||||
(files/check-edition-permissions! conn profile-id file-id)
|
||||
(let [token (-> (bn/random-bytes 16)
|
||||
(bc/bytes->b64u)
|
||||
(bc/bytes->str))]
|
||||
(db/insert! conn :file-share-token
|
||||
{:file-id file-id
|
||||
:page-id page-id
|
||||
:token token})
|
||||
{:token token})))
|
||||
|
||||
|
||||
(s/def ::token ::us/not-empty-string)
|
||||
(s/def ::delete-file-share-token
|
||||
(s/keys :req-un [::profile-id ::file-id ::token]))
|
||||
|
||||
(sv/defmethod ::delete-file-share-token
|
||||
[{:keys [pool] :as cfg} {:keys [profile-id file-id token]}]
|
||||
(db/with-atomic [conn pool]
|
||||
(files/check-edition-permissions! conn profile-id file-id)
|
||||
(db/delete! conn :file-share-token
|
||||
{:file-id file-id
|
||||
:token token})
|
||||
nil))
|
||||
@@ -37,28 +37,28 @@
|
||||
:is-admin false
|
||||
:can-edit false)))
|
||||
|
||||
(defn make-edition-check-fn
|
||||
"A simple factory for edition permission check functions."
|
||||
(defn make-edition-predicate-fn
|
||||
"A simple factory for edition permission predicate functions."
|
||||
[qfn]
|
||||
(us/assert fn? qfn)
|
||||
(fn [& args]
|
||||
(let [rows (apply qfn args)]
|
||||
(if (or (empty? rows)
|
||||
(not (or (some :can-edit rows)
|
||||
(some :is-admin rows)
|
||||
(some :is-owner rows))))
|
||||
(ex/raise :type :not-found
|
||||
:code :object-not-found
|
||||
:hint "not found")
|
||||
rows))))
|
||||
(fn check
|
||||
([perms] (:can-edit perms))
|
||||
([conn & args] (check (apply qfn conn args)))))
|
||||
|
||||
(defn make-read-check-fn
|
||||
"A simple factory for read permission check functions."
|
||||
(defn make-read-predicate-fn
|
||||
"A simple factory for read permission predicate functions."
|
||||
[qfn]
|
||||
(us/assert fn? qfn)
|
||||
(fn check
|
||||
([perms] (:can-read perms))
|
||||
([conn & args] (check (apply qfn conn args)))))
|
||||
|
||||
(defn make-check-fn
|
||||
"Helper that converts a predicate permission function to a check
|
||||
function (function that raises an exception)."
|
||||
[pred]
|
||||
(fn [& args]
|
||||
(let [rows (apply qfn args)]
|
||||
(if-not (seq rows)
|
||||
(ex/raise :type :not-found
|
||||
:code :object-not-found)
|
||||
rows))))
|
||||
(when-not (apply pred args)
|
||||
(ex/raise :type :not-found
|
||||
:code :object-not-found
|
||||
:hint "not found"))))
|
||||
|
||||
@@ -12,7 +12,9 @@
|
||||
[app.db :as db]
|
||||
[app.rpc.permissions :as perms]
|
||||
[app.rpc.queries.projects :as projects]
|
||||
[app.rpc.queries.share-link :refer [retrieve-share-link]]
|
||||
[app.rpc.queries.teams :as teams]
|
||||
[app.storage.impl :as simpl]
|
||||
[app.util.blob :as blob]
|
||||
[app.util.services :as sv]
|
||||
[clojure.spec.alpha :as s]))
|
||||
@@ -58,19 +60,51 @@
|
||||
where f.id = ?
|
||||
and ppr.profile_id = ?")
|
||||
|
||||
(defn- retrieve-file-permissions
|
||||
(defn retrieve-file-permissions
|
||||
[conn profile-id file-id]
|
||||
(db/exec! conn [sql:file-permissions
|
||||
file-id profile-id
|
||||
file-id profile-id
|
||||
file-id profile-id]))
|
||||
(when (and profile-id file-id)
|
||||
(db/exec! conn [sql:file-permissions
|
||||
file-id profile-id
|
||||
file-id profile-id
|
||||
file-id profile-id])))
|
||||
|
||||
(defn get-permissions
|
||||
([conn profile-id file-id]
|
||||
(let [rows (retrieve-file-permissions conn profile-id file-id)
|
||||
is-owner (boolean (some :is-owner rows))
|
||||
is-admin (boolean (some :is-admin rows))
|
||||
can-edit (boolean (some :can-edit rows))]
|
||||
(when (seq rows)
|
||||
{:type :membership
|
||||
:is-owner is-owner
|
||||
:is-admin (or is-owner is-admin)
|
||||
:can-edit (or is-owner is-admin can-edit)
|
||||
:can-read true})))
|
||||
([conn profile-id file-id share-id]
|
||||
(let [perms (get-permissions conn profile-id file-id)
|
||||
ldata (retrieve-share-link conn file-id share-id)]
|
||||
|
||||
;; NOTE: in a future when share-link becomes more powerfull and
|
||||
;; will allow us specify which parts of the app is availabel, we
|
||||
;; will probably need to tweak this function in order to expose
|
||||
;; this flags to the frontend.
|
||||
(cond
|
||||
(some? perms) perms
|
||||
(some? ldata) {:type :share-link
|
||||
:can-read true
|
||||
:flags (:flags ldata)}))))
|
||||
|
||||
(def has-edit-permissions?
|
||||
(perms/make-edition-predicate-fn get-permissions))
|
||||
|
||||
(def has-read-permissions?
|
||||
(perms/make-read-predicate-fn get-permissions))
|
||||
|
||||
(def check-edition-permissions!
|
||||
(perms/make-edition-check-fn retrieve-file-permissions))
|
||||
(perms/make-check-fn has-edit-permissions?))
|
||||
|
||||
(def check-read-permissions!
|
||||
(perms/make-read-check-fn retrieve-file-permissions))
|
||||
|
||||
(perms/make-check-fn has-read-permissions?))
|
||||
|
||||
;; --- Query: Files search
|
||||
|
||||
@@ -112,37 +146,16 @@
|
||||
order by f.created_at asc")
|
||||
|
||||
(s/def ::search-files
|
||||
(s/keys :req-un [::profile-id ::team-id ::search-term]))
|
||||
(s/keys :req-un [::profile-id ::team-id]
|
||||
:opt-un [::search-term]))
|
||||
|
||||
(sv/defmethod ::search-files
|
||||
[{:keys [pool] :as cfg} {:keys [profile-id team-id search-term] :as params}]
|
||||
(db/exec! pool [sql:search-files
|
||||
profile-id team-id
|
||||
profile-id team-id
|
||||
search-term]))
|
||||
|
||||
|
||||
;; --- Query: Files
|
||||
|
||||
;; DEPRECATED: should be removed probably on 1.6.x
|
||||
|
||||
(def ^:private sql:files
|
||||
"select f.*
|
||||
from file as f
|
||||
where f.project_id = ?
|
||||
and f.deleted_at is null
|
||||
order by f.modified_at desc")
|
||||
|
||||
(s/def ::project-id ::us/uuid)
|
||||
(s/def ::files
|
||||
(s/keys :req-un [::profile-id ::project-id]))
|
||||
|
||||
(sv/defmethod ::files
|
||||
[{:keys [pool] :as cfg} {:keys [profile-id project-id] :as params}]
|
||||
(with-open [conn (db/open pool)]
|
||||
(projects/check-read-permissions! conn profile-id project-id)
|
||||
(into [] decode-row-xf (db/exec! conn [sql:files project-id]))))
|
||||
|
||||
(when search-term
|
||||
(db/exec! pool [sql:search-files
|
||||
profile-id team-id
|
||||
profile-id team-id
|
||||
search-term])))
|
||||
|
||||
;; --- Query: Project Files
|
||||
|
||||
@@ -169,20 +182,37 @@
|
||||
|
||||
;; --- Query: File (By ID)
|
||||
|
||||
(defn- retrieve-data*
|
||||
[{:keys [storage] :as cfg} file]
|
||||
(when-let [backend (simpl/resolve-backend storage (:data-backend file))]
|
||||
(simpl/get-object-bytes backend file)))
|
||||
|
||||
(defn retrieve-data
|
||||
[cfg file]
|
||||
(if (bytes? (:data file))
|
||||
file
|
||||
(assoc file :data (retrieve-data* cfg file))))
|
||||
|
||||
(defn retrieve-file
|
||||
[conn id]
|
||||
(-> (db/get-by-id conn :file id)
|
||||
(decode-row)
|
||||
(pmg/migrate-file)))
|
||||
[{:keys [conn] :as cfg} id]
|
||||
(->> (db/get-by-id conn :file id)
|
||||
(retrieve-data cfg)
|
||||
(decode-row)
|
||||
(pmg/migrate-file)))
|
||||
|
||||
(s/def ::file
|
||||
(s/keys :req-un [::profile-id ::id]))
|
||||
|
||||
(sv/defmethod ::file
|
||||
"Retrieve a file by its ID. Only authenticated users."
|
||||
[{:keys [pool] :as cfg} {:keys [profile-id id] :as params}]
|
||||
(db/with-atomic [conn pool]
|
||||
(check-edition-permissions! conn profile-id id)
|
||||
(retrieve-file conn id)))
|
||||
(let [cfg (assoc cfg :conn conn)
|
||||
perms (get-permissions conn profile-id id)]
|
||||
|
||||
(check-read-permissions! perms)
|
||||
(some-> (retrieve-file cfg id)
|
||||
(assoc :permissions perms)))))
|
||||
|
||||
(s/def ::page
|
||||
(s/keys :req-un [::profile-id ::file-id]))
|
||||
@@ -215,11 +245,12 @@
|
||||
(update data :objects update-objects)))
|
||||
|
||||
(sv/defmethod ::page
|
||||
[{:keys [pool] :as cfg} {:keys [profile-id file-id id strip-thumbnails]}]
|
||||
[{:keys [pool] :as cfg} {:keys [profile-id file-id]}]
|
||||
[{:keys [pool] :as cfg} {:keys [profile-id file-id strip-thumbnails]}]
|
||||
(db/with-atomic [conn pool]
|
||||
(check-edition-permissions! conn profile-id file-id)
|
||||
(let [file (retrieve-file conn file-id)
|
||||
(check-read-permissions! conn profile-id file-id)
|
||||
|
||||
(let [cfg (assoc cfg :conn conn)
|
||||
file (retrieve-file cfg file-id)
|
||||
page-id (get-in file [:data :pages 0])]
|
||||
(cond-> (get-in file [:data :pages-index page-id])
|
||||
strip-thumbnails
|
||||
@@ -227,28 +258,6 @@
|
||||
|
||||
;; --- Query: Shared Library Files
|
||||
|
||||
;; DEPRECATED: and will be removed on 1.6.x
|
||||
|
||||
(def ^:private sql:shared-files
|
||||
"select f.*
|
||||
from file as f
|
||||
inner join project as p on (p.id = f.project_id)
|
||||
where f.is_shared = true
|
||||
and f.deleted_at is null
|
||||
and p.deleted_at is null
|
||||
and p.team_id = ?
|
||||
order by f.modified_at desc")
|
||||
|
||||
(s/def ::shared-files
|
||||
(s/keys :req-un [::profile-id ::team-id]))
|
||||
|
||||
(sv/defmethod ::shared-files
|
||||
[{:keys [pool] :as cfg} {:keys [profile-id team-id] :as params}]
|
||||
(into [] decode-row-xf (db/exec! pool [sql:shared-files team-id])))
|
||||
|
||||
|
||||
;; --- Query: Shared Library Files
|
||||
|
||||
(def ^:private sql:team-shared-files
|
||||
"select f.id,
|
||||
f.project_id,
|
||||
@@ -268,30 +277,43 @@
|
||||
(s/keys :req-un [::profile-id ::team-id]))
|
||||
|
||||
(sv/defmethod ::team-shared-files
|
||||
[{:keys [pool] :as cfg} {:keys [profile-id team-id] :as params}]
|
||||
[{:keys [pool] :as cfg} {:keys [team-id] :as params}]
|
||||
(db/exec! pool [sql:team-shared-files team-id]))
|
||||
|
||||
|
||||
;; --- Query: File Libraries used by a File
|
||||
|
||||
(def ^:private sql:file-libraries
|
||||
"select fl.*,
|
||||
|
||||
flr.synced_at as synced_at
|
||||
from file as fl
|
||||
inner join file_library_rel as flr on (flr.library_file_id = fl.id)
|
||||
where flr.file_id = ?
|
||||
and fl.deleted_at is null")
|
||||
"WITH RECURSIVE libs AS (
|
||||
SELECT fl.*, flr.synced_at
|
||||
FROM file AS fl
|
||||
JOIN file_library_rel AS flr ON (flr.library_file_id = fl.id)
|
||||
WHERE flr.file_id = ?::uuid
|
||||
UNION
|
||||
SELECT fl.*, flr.synced_at
|
||||
FROM file AS fl
|
||||
JOIN file_library_rel AS flr ON (flr.library_file_id = fl.id)
|
||||
JOIN libs AS l ON (flr.file_id = l.id)
|
||||
)
|
||||
SELECT l.id,
|
||||
l.data,
|
||||
l.project_id,
|
||||
l.created_at,
|
||||
l.modified_at,
|
||||
l.deleted_at,
|
||||
l.name,
|
||||
l.revn,
|
||||
l.synced_at
|
||||
FROM libs AS l
|
||||
WHERE l.deleted_at IS NULL OR l.deleted_at > now();")
|
||||
|
||||
(defn retrieve-file-libraries
|
||||
[conn is-indirect file-id]
|
||||
(let [libraries (->> (db/exec! conn [sql:file-libraries file-id])
|
||||
(map #(assoc % :is-indirect is-indirect))
|
||||
(into #{} decode-row-xf))]
|
||||
(reduce #(into %1 (retrieve-file-libraries conn true %2))
|
||||
libraries
|
||||
(map :id libraries))))
|
||||
|
||||
[{:keys [conn] :as cfg} is-indirect file-id]
|
||||
(let [xform (comp
|
||||
(map #(assoc % :is-indirect is-indirect))
|
||||
(map #(retrieve-data cfg %))
|
||||
(map decode-row))]
|
||||
(into #{} xform (db/exec! conn [sql:file-libraries file-id]))))
|
||||
|
||||
(s/def ::file-libraries
|
||||
(s/keys :req-un [::profile-id ::file-id]))
|
||||
@@ -299,8 +321,9 @@
|
||||
(sv/defmethod ::file-libraries
|
||||
[{:keys [pool] :as cfg} {:keys [profile-id file-id] :as params}]
|
||||
(db/with-atomic [conn pool]
|
||||
(check-edition-permissions! conn profile-id file-id)
|
||||
(retrieve-file-libraries conn false file-id)))
|
||||
(let [cfg (assoc cfg :conn conn)]
|
||||
(check-read-permissions! conn profile-id file-id)
|
||||
(retrieve-file-libraries cfg false file-id))))
|
||||
|
||||
;; --- QUERY: team-recent-files
|
||||
|
||||
@@ -332,7 +355,6 @@
|
||||
(teams/check-read-permissions! conn profile-id team-id)
|
||||
(db/exec! conn [sql:team-recent-files team-id])))
|
||||
|
||||
|
||||
;; --- Helpers
|
||||
|
||||
(defn decode-row
|
||||
|
||||
@@ -8,12 +8,16 @@
|
||||
(:require
|
||||
[app.common.spec :as us]
|
||||
[app.db :as db]
|
||||
[app.rpc.queries.files :as files]
|
||||
[app.rpc.queries.projects :as projects]
|
||||
[app.rpc.queries.teams :as teams]
|
||||
[app.util.services :as sv]
|
||||
[clojure.spec.alpha :as s]))
|
||||
|
||||
;; --- Query: Team Font Variants
|
||||
|
||||
;; TODO: deprecated, should be removed on 1.7.x
|
||||
|
||||
(s/def ::team-id ::us/uuid)
|
||||
(s/def ::profile-id ::us/uuid)
|
||||
(s/def ::team-font-variants
|
||||
@@ -27,3 +31,43 @@
|
||||
{:team-id team-id
|
||||
:deleted-at nil})))
|
||||
|
||||
;; --- Query: Font Variants
|
||||
|
||||
(s/def ::file-id ::us/uuid)
|
||||
(s/def ::project-id ::us/uuid)
|
||||
(s/def ::font-variants
|
||||
(s/and
|
||||
(s/keys :req-un [::profile-id]
|
||||
:opt-un [::team-id
|
||||
::file-id
|
||||
::project-id])
|
||||
(fn [o]
|
||||
(or (contains? o :team-id)
|
||||
(contains? o :file-id)
|
||||
(contains? o :project-id)))))
|
||||
|
||||
(sv/defmethod ::font-variants
|
||||
[{:keys [pool] :as cfg} {:keys [profile-id team-id file-id project-id] :as params}]
|
||||
(with-open [conn (db/open pool)]
|
||||
(cond
|
||||
(uuid? team-id)
|
||||
(do
|
||||
(teams/check-read-permissions! conn profile-id team-id)
|
||||
(db/query conn :team-font-variant
|
||||
{:team-id team-id
|
||||
:deleted-at nil}))
|
||||
|
||||
(uuid? project-id)
|
||||
(let [project (db/get-by-id conn :project project-id {:columns [:id :team-id]})]
|
||||
(projects/check-read-permissions! conn profile-id project-id)
|
||||
(db/query conn :team-font-variant
|
||||
{:team-id (:team-id project)
|
||||
:deleted-at nil}))
|
||||
|
||||
(uuid? file-id)
|
||||
(let [file (db/get-by-id conn :file file-id {:columns [:id :project-id]})
|
||||
project (db/get-by-id conn :project (:project-id file) {:columns [:id :team-id]})]
|
||||
(files/check-read-permissions! conn profile-id file-id)
|
||||
(db/query conn :team-font-variant
|
||||
{:team-id (:team-id project)
|
||||
:deleted-at nil})))))
|
||||
|
||||
@@ -11,7 +11,8 @@
|
||||
[app.common.uuid :as uuid]
|
||||
[app.db :as db]
|
||||
[app.util.services :as sv]
|
||||
[clojure.spec.alpha :as s]))
|
||||
[clojure.spec.alpha :as s]
|
||||
[cuerdas.core :as str]))
|
||||
|
||||
;; --- Helpers & Specs
|
||||
|
||||
@@ -69,10 +70,15 @@
|
||||
[conn profile]
|
||||
(merge profile (retrieve-additional-data conn (:id profile))))
|
||||
|
||||
(defn- filter-profile-props
|
||||
[props]
|
||||
(into {} (filter (fn [[k _]] (simple-ident? k))) props))
|
||||
|
||||
(defn decode-profile-row
|
||||
[{:keys [props] :as row}]
|
||||
(cond-> row
|
||||
(db/pgobject? props) (assoc :props (db/decode-transit-pgobject props))))
|
||||
(db/pgobject? props "jsonb")
|
||||
(assoc :props (db/decode-transit-pgobject props))))
|
||||
|
||||
(defn retrieve-profile-data
|
||||
[conn id]
|
||||
@@ -81,25 +87,21 @@
|
||||
|
||||
(defn retrieve-profile
|
||||
[conn id]
|
||||
(let [profile (some->> (retrieve-profile-data conn id)
|
||||
(strip-private-attrs)
|
||||
(populate-additional-data conn))]
|
||||
(when (nil? profile)
|
||||
(ex/raise :type :not-found
|
||||
:hint "Object doest not exists."))
|
||||
(let [profile (->> (retrieve-profile-data conn id)
|
||||
(strip-private-attrs)
|
||||
(populate-additional-data conn))]
|
||||
(update profile :props filter-profile-props)))
|
||||
|
||||
profile))
|
||||
|
||||
(def sql:retrieve-profile-by-email
|
||||
(def ^:private sql:profile-by-email
|
||||
"select p.* from profile as p
|
||||
where p.email = lower(?)
|
||||
and p.deleted_at is null")
|
||||
where p.email = ?
|
||||
and (p.deleted_at is null or
|
||||
p.deleted_at > now())")
|
||||
|
||||
(defn retrieve-profile-data-by-email
|
||||
[conn email]
|
||||
(let [sql [sql:retrieve-profile-by-email email]]
|
||||
(some-> (db/exec-one! conn sql)
|
||||
(decode-profile-row))))
|
||||
(ex/ignoring
|
||||
(db/exec-one! conn [sql:profile-by-email (str/lower email)])))
|
||||
|
||||
;; --- Attrs Helpers
|
||||
|
||||
|
||||
@@ -31,18 +31,31 @@
|
||||
where ppr.project_id = ?
|
||||
and ppr.profile_id = ?")
|
||||
|
||||
(defn- retrieve-project-permissions
|
||||
(defn- get-permissions
|
||||
[conn profile-id project-id]
|
||||
(db/exec! conn [sql:project-permissions
|
||||
project-id profile-id
|
||||
project-id profile-id]))
|
||||
(let [rows (db/exec! conn [sql:project-permissions
|
||||
project-id profile-id
|
||||
project-id profile-id])
|
||||
is-owner (boolean (some :is-owner rows))
|
||||
is-admin (boolean (some :is-admin rows))
|
||||
can-edit (boolean (some :can-edit rows))]
|
||||
(when (seq rows)
|
||||
{:is-owner is-owner
|
||||
:is-admin (or is-owner is-admin)
|
||||
:can-edit (or is-owner is-admin can-edit)
|
||||
:can-read true})))
|
||||
|
||||
(def has-edit-permissions?
|
||||
(perms/make-edition-predicate-fn get-permissions))
|
||||
|
||||
(def has-read-permissions?
|
||||
(perms/make-read-predicate-fn get-permissions))
|
||||
|
||||
(def check-edition-permissions!
|
||||
(perms/make-edition-check-fn retrieve-project-permissions))
|
||||
(perms/make-check-fn has-edit-permissions?))
|
||||
|
||||
(def check-read-permissions!
|
||||
(perms/make-read-check-fn retrieve-project-permissions))
|
||||
|
||||
(perms/make-check-fn has-read-permissions?))
|
||||
|
||||
;; --- Query: Projects
|
||||
|
||||
@@ -66,12 +79,14 @@
|
||||
where f.project_id = p.id
|
||||
and deleted_at is null) as count
|
||||
from project as p
|
||||
inner join team as t on (t.id = p.team_id)
|
||||
left join team_project_profile_rel as tpp
|
||||
on (tpp.project_id = p.id and
|
||||
tpp.team_id = p.team_id and
|
||||
tpp.profile_id = ?)
|
||||
where p.team_id = ?
|
||||
and p.deleted_at is null
|
||||
and t.deleted_at is null
|
||||
order by p.modified_at desc")
|
||||
|
||||
(defn retrieve-projects
|
||||
@@ -95,26 +110,26 @@
|
||||
(def sql:all-projects
|
||||
"select p1.*, t.name as team_name, t.is_default as is_default_team
|
||||
from project as p1
|
||||
inner join team as t
|
||||
on t.id = p1.team_id
|
||||
inner join team as t on (t.id = p1.team_id)
|
||||
where t.id in (select team_id
|
||||
from team_profile_rel as tpr
|
||||
where tpr.profile_id = ?
|
||||
and (tpr.can_edit = true or
|
||||
tpr.is_owner = true or
|
||||
tpr.is_admin = true))
|
||||
and t.deleted_at is null
|
||||
and p1.deleted_at is null
|
||||
union
|
||||
select p2.*, t.name as team_name, t.is_default as is_default_team
|
||||
from project as p2
|
||||
inner join team as t
|
||||
on t.id = p2.team_id
|
||||
inner join team as t on (t.id = p2.team_id)
|
||||
where p2.id in (select project_id
|
||||
from project_profile_rel as ppr
|
||||
where ppr.profile_id = ?
|
||||
and (ppr.can_edit = true or
|
||||
ppr.is_owner = true or
|
||||
ppr.is_admin = true))
|
||||
and t.deleted_at is null
|
||||
and p2.deleted_at is null
|
||||
order by team_name, name;")
|
||||
|
||||
|
||||
@@ -1,42 +0,0 @@
|
||||
;; This Source Code Form is subject to the terms of the Mozilla Public
|
||||
;; License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
;;
|
||||
;; Copyright (c) UXBOX Labs SL
|
||||
|
||||
(ns app.rpc.queries.recent-files
|
||||
(:require
|
||||
[app.common.spec :as us]
|
||||
[app.db :as db]
|
||||
[app.rpc.queries.files :refer [decode-row-xf]]
|
||||
[app.rpc.queries.teams :as teams]
|
||||
[app.util.services :as sv]
|
||||
[clojure.spec.alpha :as s]))
|
||||
|
||||
;; DEPRECATED: should be removed on 1.6.x
|
||||
|
||||
(def sql:recent-files
|
||||
"with recent_files as (
|
||||
select f.*, row_number() over w as row_num
|
||||
from file as f
|
||||
join project as p on (p.id = f.project_id)
|
||||
where p.team_id = ?
|
||||
and p.deleted_at is null
|
||||
and f.deleted_at is null
|
||||
window w as (partition by f.project_id order by f.modified_at desc)
|
||||
order by f.modified_at desc
|
||||
)
|
||||
select * from recent_files where row_num <= 10;")
|
||||
|
||||
(s/def ::team-id ::us/uuid)
|
||||
(s/def ::profile-id ::us/uuid)
|
||||
|
||||
(s/def ::recent-files
|
||||
(s/keys :req-un [::profile-id ::team-id]))
|
||||
|
||||
(sv/defmethod ::recent-files
|
||||
[{:keys [pool] :as cfg} {:keys [profile-id team-id]}]
|
||||
(with-open [conn (db/open pool)]
|
||||
(teams/check-read-permissions! conn profile-id team-id)
|
||||
(let [files (db/exec! conn [sql:recent-files team-id])]
|
||||
(into [] decode-row-xf files))))
|
||||
23
backend/src/app/rpc/queries/share_link.clj
Normal file
23
backend/src/app/rpc/queries/share_link.clj
Normal file
@@ -0,0 +1,23 @@
|
||||
;; This Source Code Form is subject to the terms of the Mozilla Public
|
||||
;; License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
;;
|
||||
;; Copyright (c) UXBOX Labs SL
|
||||
|
||||
(ns app.rpc.queries.share-link
|
||||
(:require
|
||||
[app.db :as db]))
|
||||
|
||||
(defn decode-share-link-row
|
||||
[row]
|
||||
(-> row
|
||||
(update :flags db/decode-pgarray #{})
|
||||
(update :pages db/decode-pgarray #{})))
|
||||
|
||||
(defn retrieve-share-link
|
||||
[conn file-id share-id]
|
||||
(some-> (db/get-by-params conn :share-link
|
||||
{:id share-id :file-id file-id}
|
||||
{:check-not-found false})
|
||||
(decode-share-link-row)))
|
||||
|
||||
@@ -24,16 +24,29 @@
|
||||
where tpr.profile_id = ?
|
||||
and tpr.team_id = ?")
|
||||
|
||||
(defn- retrieve-team-permissions
|
||||
(defn get-permissions
|
||||
[conn profile-id team-id]
|
||||
(db/exec! conn [sql:team-permissions profile-id team-id]))
|
||||
(let [rows (db/exec! conn [sql:team-permissions profile-id team-id])
|
||||
is-owner (boolean (some :is-owner rows))
|
||||
is-admin (boolean (some :is-admin rows))
|
||||
can-edit (boolean (some :can-edit rows))]
|
||||
(when (seq rows)
|
||||
{:is-owner is-owner
|
||||
:is-admin (or is-owner is-admin)
|
||||
:can-edit (or is-owner is-admin can-edit)
|
||||
:can-read true})))
|
||||
|
||||
(def has-edit-permissions?
|
||||
(perms/make-edition-predicate-fn get-permissions))
|
||||
|
||||
(def has-read-permissions?
|
||||
(perms/make-read-predicate-fn get-permissions))
|
||||
|
||||
(def check-edition-permissions!
|
||||
(perms/make-edition-check-fn retrieve-team-permissions))
|
||||
(perms/make-check-fn has-edit-permissions?))
|
||||
|
||||
(def check-read-permissions!
|
||||
(perms/make-read-check-fn retrieve-team-permissions))
|
||||
|
||||
(perms/make-check-fn has-read-permissions?))
|
||||
|
||||
;; --- Query: Teams
|
||||
|
||||
@@ -58,12 +71,26 @@
|
||||
join team as t on (t.id = tp.team_id)
|
||||
where t.deleted_at is null
|
||||
and tp.profile_id = ?
|
||||
order by t.created_at asc")
|
||||
order by tp.created_at asc")
|
||||
|
||||
(defn process-permissions
|
||||
[team]
|
||||
(let [is-owner (:is-owner team)
|
||||
is-admin (:is-admin team)
|
||||
can-edit (:can-edit team)
|
||||
permissions {:type :membership
|
||||
:is-owner is-owner
|
||||
:is-admin (or is-owner is-admin)
|
||||
:can-edit (or is-owner is-admin can-edit)}]
|
||||
(-> team
|
||||
(dissoc :is-owner :is-admin :can-edit)
|
||||
(assoc :permissions permissions))))
|
||||
|
||||
(defn retrieve-teams
|
||||
[conn profile-id]
|
||||
(let [defaults (profile/retrieve-additional-data conn profile-id)]
|
||||
(db/exec! conn [sql:teams (:default-team-id defaults) profile-id])))
|
||||
(->> (db/exec! conn [sql:teams (:default-team-id defaults) profile-id])
|
||||
(mapv process-permissions))))
|
||||
|
||||
;; --- Query: Team (by ID)
|
||||
|
||||
@@ -86,7 +113,7 @@
|
||||
(when-not result
|
||||
(ex/raise :type :not-found
|
||||
:code :team-does-not-exist))
|
||||
result))
|
||||
(process-permissions result)))
|
||||
|
||||
|
||||
;; --- Query: Team Members
|
||||
|
||||
@@ -10,71 +10,74 @@
|
||||
[app.common.spec :as us]
|
||||
[app.db :as db]
|
||||
[app.rpc.queries.files :as files]
|
||||
[app.rpc.queries.share-link :as slnk]
|
||||
[app.rpc.queries.teams :as teams]
|
||||
[app.util.services :as sv]
|
||||
[clojure.spec.alpha :as s]))
|
||||
|
||||
;; --- Query: Viewer Bundle (by Page ID)
|
||||
|
||||
(declare check-shared-token!)
|
||||
(declare retrieve-shared-token)
|
||||
|
||||
(def ^:private
|
||||
sql:project
|
||||
"select p.id, p.name, p.team_id
|
||||
from project as p
|
||||
where p.id = ?
|
||||
and p.deleted_at is null")
|
||||
;; --- Query: View Only Bundle
|
||||
|
||||
(defn- retrieve-project
|
||||
[conn id]
|
||||
(db/exec-one! conn [sql:project id]))
|
||||
(db/get-by-id conn :project id {:columns [:id :name :team-id]}))
|
||||
|
||||
(defn- retrieve-bundle
|
||||
[{:keys [conn] :as cfg} file-id]
|
||||
(let [file (files/retrieve-file cfg file-id)
|
||||
project (retrieve-project conn (:project-id file))
|
||||
libs (files/retrieve-file-libraries cfg false file-id)
|
||||
users (teams/retrieve-users conn (:team-id project))
|
||||
|
||||
links (->> (db/query conn :share-link {:file-id file-id})
|
||||
(mapv slnk/decode-share-link-row))
|
||||
|
||||
fonts (db/query conn :team-font-variant
|
||||
{:team-id (:team-id project)
|
||||
:deleted-at nil})]
|
||||
{:file file
|
||||
:users users
|
||||
:fonts fonts
|
||||
:project project
|
||||
:share-links links
|
||||
:libraries libs}))
|
||||
|
||||
(s/def ::id ::us/uuid)
|
||||
(s/def ::file-id ::us/uuid)
|
||||
(s/def ::page-id ::us/uuid)
|
||||
(s/def ::token ::us/string)
|
||||
(s/def ::profile-id ::us/uuid)
|
||||
(s/def ::share-id ::us/uuid)
|
||||
|
||||
(s/def ::viewer-bundle
|
||||
(s/keys :req-un [::file-id ::page-id]
|
||||
:opt-un [::profile-id ::token]))
|
||||
(s/def ::view-only-bundle
|
||||
(s/keys :req-un [::file-id] :opt-un [::profile-id ::share-id]))
|
||||
|
||||
(sv/defmethod ::viewer-bundle {:auth false}
|
||||
[{:keys [pool] :as cfg} {:keys [profile-id file-id page-id token] :as params}]
|
||||
(sv/defmethod ::view-only-bundle {:auth false}
|
||||
[{:keys [pool] :as cfg} {:keys [profile-id file-id share-id] :as params}]
|
||||
(db/with-atomic [conn pool]
|
||||
(let [file (files/retrieve-file conn file-id)
|
||||
project (retrieve-project conn (:project-id file))
|
||||
page (get-in file [:data :pages-index page-id])
|
||||
file (merge (dissoc file :data)
|
||||
(select-keys (:data file) [:colors :media :typographies]))
|
||||
libs (files/retrieve-file-libraries conn false file-id)
|
||||
users (teams/retrieve-users conn (:team-id project))
|
||||
(let [cfg (assoc cfg :conn conn)
|
||||
slink (slnk/retrieve-share-link conn file-id share-id)
|
||||
perms (files/get-permissions conn profile-id file-id share-id)
|
||||
|
||||
bundle {:file file
|
||||
:page page
|
||||
:users users
|
||||
:project project
|
||||
:libraries libs}]
|
||||
bundle (some-> (retrieve-bundle cfg file-id)
|
||||
(assoc :permissions perms))]
|
||||
|
||||
(if (string? token)
|
||||
(do
|
||||
(check-shared-token! conn file-id page-id token)
|
||||
(assoc bundle :token token))
|
||||
(let [stoken (retrieve-shared-token conn file-id page-id)]
|
||||
(files/check-read-permissions! conn profile-id file-id)
|
||||
(assoc bundle :token (:token stoken)))))))
|
||||
|
||||
(defn check-shared-token!
|
||||
[conn file-id page-id token]
|
||||
(let [sql "select exists(select 1 from file_share_token where file_id=? and page_id=? and token=?) as exists"]
|
||||
(when-not (:exists (db/exec-one! conn [sql file-id page-id token]))
|
||||
(ex/raise :type :not-found
|
||||
:code :object-not-found))))
|
||||
|
||||
(defn retrieve-shared-token
|
||||
[conn file-id page-id]
|
||||
(let [sql "select * from file_share_token where file_id=? and page_id=?"]
|
||||
(db/exec-one! conn [sql file-id page-id])))
|
||||
;; When we have neither profile nor share, we just return a not
|
||||
;; found response to the user.
|
||||
(when (and (not profile-id)
|
||||
(not slink))
|
||||
(ex/raise :type :not-found
|
||||
:code :object-not-found))
|
||||
|
||||
;; When we have only profile, we need to check read permissiones
|
||||
;; on file.
|
||||
(when (and profile-id (not slink))
|
||||
(files/check-read-permissions! conn profile-id file-id))
|
||||
|
||||
(cond-> bundle
|
||||
(some? slink)
|
||||
(assoc :share slink)
|
||||
|
||||
(and (some? slink)
|
||||
(not (contains? (:flags slink) "view-all-pages")))
|
||||
(update-in [:file :data] (fn [data]
|
||||
(let [allowed-pages (:pages slink)]
|
||||
(-> data
|
||||
(update :pages (fn [pages] (filterv #(contains? allowed-pages %) pages)))
|
||||
(update :pages-index (fn [index] (select-keys index allowed-pages)))))))))))
|
||||
|
||||
29
backend/src/app/setup/keys.clj
Normal file
29
backend/src/app/setup/keys.clj
Normal file
@@ -0,0 +1,29 @@
|
||||
;; This Source Code Form is subject to the terms of the Mozilla Public
|
||||
;; License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
;;
|
||||
;; Copyright (c) UXBOX Labs SL
|
||||
|
||||
(ns app.setup.keys
|
||||
"Keys derivation service."
|
||||
(:require
|
||||
[app.common.spec :as us]
|
||||
[buddy.core.kdf :as bk]
|
||||
[clojure.spec.alpha :as s]
|
||||
[integrant.core :as ig]))
|
||||
|
||||
(s/def ::secret-key ::us/string)
|
||||
(s/def ::props (s/keys :req-un [::secret-key]))
|
||||
|
||||
(defmethod ig/pre-init-spec :app.setup/keys [_]
|
||||
(s/keys :req-un [::props]))
|
||||
|
||||
(defmethod ig/init-key :app.setup/keys
|
||||
[_ {:keys [props] :as cfg}]
|
||||
(fn [& {:keys [salt _]}]
|
||||
(let [engine (bk/engine {:key (:secret-key props)
|
||||
:salt salt
|
||||
:alg :hkdf
|
||||
:digest :blake2b-512})]
|
||||
(bk/get-bytes engine 32))))
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
(ns app.srepl
|
||||
"Server Repl."
|
||||
(:require
|
||||
[app.common.logging :as l]
|
||||
[app.common.spec :as us]
|
||||
[app.srepl.main]
|
||||
[clojure.core.server :as ccs]
|
||||
@@ -41,14 +42,17 @@
|
||||
|
||||
(defmethod ig/init-key ::server
|
||||
[_ {:keys [port host name] :as cfg}]
|
||||
(ccs/start-server {:address host
|
||||
:port port
|
||||
:name name
|
||||
:accept 'app.srepl/repl})
|
||||
cfg)
|
||||
(when (and port host name)
|
||||
(l/info :msg "initializing server repl" :port port :host host :name name)
|
||||
(ccs/start-server {:address host
|
||||
:port port
|
||||
:name name
|
||||
:accept 'app.srepl/repl})
|
||||
cfg))
|
||||
|
||||
(defmethod ig/halt-key! ::server
|
||||
[_ cfg]
|
||||
(ccs/stop-server (:name cfg)))
|
||||
(when cfg
|
||||
(ccs/stop-server (:name cfg))))
|
||||
|
||||
|
||||
|
||||
@@ -5,10 +5,11 @@
|
||||
;; Copyright (c) UXBOX Labs SL
|
||||
|
||||
(ns app.storage
|
||||
"File Storage abstraction layer."
|
||||
"Objects storage abstraction layer."
|
||||
(:require
|
||||
[app.common.data :as d]
|
||||
[app.common.exceptions :as ex]
|
||||
[app.common.logging :as l]
|
||||
[app.common.spec :as us]
|
||||
[app.common.uuid :as uuid]
|
||||
[app.db :as db]
|
||||
@@ -16,33 +17,30 @@
|
||||
[app.storage.fs :as sfs]
|
||||
[app.storage.impl :as impl]
|
||||
[app.storage.s3 :as ss3]
|
||||
[app.util.logging :as l]
|
||||
[app.util.time :as dt]
|
||||
[app.worker :as wrk]
|
||||
[clojure.spec.alpha :as s]
|
||||
[cuerdas.core :as str]
|
||||
[datoteka.core :as fs]
|
||||
[integrant.core :as ig]
|
||||
[promesa.exec :as px])
|
||||
(:import
|
||||
java.io.InputStream))
|
||||
|
||||
[promesa.exec :as px]))
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;; Storage Module State
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
|
||||
(s/def ::backend ::us/keyword)
|
||||
|
||||
(s/def ::s3 ::ss3/backend)
|
||||
(s/def ::fs ::sfs/backend)
|
||||
(s/def ::db ::sdb/backend)
|
||||
|
||||
(s/def ::backends
|
||||
(s/keys :opt-un [::s3 ::fs ::db]))
|
||||
(s/map-of ::us/keyword
|
||||
(s/nilable
|
||||
(s/or :s3 ::ss3/backend
|
||||
:fs ::sfs/backend
|
||||
:db ::sdb/backend))))
|
||||
|
||||
(defmethod ig/pre-init-spec ::storage [_]
|
||||
(s/keys :req-un [::backend ::wrk/executor ::db/pool ::backends]))
|
||||
(s/keys :req-un [::wrk/executor ::db/pool ::backends]))
|
||||
|
||||
(defmethod ig/prep-key ::storage
|
||||
[_ {:keys [backends] :as cfg}]
|
||||
@@ -50,11 +48,12 @@
|
||||
(assoc :backends (d/without-nils backends))))
|
||||
|
||||
(defmethod ig/init-key ::storage
|
||||
[_ cfg]
|
||||
cfg)
|
||||
[_ {:keys [backends] :as cfg}]
|
||||
(-> (d/without-nils cfg)
|
||||
(assoc :backends (d/without-nils backends))))
|
||||
|
||||
(s/def ::storage
|
||||
(s/keys :req-un [::backends ::wrk/executor ::db/pool ::backend]))
|
||||
(s/keys :req-un [::backends ::wrk/executor ::db/pool]))
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;; Database Objects
|
||||
@@ -151,8 +150,6 @@
|
||||
;; API
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
|
||||
(declare resolve-backend)
|
||||
|
||||
(defn object->relative-path
|
||||
[{:keys [id] :as obj}]
|
||||
(impl/id->path id))
|
||||
@@ -185,7 +182,7 @@
|
||||
(px/run! executor #(register-recheck storage backend (:id object)))
|
||||
|
||||
;; Store the data finally on the underlying storage subsystem.
|
||||
(-> (resolve-backend storage backend)
|
||||
(-> (impl/resolve-backend storage backend)
|
||||
(impl/put-object object content))
|
||||
|
||||
object))
|
||||
@@ -201,28 +198,37 @@
|
||||
;; if the source and destination backends are the same, we
|
||||
;; proceed to use the fast path with specific copy
|
||||
;; implementation on backend.
|
||||
(-> (resolve-backend storage (:backend storage))
|
||||
(-> (impl/resolve-backend storage (:backend storage))
|
||||
(impl/copy-object object object*))
|
||||
|
||||
;; if the source and destination backends are different, we just
|
||||
;; need to obtain the streams and proceed full copy of the data
|
||||
(with-open [^InputStream input
|
||||
(-> (resolve-backend storage (:backend object))
|
||||
(impl/get-object-data object))]
|
||||
(-> (resolve-backend storage (:backend storage))
|
||||
(impl/put-object object* (impl/content input (:size object))))))
|
||||
|
||||
(with-open [is (-> (impl/resolve-backend storage (:backend object))
|
||||
(impl/get-object-data object))]
|
||||
(-> (impl/resolve-backend storage (:backend storage))
|
||||
(impl/put-object object* (impl/content is (:size object))))))
|
||||
object*))
|
||||
|
||||
(defn get-object-data
|
||||
"Return an input stream instance of the object content."
|
||||
[{:keys [pool conn] :as storage} object]
|
||||
(us/assert ::storage storage)
|
||||
(when (or (nil? (:expired-at object))
|
||||
(dt/is-after? (:expired-at object) (dt/now)))
|
||||
(-> (assoc storage :conn (or conn pool))
|
||||
(resolve-backend (:backend object))
|
||||
(impl/resolve-backend (:backend object))
|
||||
(impl/get-object-data object))))
|
||||
|
||||
(defn get-object-bytes
|
||||
"Returns a byte array of object content."
|
||||
[{:keys [pool conn] :as storage} object]
|
||||
(us/assert ::storage storage)
|
||||
(when (or (nil? (:expired-at object))
|
||||
(dt/is-after? (:expired-at object) (dt/now)))
|
||||
(-> (assoc storage :conn (or conn pool))
|
||||
(impl/resolve-backend (:backend object))
|
||||
(impl/get-object-bytes object))))
|
||||
|
||||
(defn get-object-url
|
||||
([storage object]
|
||||
(get-object-url storage object nil))
|
||||
@@ -231,14 +237,14 @@
|
||||
(when (or (nil? (:expired-at object))
|
||||
(dt/is-after? (:expired-at object) (dt/now)))
|
||||
(-> (assoc storage :conn (or conn pool))
|
||||
(resolve-backend (:backend object))
|
||||
(impl/resolve-backend (:backend object))
|
||||
(impl/get-object-url object options)))))
|
||||
|
||||
(defn get-object-path
|
||||
"Get the Path to the object. Only works with `:fs` type of
|
||||
storages."
|
||||
[storage object]
|
||||
(let [backend (resolve-backend storage (:backend object))]
|
||||
(let [backend (impl/resolve-backend storage (:backend object))]
|
||||
(when (not= :fs (:type backend))
|
||||
(ex/raise :type :internal
|
||||
:code :operation-not-allowed
|
||||
@@ -254,16 +260,7 @@
|
||||
(-> (assoc storage :conn (or conn pool))
|
||||
(delete-database-object (if (uuid? id-or-obj) id-or-obj (:id id-or-obj)))))
|
||||
|
||||
;; --- impl
|
||||
|
||||
(defn resolve-backend
|
||||
[{:keys [conn pool] :as storage} backend-id]
|
||||
(let [backend (get-in storage [:backends backend-id])]
|
||||
(when-not backend
|
||||
(ex/raise :type :internal
|
||||
:code :backend-not-configured
|
||||
:hint (str/fmt "backend '%s' not configured" backend-id)))
|
||||
(assoc backend :conn (or conn pool))))
|
||||
(d/export impl/resolve-backend)
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;; Garbage Collection: Permanently delete objects
|
||||
@@ -295,7 +292,7 @@
|
||||
(some-> (seq rows) (group-by-backend))))
|
||||
|
||||
(delete-in-bulk [conn [backend ids]]
|
||||
(let [backend (resolve-backend storage backend)
|
||||
(let [backend (impl/resolve-backend storage backend)
|
||||
backend (assoc backend :conn conn)]
|
||||
(impl/del-objects-in-bulk backend ids)))]
|
||||
|
||||
@@ -319,7 +316,7 @@
|
||||
where s.deleted_at is not null
|
||||
and s.deleted_at < (now() - ?::interval)
|
||||
order by s.deleted_at
|
||||
limit 500
|
||||
limit 100
|
||||
)
|
||||
delete from storage_object
|
||||
where id in (select id from items_part)
|
||||
@@ -396,7 +393,7 @@
|
||||
from storage_object as so
|
||||
where so.touched_at is not null
|
||||
order by so.touched_at
|
||||
limit 500;")
|
||||
limit 100;")
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;; Recheck Stalled Task
|
||||
@@ -445,7 +442,7 @@
|
||||
(some-> (seq rows) (group-results))))
|
||||
|
||||
(delete-group [conn [backend ids]]
|
||||
(let [backend (resolve-backend storage backend)
|
||||
(let [backend (impl/resolve-backend storage backend)
|
||||
backend (assoc backend :conn conn)]
|
||||
(impl/del-objects-in-bulk backend ids)))
|
||||
|
||||
|
||||
@@ -46,12 +46,24 @@
|
||||
(let [result (db/exec-one! conn ["select data from storage_data where id=?" id])]
|
||||
(ByteArrayInputStream. (:data result))))
|
||||
|
||||
(defmethod impl/get-object-bytes :db
|
||||
[{:keys [conn] :as backend} {:keys [id] :as object}]
|
||||
(let [result (db/exec-one! conn ["select data from storage_data where id=?" id])]
|
||||
(:data result)))
|
||||
|
||||
(defmethod impl/get-object-url :db
|
||||
[_ _]
|
||||
(throw (UnsupportedOperationException. "not supported")))
|
||||
|
||||
(defmethod impl/del-object :db
|
||||
[_ _]
|
||||
;; NOOP: because deleting the row already deletes the file data from
|
||||
;; the database.
|
||||
nil)
|
||||
|
||||
(defmethod impl/del-objects-in-bulk :db
|
||||
[_ _]
|
||||
;; NOOP: because deleting the row already deletes the file data from
|
||||
;; the database.
|
||||
nil)
|
||||
|
||||
|
||||
@@ -79,6 +79,10 @@
|
||||
:path (str full)))
|
||||
(io/input-stream full)))
|
||||
|
||||
(defmethod impl/get-object-bytes :fs
|
||||
[backend object]
|
||||
(fs/slurp-bytes (impl/get-object-data backend object)))
|
||||
|
||||
(defmethod impl/get-object-url :fs
|
||||
[{:keys [uri] :as backend} {:keys [id] :as object} _]
|
||||
(update uri :path
|
||||
@@ -87,6 +91,13 @@
|
||||
(str existing (impl/id->path id))
|
||||
(str existing "/" (impl/id->path id))))))
|
||||
|
||||
(defmethod impl/del-object :fs
|
||||
[backend {:keys [id] :as object}]
|
||||
(let [base (fs/path (:directory backend))
|
||||
path (fs/path (impl/id->path id))
|
||||
path (fs/join base path)]
|
||||
(Files/deleteIfExists ^Path path)))
|
||||
|
||||
(defmethod impl/del-objects-in-bulk :fs
|
||||
[backend ids]
|
||||
(let [base (fs/path (:directory backend))]
|
||||
@@ -94,3 +105,4 @@
|
||||
(let [path (fs/path (impl/id->path id))
|
||||
path (fs/join base path)]
|
||||
(Files/deleteIfExists ^Path path)))))
|
||||
|
||||
|
||||
@@ -8,10 +8,10 @@
|
||||
"Storage backends abstraction layer."
|
||||
(:require
|
||||
[app.common.exceptions :as ex]
|
||||
[app.common.spec :as us]
|
||||
[app.common.uuid :as uuid]
|
||||
[buddy.core.codecs :as bc]
|
||||
[clojure.java.io :as io])
|
||||
[clojure.java.io :as io]
|
||||
[cuerdas.core :as str])
|
||||
(:import
|
||||
java.nio.ByteBuffer
|
||||
java.util.UUID
|
||||
@@ -45,6 +45,14 @@
|
||||
:code :invalid-storage-backend
|
||||
:context cfg))
|
||||
|
||||
(defmulti get-object-bytes (fn [cfg _] (:type cfg)))
|
||||
|
||||
(defmethod get-object-bytes :default
|
||||
[cfg _]
|
||||
(ex/raise :type :internal
|
||||
:code :invalid-storage-backend
|
||||
:context cfg))
|
||||
|
||||
(defmulti get-object-url (fn [cfg _ _] (:type cfg)))
|
||||
|
||||
(defmethod get-object-url :default
|
||||
@@ -54,6 +62,14 @@
|
||||
:context cfg))
|
||||
|
||||
|
||||
(defmulti del-object (fn [cfg _] (:type cfg)))
|
||||
|
||||
(defmethod del-object :default
|
||||
[cfg _]
|
||||
(ex/raise :type :internal
|
||||
:code :invalid-storage-backend
|
||||
:context cfg))
|
||||
|
||||
(defmulti del-objects-in-bulk (fn [cfg _] (:type cfg)))
|
||||
|
||||
(defmethod del-objects-in-bulk :default
|
||||
@@ -62,7 +78,6 @@
|
||||
:code :invalid-storage-backend
|
||||
:context cfg))
|
||||
|
||||
|
||||
;; --- HELPERS
|
||||
|
||||
(defn uuid->hex
|
||||
@@ -109,7 +124,10 @@
|
||||
(make-output-stream [_ opts]
|
||||
(throw (UnsupportedOperationException. "not implemented")))
|
||||
clojure.lang.Counted
|
||||
(count [_] size))))
|
||||
(count [_] size)
|
||||
|
||||
java.lang.AutoCloseable
|
||||
(close [_]))))
|
||||
|
||||
(defn string->content
|
||||
[^String v]
|
||||
@@ -129,7 +147,10 @@
|
||||
|
||||
clojure.lang.Counted
|
||||
(count [_]
|
||||
(alength data)))))
|
||||
(alength data))
|
||||
|
||||
java.lang.AutoCloseable
|
||||
(close [_]))))
|
||||
|
||||
(defn- input-stream->content
|
||||
[^InputStream is size]
|
||||
@@ -137,7 +158,7 @@
|
||||
IContentObject
|
||||
io/IOFactory
|
||||
(make-reader [_ opts]
|
||||
(io/make-reader is opts))
|
||||
(io/make-reader is opts))
|
||||
(make-writer [_ opts]
|
||||
(throw (UnsupportedOperationException. "not implemented")))
|
||||
(make-input-stream [_ opts]
|
||||
@@ -146,7 +167,11 @@
|
||||
(throw (UnsupportedOperationException. "not implemented")))
|
||||
|
||||
clojure.lang.Counted
|
||||
(count [_] size)))
|
||||
(count [_] size)
|
||||
|
||||
java.lang.AutoCloseable
|
||||
(close [_]
|
||||
(.close is))))
|
||||
|
||||
(defn content
|
||||
([data] (content data nil))
|
||||
@@ -179,10 +204,20 @@
|
||||
|
||||
(defn slurp-bytes
|
||||
[content]
|
||||
(us/assert content? content)
|
||||
(with-open [input (io/input-stream content)
|
||||
output (java.io.ByteArrayOutputStream. (count content))]
|
||||
(io/copy input output)
|
||||
(.toByteArray output)))
|
||||
|
||||
(defn resolve-backend
|
||||
[{:keys [conn pool] :as storage} backend-id]
|
||||
(when backend-id
|
||||
(let [backend (get-in storage [:backends backend-id])]
|
||||
(when-not backend
|
||||
(ex/raise :type :internal
|
||||
:code :backend-not-configured
|
||||
:hint (str/fmt "backend '%s' not configured" backend-id)))
|
||||
(assoc backend
|
||||
:conn (or conn pool)
|
||||
:id backend-id))))
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
;; Copyright (c) UXBOX Labs SL
|
||||
|
||||
(ns app.storage.s3
|
||||
"Storage backends abstraction layer."
|
||||
"S3 Storage backend implementation."
|
||||
(:require
|
||||
[app.common.data :as d]
|
||||
[app.common.exceptions :as ex]
|
||||
@@ -18,25 +18,34 @@
|
||||
[integrant.core :as ig])
|
||||
(:import
|
||||
java.time.Duration
|
||||
java.io.InputStream
|
||||
java.util.Collection
|
||||
software.amazon.awssdk.core.sync.RequestBody
|
||||
software.amazon.awssdk.core.ResponseBytes
|
||||
;; software.amazon.awssdk.core.ResponseInputStream
|
||||
software.amazon.awssdk.regions.Region
|
||||
software.amazon.awssdk.services.s3.S3Client
|
||||
software.amazon.awssdk.services.s3.model.Delete
|
||||
software.amazon.awssdk.services.s3.model.CopyObjectRequest
|
||||
software.amazon.awssdk.services.s3.model.DeleteObjectsRequest
|
||||
software.amazon.awssdk.services.s3.model.DeleteObjectsResponse
|
||||
software.amazon.awssdk.services.s3.model.DeleteObjectRequest
|
||||
software.amazon.awssdk.services.s3.model.GetObjectRequest
|
||||
software.amazon.awssdk.services.s3.model.ObjectIdentifier
|
||||
software.amazon.awssdk.services.s3.model.PutObjectRequest
|
||||
;; software.amazon.awssdk.services.s3.model.GetObjectResponse
|
||||
software.amazon.awssdk.services.s3.presigner.S3Presigner
|
||||
software.amazon.awssdk.services.s3.presigner.model.GetObjectPresignRequest
|
||||
software.amazon.awssdk.services.s3.presigner.model.PresignedGetObjectRequest))
|
||||
software.amazon.awssdk.services.s3.presigner.model.PresignedGetObjectRequest
|
||||
|
||||
))
|
||||
|
||||
(declare put-object)
|
||||
(declare copy-object)
|
||||
(declare get-object)
|
||||
(declare get-object-bytes)
|
||||
(declare get-object-data)
|
||||
(declare get-object-url)
|
||||
(declare del-object)
|
||||
(declare del-object-in-bulk)
|
||||
(declare build-s3-client)
|
||||
(declare build-s3-presigner)
|
||||
@@ -87,12 +96,20 @@
|
||||
|
||||
(defmethod impl/get-object-data :s3
|
||||
[backend object]
|
||||
(get-object backend object))
|
||||
(get-object-data backend object))
|
||||
|
||||
(defmethod impl/get-object-bytes :s3
|
||||
[backend object]
|
||||
(get-object-bytes backend object))
|
||||
|
||||
(defmethod impl/get-object-url :s3
|
||||
[backend object options]
|
||||
(get-object-url backend object options))
|
||||
|
||||
(defmethod impl/del-object :s3
|
||||
[backend object]
|
||||
(del-object backend object))
|
||||
|
||||
(defmethod impl/del-objects-in-bulk :s3
|
||||
[backend ids]
|
||||
(del-object-in-bulk backend ids))
|
||||
@@ -104,19 +121,19 @@
|
||||
(case region
|
||||
:eu-central-1 Region/EU_CENTRAL_1))
|
||||
|
||||
(defn- build-s3-client
|
||||
(defn build-s3-client
|
||||
[{:keys [region]}]
|
||||
(.. (S3Client/builder)
|
||||
(region (lookup-region region))
|
||||
(build)))
|
||||
|
||||
(defn- build-s3-presigner
|
||||
(defn build-s3-presigner
|
||||
[{:keys [region]}]
|
||||
(.. (S3Presigner/builder)
|
||||
(region (lookup-region region))
|
||||
(build)))
|
||||
|
||||
(defn- put-object
|
||||
(defn put-object
|
||||
[{:keys [client bucket prefix]} {:keys [id] :as object} content]
|
||||
(let [path (str prefix (impl/id->path id))
|
||||
mdata (meta object)
|
||||
@@ -125,14 +142,15 @@
|
||||
(bucket bucket)
|
||||
(contentType mtype)
|
||||
(key path)
|
||||
(build))
|
||||
content (RequestBody/fromInputStream (io/input-stream content)
|
||||
(count content))]
|
||||
(.putObject ^S3Client client
|
||||
^PutObjectRequest request
|
||||
^RequestBody content)))
|
||||
(build))]
|
||||
|
||||
(defn- copy-object
|
||||
(with-open [^InputStream is (io/input-stream content)]
|
||||
(let [content (RequestBody/fromInputStream is (count content))]
|
||||
(.putObject ^S3Client client
|
||||
^PutObjectRequest request
|
||||
^RequestBody content)))))
|
||||
|
||||
(defn copy-object
|
||||
[{:keys [client bucket prefix]} src-object dst-object]
|
||||
(let [source-path (str prefix (impl/id->path (:id src-object)))
|
||||
source-mdata (meta src-object)
|
||||
@@ -146,22 +164,33 @@
|
||||
(contentType source-mtype)
|
||||
(build))]
|
||||
|
||||
(.copyObject ^S3Client client
|
||||
^CopyObjectRequest request)))
|
||||
(.copyObject ^S3Client client ^CopyObjectRequest request)))
|
||||
|
||||
(defn- get-object
|
||||
(defn get-object-data
|
||||
[{:keys [client bucket prefix]} {:keys [id]}]
|
||||
(let [gor (.. (GetObjectRequest/builder)
|
||||
(bucket bucket)
|
||||
(key (str prefix (impl/id->path id)))
|
||||
(build))
|
||||
obj (.getObject ^S3Client client ^GetObjectRequest gor)]
|
||||
obj (.getObject ^S3Client client ^GetObjectRequest gor)
|
||||
;; rsp (.response ^ResponseInputStream obj)
|
||||
;; len (.contentLength ^GetObjectResponse rsp)
|
||||
]
|
||||
(io/input-stream obj)))
|
||||
|
||||
(defn get-object-bytes
|
||||
[{:keys [client bucket prefix]} {:keys [id]}]
|
||||
(let [gor (.. (GetObjectRequest/builder)
|
||||
(bucket bucket)
|
||||
(key (str prefix (impl/id->path id)))
|
||||
(build))
|
||||
obj (.getObjectAsBytes ^S3Client client ^GetObjectRequest gor)]
|
||||
(.asByteArray ^ResponseBytes obj)))
|
||||
|
||||
(def default-max-age
|
||||
(dt/duration {:minutes 10}))
|
||||
|
||||
(defn- get-object-url
|
||||
(defn get-object-url
|
||||
[{:keys [presigner bucket prefix]} {:keys [id]} {:keys [max-age] :or {max-age default-max-age}}]
|
||||
(us/assert dt/duration? max-age)
|
||||
(let [gor (.. (GetObjectRequest/builder)
|
||||
@@ -175,7 +204,16 @@
|
||||
pgor (.presignGetObject ^S3Presigner presigner ^GetObjectPresignRequest gopr)]
|
||||
(u/uri (str (.url ^PresignedGetObjectRequest pgor)))))
|
||||
|
||||
(defn- del-object-in-bulk
|
||||
(defn del-object
|
||||
[{:keys [bucket client prefix]} {:keys [id] :as obj}]
|
||||
(let [dor (.. (DeleteObjectRequest/builder)
|
||||
(bucket bucket)
|
||||
(key (str prefix (impl/id->path id)))
|
||||
(build))]
|
||||
(.deleteObject ^S3Client client
|
||||
^DeleteObjectRequest dor)))
|
||||
|
||||
(defn del-object-in-bulk
|
||||
[{:keys [bucket client prefix]} ids]
|
||||
(let [oids (map (fn [id]
|
||||
(.. (ObjectIdentifier/builder)
|
||||
|
||||
@@ -1,67 +0,0 @@
|
||||
;; This Source Code Form is subject to the terms of the Mozilla Public
|
||||
;; License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
;;
|
||||
;; Copyright (c) UXBOX Labs SL
|
||||
|
||||
(ns app.tasks.delete-object
|
||||
"Generic task for permanent deletion of objects."
|
||||
(:require
|
||||
[app.common.data :as d]
|
||||
[app.common.spec :as us]
|
||||
[app.db :as db]
|
||||
[app.storage :as sto]
|
||||
[app.util.logging :as l]
|
||||
[clojure.spec.alpha :as s]
|
||||
[integrant.core :as ig]))
|
||||
|
||||
(declare handle-deletion)
|
||||
|
||||
(defmethod ig/pre-init-spec ::handler [_]
|
||||
(s/keys :req-un [::db/pool]))
|
||||
|
||||
(defmethod ig/init-key ::handler
|
||||
[_ {:keys [pool] :as cfg}]
|
||||
(fn [{:keys [props] :as task}]
|
||||
(us/verify ::props props)
|
||||
(db/with-atomic [conn pool]
|
||||
(let [cfg (assoc cfg :conn conn)]
|
||||
(handle-deletion cfg props)))))
|
||||
|
||||
(s/def ::type ::us/keyword)
|
||||
(s/def ::id ::us/uuid)
|
||||
(s/def ::props (s/keys :req-un [::id ::type]))
|
||||
|
||||
(defmulti handle-deletion
|
||||
(fn [_ props] (:type props)))
|
||||
|
||||
(defmethod handle-deletion :default
|
||||
[_cfg {:keys [type]}]
|
||||
(l/warn :hint "no handler found"
|
||||
:type (d/name type)))
|
||||
|
||||
(defmethod handle-deletion :file
|
||||
[{:keys [conn]} {:keys [id] :as props}]
|
||||
(let [sql "delete from file where id=? and deleted_at is not null"]
|
||||
(db/exec-one! conn [sql id])))
|
||||
|
||||
(defmethod handle-deletion :project
|
||||
[{:keys [conn]} {:keys [id] :as props}]
|
||||
(let [sql "delete from project where id=? and deleted_at is not null"]
|
||||
(db/exec-one! conn [sql id])))
|
||||
|
||||
(defmethod handle-deletion :team
|
||||
[{:keys [conn]} {:keys [id] :as props}]
|
||||
(let [sql "delete from team where id=? and deleted_at is not null"]
|
||||
(db/exec-one! conn [sql id])))
|
||||
|
||||
(defmethod handle-deletion :team-font-variant
|
||||
[{:keys [conn storage]} {:keys [id] :as props}]
|
||||
(let [font (db/get-by-id conn :team-font-variant id {:uncheked true})
|
||||
storage (assoc storage :conn conn)]
|
||||
(when (:deleted-at font)
|
||||
(db/delete! conn :team-font-variant {:id id})
|
||||
(some->> (:woff1-file-id font) (sto/del-object storage))
|
||||
(some->> (:woff2-file-id font) (sto/del-object storage))
|
||||
(some->> (:otf-file-id font) (sto/del-object storage))
|
||||
(some->> (:ttf-file-id font) (sto/del-object storage)))))
|
||||
@@ -1,76 +0,0 @@
|
||||
;; This Source Code Form is subject to the terms of the Mozilla Public
|
||||
;; License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
;;
|
||||
;; Copyright (c) UXBOX Labs SL
|
||||
|
||||
(ns app.tasks.delete-profile
|
||||
"Task for permanent deletion of profiles."
|
||||
(:require
|
||||
[app.common.spec :as us]
|
||||
[app.db :as db]
|
||||
[app.db.sql :as sql]
|
||||
[app.util.logging :as l]
|
||||
[clojure.spec.alpha :as s]
|
||||
[integrant.core :as ig]))
|
||||
|
||||
(declare delete-profile-data)
|
||||
|
||||
;; --- INIT
|
||||
|
||||
(defmethod ig/pre-init-spec ::handler [_]
|
||||
(s/keys :req-un [::db/pool]))
|
||||
|
||||
;; This task is responsible to permanently delete a profile with all
|
||||
;; the dependent data. As step (1) we delete all owned teams of the
|
||||
;; profile (that will cause to delete all underlying projects, files,
|
||||
;; file_media and mark to be deleted storage_object's used by team,
|
||||
;; profile and files previously deleted. Then, finally as step (2) we
|
||||
;; proceed to delete the profile row.
|
||||
;;
|
||||
;; The storage_objects marked as deleted will be deleted by the
|
||||
;; corresponding garbage collector task.
|
||||
|
||||
(s/def ::profile-id ::us/uuid)
|
||||
(s/def ::props (s/keys :req-un [::profile-id]))
|
||||
|
||||
(defmethod ig/init-key ::handler
|
||||
[_ {:keys [pool] :as cfg}]
|
||||
(fn [{:keys [props] :as task}]
|
||||
(us/verify ::props props)
|
||||
(db/with-atomic [conn pool]
|
||||
(let [id (:profile-id props)
|
||||
profile (db/exec-one! conn (sql/select :profile {:id id} {:for-update true}))]
|
||||
(if (or (:is-demo profile)
|
||||
(:deleted-at profile))
|
||||
(delete-profile-data conn id)
|
||||
(l/warn :hint "profile does not match constraints for deletion"
|
||||
:profile-id id))))))
|
||||
|
||||
;; --- IMPL
|
||||
|
||||
(def ^:private sql:remove-owned-teams
|
||||
"delete from team
|
||||
where id in (
|
||||
select tpr.team_id
|
||||
from team_profile_rel as tpr
|
||||
where tpr.is_owner is true
|
||||
and tpr.profile_id = ?
|
||||
)")
|
||||
|
||||
(defn- delete-teams
|
||||
[conn profile-id]
|
||||
(db/exec-one! conn [sql:remove-owned-teams profile-id]))
|
||||
|
||||
(defn delete-profile
|
||||
[conn profile-id]
|
||||
(db/delete! conn :profile {:id profile-id}))
|
||||
|
||||
(defn- delete-profile-data
|
||||
[conn profile-id]
|
||||
(l/debug :action "delete profile"
|
||||
:profile-id profile-id)
|
||||
(delete-teams conn profile-id)
|
||||
(delete-profile conn profile-id)
|
||||
true)
|
||||
|
||||
@@ -9,10 +9,10 @@
|
||||
objects from files. A file is ellegible to be garbage collected
|
||||
after some period of inactivity (the default threshold is 72h)."
|
||||
(:require
|
||||
[app.common.logging :as l]
|
||||
[app.common.pages.migrations :as pmg]
|
||||
[app.db :as db]
|
||||
[app.util.blob :as blob]
|
||||
[app.util.logging :as l]
|
||||
[app.util.time :as dt]
|
||||
[clojure.spec.alpha :as s]
|
||||
[integrant.core :as ig]))
|
||||
@@ -64,16 +64,21 @@
|
||||
(comp
|
||||
(map :objects)
|
||||
(mapcat vals)
|
||||
(filter #(= :image (:type %)))
|
||||
(map :metadata)
|
||||
(map :id)))
|
||||
(map (fn [{:keys [type] :as obj}]
|
||||
(case type
|
||||
:path (get-in obj [:fill-image :id])
|
||||
:image (get-in obj [:metadata :id])
|
||||
nil)))
|
||||
(filter uuid?)))
|
||||
|
||||
(defn- collect-used-media
|
||||
[data]
|
||||
(let [pages (concat
|
||||
(vals (:pages-index data))
|
||||
(vals (:components data)))]
|
||||
(-> #{}
|
||||
(into collect-media-xf (vals (:pages-index data)))
|
||||
(into collect-media-xf (vals (:components data)))
|
||||
(into (keys (:media data)))))
|
||||
(into collect-media-xf pages)
|
||||
(into (keys (:media data))))))
|
||||
|
||||
(defn- process-file
|
||||
[{:keys [conn] :as cfg} {:keys [id data age] :as file}]
|
||||
@@ -100,6 +105,7 @@
|
||||
:id (:id mobj)
|
||||
:media-id (:media-id mobj)
|
||||
:thumbnail-id (:thumbnail-id mobj))
|
||||
|
||||
;; NOTE: deleting the file-media-object in the database
|
||||
;; automatically marks as toched the referenced storage
|
||||
;; objects. The touch mechanism is needed because many files can
|
||||
|
||||
63
backend/src/app/tasks/file_offload.clj
Normal file
63
backend/src/app/tasks/file_offload.clj
Normal file
@@ -0,0 +1,63 @@
|
||||
;; This Source Code Form is subject to the terms of the Mozilla Public
|
||||
;; License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
;;
|
||||
;; Copyright (c) UXBOX Labs SL
|
||||
|
||||
(ns app.tasks.file-offload
|
||||
"A maintenance task that offloads file data to an external storage (S3)."
|
||||
(:require
|
||||
[app.common.logging :as l]
|
||||
[app.common.spec :as us]
|
||||
[app.db :as db]
|
||||
[app.storage :as sto]
|
||||
[app.storage.impl :as simpl]
|
||||
[app.util.time :as dt]
|
||||
[clojure.spec.alpha :as s]
|
||||
[integrant.core :as ig]))
|
||||
|
||||
(def sql:offload-candidates-chunk
|
||||
"select f.id, f.data from file as f
|
||||
where f.data is not null
|
||||
and f.modified_at < now() - ?::interval
|
||||
order by f.modified_at
|
||||
limit 10")
|
||||
|
||||
(defn- retrieve-candidates
|
||||
[{:keys [conn max-age]}]
|
||||
(db/exec! conn [sql:offload-candidates-chunk max-age]))
|
||||
|
||||
(defn- offload-candidate
|
||||
[{:keys [storage conn backend] :as cfg} {:keys [id data] :as file}]
|
||||
(l/debug :action "offload file data" :id id)
|
||||
(let [backend (simpl/resolve-backend storage backend)]
|
||||
(->> (simpl/content data)
|
||||
(simpl/put-object backend file))
|
||||
(db/update! conn :file
|
||||
{:data nil
|
||||
:data-backend (name (:id backend))}
|
||||
{:id id})))
|
||||
|
||||
;; ---- STATE INIT
|
||||
|
||||
(s/def ::max-age ::dt/duration)
|
||||
(s/def ::backend ::us/keyword)
|
||||
|
||||
(defmethod ig/pre-init-spec ::handler [_]
|
||||
(s/keys :req-un [::db/pool ::max-age ::sto/storage ::backend]))
|
||||
|
||||
(defmethod ig/init-key ::handler
|
||||
[_ {:keys [pool max-age] :as cfg}]
|
||||
(fn [_]
|
||||
(db/with-atomic [conn pool]
|
||||
(let [max-age (db/interval max-age)
|
||||
cfg (-> cfg
|
||||
(assoc :conn conn)
|
||||
(assoc :max-age max-age))]
|
||||
(loop [n 0]
|
||||
(let [candidates (retrieve-candidates cfg)]
|
||||
(if (seq candidates)
|
||||
(do
|
||||
(run! (partial offload-candidate cfg) candidates)
|
||||
(recur (+ n (count candidates))))
|
||||
(l/debug :hint "offload summary" :count n))))))))
|
||||
@@ -8,8 +8,8 @@
|
||||
"A maintenance task that performs a garbage collection of the file
|
||||
change (transaction) log."
|
||||
(:require
|
||||
[app.common.logging :as l]
|
||||
[app.db :as db]
|
||||
[app.util.logging :as l]
|
||||
[app.util.time :as dt]
|
||||
[clojure.spec.alpha :as s]
|
||||
[integrant.core :as ig]))
|
||||
@@ -28,7 +28,7 @@
|
||||
(let [interval (db/interval max-age)
|
||||
result (db/exec-one! conn [sql:delete-files-xlog interval])
|
||||
result (:next.jdbc/update-count result)]
|
||||
(l/debug :action "trim file-change table" :removed result)
|
||||
(l/debug :removed result :hint "remove old file changes")
|
||||
result))))
|
||||
|
||||
(def ^:private
|
||||
|
||||
171
backend/src/app/tasks/objects_gc.clj
Normal file
171
backend/src/app/tasks/objects_gc.clj
Normal file
@@ -0,0 +1,171 @@
|
||||
;; This Source Code Form is subject to the terms of the Mozilla Public
|
||||
;; License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
;;
|
||||
;; Copyright (c) UXBOX Labs SL
|
||||
|
||||
(ns app.tasks.objects-gc
|
||||
"A maintenance task that performs a general purpose garbage collection
|
||||
of deleted objects."
|
||||
(:require
|
||||
[app.common.logging :as l]
|
||||
[app.config :as cf]
|
||||
[app.db :as db]
|
||||
[app.storage :as sto]
|
||||
[app.storage.impl :as simpl]
|
||||
[app.util.time :as dt]
|
||||
[clojure.spec.alpha :as s]
|
||||
[cuerdas.core :as str]
|
||||
[integrant.core :as ig]))
|
||||
|
||||
(def target-tables
|
||||
["profile"
|
||||
"team"
|
||||
"file"
|
||||
"project"
|
||||
"team_font_variant"])
|
||||
|
||||
(defmulti delete-objects :table)
|
||||
|
||||
(def sql:delete-objects
|
||||
"with deleted as (
|
||||
select id from %(table)s
|
||||
where deleted_at is not null
|
||||
and deleted_at < now() - ?::interval
|
||||
order by deleted_at
|
||||
limit %(limit)s
|
||||
)
|
||||
delete from %(table)s
|
||||
where id in (select id from deleted)
|
||||
returning *")
|
||||
|
||||
;; --- IMPL: generic object deletion
|
||||
|
||||
(defmethod delete-objects :default
|
||||
[{:keys [conn max-age table] :as cfg}]
|
||||
(let [sql (str/fmt sql:delete-objects
|
||||
{:table table :limit 50})
|
||||
result (db/exec! conn [sql max-age])]
|
||||
|
||||
(doseq [{:keys [id] :as item} result]
|
||||
(l/trace :action "delete object" :table table :id id))
|
||||
|
||||
(count result)))
|
||||
|
||||
|
||||
;; --- IMPL: file deletion
|
||||
|
||||
(defmethod delete-objects "file"
|
||||
[{:keys [conn max-age table storage] :as cfg}]
|
||||
(let [sql (str/fmt sql:delete-objects
|
||||
{:table table :limit 50})
|
||||
result (db/exec! conn [sql max-age])
|
||||
backend (simpl/resolve-backend storage (cf/get :fdata-storage-backend))]
|
||||
|
||||
(doseq [{:keys [id] :as item} result]
|
||||
(l/trace :action "delete object" :table table :id id)
|
||||
(when backend
|
||||
(simpl/del-object backend item)))
|
||||
|
||||
(count result)))
|
||||
|
||||
;; --- IMPL: team-font-variant deletion
|
||||
|
||||
(defmethod delete-objects "team_font_variant"
|
||||
[{:keys [conn max-age storage table] :as cfg}]
|
||||
(let [sql (str/fmt sql:delete-objects
|
||||
{:table table :limit 50})
|
||||
fonts (db/exec! conn [sql max-age])
|
||||
storage (assoc storage :conn conn)]
|
||||
(doseq [{:keys [id] :as font} fonts]
|
||||
(l/trace :action "delete object" :table table :id id)
|
||||
(some->> (:woff1-file-id font) (sto/del-object storage))
|
||||
(some->> (:woff2-file-id font) (sto/del-object storage))
|
||||
(some->> (:otf-file-id font) (sto/del-object storage))
|
||||
(some->> (:ttf-file-id font) (sto/del-object storage)))
|
||||
(count fonts)))
|
||||
|
||||
;; --- IMPL: team deletion
|
||||
|
||||
(defmethod delete-objects "team"
|
||||
[{:keys [conn max-age storage table] :as cfg}]
|
||||
(let [sql (str/fmt sql:delete-objects
|
||||
{:table table :limit 50})
|
||||
teams (db/exec! conn [sql max-age])
|
||||
storage (assoc storage :conn conn)]
|
||||
|
||||
(doseq [{:keys [id] :as team} teams]
|
||||
(l/trace :action "delete object" :table table :id id)
|
||||
(some->> (:photo-id team) (sto/del-object storage)))
|
||||
|
||||
(count teams)))
|
||||
|
||||
;; --- IMPL: profile deletion
|
||||
|
||||
(def sql:retrieve-deleted-profiles
|
||||
"select id, photo_id from profile
|
||||
where deleted_at is not null
|
||||
and deleted_at < now() - ?::interval
|
||||
order by deleted_at
|
||||
limit %(limit)s
|
||||
for update")
|
||||
|
||||
(def sql:mark-owned-teams-deleted
|
||||
"with owned as (
|
||||
select tpr.team_id as id
|
||||
from team_profile_rel as tpr
|
||||
where tpr.is_owner is true
|
||||
and tpr.profile_id = ?
|
||||
)
|
||||
update team set deleted_at = now() - ?::interval
|
||||
where id in (select id from owned)")
|
||||
|
||||
(defmethod delete-objects "profile"
|
||||
[{:keys [conn max-age storage table] :as cfg}]
|
||||
(let [sql (str/fmt sql:retrieve-deleted-profiles {:limit 50})
|
||||
profiles (db/exec! conn [sql max-age])
|
||||
storage (assoc storage :conn conn)]
|
||||
|
||||
(doseq [{:keys [id] :as profile} profiles]
|
||||
(l/trace :action "delete object" :table table :id id)
|
||||
|
||||
;; Mark the owned teams as deleted; this enables them to be procesed
|
||||
;; in the same transaction in the "team" table step.
|
||||
(db/exec-one! conn [sql:mark-owned-teams-deleted id max-age])
|
||||
|
||||
;; Mark as deleted the storage object related with the photo-id
|
||||
;; field.
|
||||
(some->> (:photo-id profile) (sto/del-object storage))
|
||||
|
||||
;; And finally, permanently delete the profile.
|
||||
(db/delete! conn :profile {:id id}))
|
||||
|
||||
(count profiles)))
|
||||
|
||||
;; --- INIT
|
||||
|
||||
(defn- process-table
|
||||
[{:keys [table] :as cfg}]
|
||||
(loop [n 0]
|
||||
(let [res (delete-objects cfg)]
|
||||
(if (pos? res)
|
||||
(recur (+ n res))
|
||||
(l/debug :hint "table gc summary" :table table :deleted n)))))
|
||||
|
||||
(s/def ::max-age ::dt/duration)
|
||||
|
||||
(defmethod ig/pre-init-spec ::handler [_]
|
||||
(s/keys :req-un [::db/pool ::sto/storage ::max-age]))
|
||||
|
||||
(defmethod ig/init-key ::handler
|
||||
[_ {:keys [pool max-age] :as cfg}]
|
||||
(fn [task]
|
||||
;; Checking first on task argument allows properly testing it.
|
||||
(let [max-age (get task :max-age max-age)]
|
||||
(db/with-atomic [conn pool]
|
||||
(let [max-age (db/interval max-age)
|
||||
cfg (-> cfg
|
||||
(assoc :max-age max-age)
|
||||
(assoc :conn conn))]
|
||||
(doseq [table target-tables]
|
||||
(process-table (assoc cfg :table table))))))))
|
||||
@@ -8,8 +8,8 @@
|
||||
"A maintenance task that performs a cleanup of already executed tasks
|
||||
from the database table."
|
||||
(:require
|
||||
[app.common.logging :as l]
|
||||
[app.db :as db]
|
||||
[app.util.logging :as l]
|
||||
[app.util.time :as dt]
|
||||
[clojure.spec.alpha :as s]
|
||||
[integrant.core :as ig]))
|
||||
|
||||
@@ -9,21 +9,12 @@
|
||||
(:require
|
||||
[app.common.exceptions :as ex]
|
||||
[app.common.spec :as us]
|
||||
[app.common.transit :as t]
|
||||
[app.util.time :as dt]
|
||||
[app.util.transit :as t]
|
||||
[buddy.core.kdf :as bk]
|
||||
[buddy.sign.jwe :as jwe]
|
||||
[clojure.spec.alpha :as s]
|
||||
[integrant.core :as ig]))
|
||||
|
||||
(defn- derive-tokens-secret
|
||||
[key]
|
||||
(let [engine (bk/engine {:key key
|
||||
:salt "tokens"
|
||||
:alg :hkdf
|
||||
:digest :blake2b-512})]
|
||||
(bk/get-bytes engine 32)))
|
||||
|
||||
(defn- generate
|
||||
[cfg claims]
|
||||
(let [payload (t/encode claims)]
|
||||
@@ -50,13 +41,6 @@
|
||||
:params params))
|
||||
claims))
|
||||
|
||||
(s/def ::secret-key ::us/string)
|
||||
(s/def ::props
|
||||
(s/keys :req-un [::secret-key]))
|
||||
|
||||
(defmethod ig/pre-init-spec ::tokens [_]
|
||||
(s/keys :req-un [::props]))
|
||||
|
||||
(defn- generate-predefined
|
||||
[cfg {:keys [iss profile-id] :as params}]
|
||||
(case iss
|
||||
@@ -70,9 +54,14 @@
|
||||
:code :not-implemented
|
||||
:hint "no predefined token")))
|
||||
|
||||
(s/def ::keys fn?)
|
||||
|
||||
(defmethod ig/pre-init-spec ::tokens [_]
|
||||
(s/keys :req-un [::keys]))
|
||||
|
||||
(defmethod ig/init-key ::tokens
|
||||
[_ {:keys [props] :as cfg}]
|
||||
(let [secret (derive-tokens-secret (:secret-key props))
|
||||
[_ {:keys [keys] :as cfg}]
|
||||
(let [secret (keys :salt "tokens" :size 32)
|
||||
cfg (assoc cfg ::secret secret)]
|
||||
(fn [action params]
|
||||
(case action
|
||||
|
||||
@@ -64,12 +64,14 @@
|
||||
(defn batch
|
||||
[in {:keys [max-batch-size
|
||||
max-batch-age
|
||||
buffer-size
|
||||
init]
|
||||
:or {max-batch-size 200
|
||||
max-batch-age (* 30 1000)
|
||||
buffer-size 128
|
||||
init #{}}
|
||||
:as opts}]
|
||||
(let [out (a/chan)]
|
||||
(let [out (a/chan buffer-size)]
|
||||
(a/go-loop [tch (a/timeout max-batch-age) buf init]
|
||||
(let [[val port] (a/alts! [tch in])]
|
||||
(cond
|
||||
|
||||
@@ -8,8 +8,8 @@
|
||||
"A generic blob storage encoding. Mainly used for page data, page
|
||||
options and txlog payload storage."
|
||||
(:require
|
||||
[app.common.transit :as t]
|
||||
[app.config :as cf]
|
||||
[app.util.transit :as t]
|
||||
[taoensso.nippy :as n])
|
||||
(:import
|
||||
java.io.ByteArrayInputStream
|
||||
@@ -108,7 +108,7 @@
|
||||
cdata (byte-array mlen)
|
||||
clen (Zstd/compressByteArray ^bytes cdata 0 mlen
|
||||
^bytes data 0 dlen
|
||||
4)]
|
||||
6)]
|
||||
(with-open [^ByteArrayOutputStream baos (ByteArrayOutputStream. (+ (alength cdata) 2 4))
|
||||
^DataOutputStream dos (DataOutputStream. baos)]
|
||||
(.writeShort dos (short 3)) ;; version number
|
||||
|
||||
@@ -6,7 +6,6 @@
|
||||
|
||||
(ns app.util.emails
|
||||
(:require
|
||||
[app.common.data :as d]
|
||||
[app.common.exceptions :as ex]
|
||||
[app.common.spec :as us]
|
||||
[app.util.template :as tmpl]
|
||||
@@ -199,7 +198,7 @@
|
||||
(ex/raise :type :internal
|
||||
:code :missing-email-templates))
|
||||
{:subject subj
|
||||
:body (d/concat
|
||||
:body (into
|
||||
[{:type "text/plain"
|
||||
:content text}]
|
||||
(when html
|
||||
|
||||
@@ -1,110 +0,0 @@
|
||||
;; This Source Code Form is subject to the terms of the Mozilla Public
|
||||
;; License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
;;
|
||||
;; Copyright (c) UXBOX Labs SL
|
||||
|
||||
(ns app.util.logging
|
||||
(:require
|
||||
[clojure.pprint :refer [pprint]])
|
||||
(:import
|
||||
org.apache.logging.log4j.Level
|
||||
org.apache.logging.log4j.LogManager
|
||||
org.apache.logging.log4j.Logger
|
||||
org.apache.logging.log4j.ThreadContext
|
||||
org.apache.logging.log4j.message.MapMessage
|
||||
org.apache.logging.log4j.spi.LoggerContext))
|
||||
|
||||
(defn build-map-message
|
||||
[m]
|
||||
(let [message (MapMessage. (count m))]
|
||||
(reduce-kv #(.with ^MapMessage %1 (name %2) %3) message m)))
|
||||
|
||||
(defprotocol ILogger
|
||||
(-enabled? [logger level])
|
||||
(-write! [logger level throwable message]))
|
||||
|
||||
(def logger-context
|
||||
(LogManager/getContext false))
|
||||
|
||||
(def logging-agent
|
||||
(agent nil :error-mode :continue))
|
||||
|
||||
(defn get-logger
|
||||
[lname]
|
||||
(.getLogger ^LoggerContext logger-context ^String lname))
|
||||
|
||||
(defn get-level
|
||||
[level]
|
||||
(case level
|
||||
:trace Level/TRACE
|
||||
:debug Level/DEBUG
|
||||
:info Level/INFO
|
||||
:warn Level/WARN
|
||||
:error Level/ERROR
|
||||
:fatal Level/FATAL))
|
||||
|
||||
(defn enabled?
|
||||
[logger level]
|
||||
(.isEnabled ^Logger logger ^Level level))
|
||||
|
||||
(defn write-log!
|
||||
[logger level e msg]
|
||||
(if e
|
||||
(.log ^Logger logger
|
||||
^Level level
|
||||
^Object msg
|
||||
^Throwable e)
|
||||
(.log ^Logger logger
|
||||
^Level level
|
||||
^Object msg)))
|
||||
|
||||
(defmacro log
|
||||
[& {:keys [level cause ::logger ::async ::raw] :as props}]
|
||||
(let [props (dissoc props :level :cause ::logger ::async ::raw)
|
||||
logger (or logger (str *ns*))
|
||||
logger-sym (gensym "log")
|
||||
level-sym (gensym "log")]
|
||||
`(let [~logger-sym (get-logger ~logger)
|
||||
~level-sym (get-level ~level)]
|
||||
(if (enabled? ~logger-sym ~level-sym)
|
||||
~(if async
|
||||
`(send-off logging-agent
|
||||
(fn [_#]
|
||||
(let [message# (or ~raw (build-map-message ~props))]
|
||||
(write-log! ~logger-sym ~level-sym ~cause message#))))
|
||||
`(let [message# (or ~raw (build-map-message ~props))]
|
||||
(write-log! ~logger-sym ~level-sym ~cause message#)))))))
|
||||
|
||||
(defmacro info
|
||||
[& params]
|
||||
`(log :level :info ~@params))
|
||||
|
||||
(defmacro error
|
||||
[& params]
|
||||
`(log :level :error ~@params))
|
||||
|
||||
(defmacro warn
|
||||
[& params]
|
||||
`(log :level :warn ~@params))
|
||||
|
||||
(defmacro debug
|
||||
[& params]
|
||||
`(log :level :debug ~@params))
|
||||
|
||||
(defmacro trace
|
||||
[& params]
|
||||
`(log :level :trace ~@params))
|
||||
|
||||
(defn update-thread-context!
|
||||
[data]
|
||||
(run! (fn [[key val]]
|
||||
(ThreadContext/put
|
||||
(name key)
|
||||
(cond
|
||||
(coll? val)
|
||||
(binding [clojure.pprint/*print-right-margin* 120]
|
||||
(with-out-str (pprint val)))
|
||||
(instance? clojure.lang.Named val) (name val)
|
||||
:else (str val))))
|
||||
data))
|
||||
@@ -6,7 +6,7 @@
|
||||
|
||||
(ns app.util.migrations
|
||||
(:require
|
||||
[app.util.logging :as l]
|
||||
[app.common.logging :as l]
|
||||
[clojure.java.io :as io]
|
||||
[clojure.spec.alpha :as s]
|
||||
[next.jdbc :as jdbc]))
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user