Compare commits
2617 Commits
0.1.0
...
1.12.2-bet
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7e8d8eef5a | ||
|
|
e6d6b60b63 | ||
|
|
70beb6c60c | ||
|
|
aa416a782d | ||
|
|
51dd869874 | ||
|
|
5347409804 | ||
|
|
aa6f82c31f | ||
|
|
a0c0ab1871 | ||
|
|
49649a8814 | ||
|
|
18a67a80bc | ||
|
|
867669cc98 | ||
|
|
0158a93391 | ||
|
|
fdb6533149 | ||
|
|
6f32d721c2 | ||
|
|
5f49656e30 | ||
|
|
8114b165d9 | ||
|
|
dd39cb5a1c | ||
|
|
7f8c217e7c | ||
|
|
d731a095c6 | ||
|
|
6630899d6e | ||
|
|
0cfd5095a7 | ||
|
|
4f379821b5 | ||
|
|
9eea7dabc2 | ||
|
|
d1c834e647 | ||
|
|
03a082fe40 | ||
|
|
0534570784 | ||
|
|
f2e389593a | ||
|
|
fae79d67e6 | ||
|
|
271f69d59d | ||
|
|
6563cd9c8b | ||
|
|
e60b8a7aef | ||
|
|
a644599b16 | ||
|
|
5d2715dd32 | ||
|
|
1bad233e2f | ||
|
|
f64b1d3651 | ||
|
|
eb57c2f980 | ||
|
|
ecd491cd09 | ||
|
|
dead3138b3 | ||
|
|
0416082d4d | ||
|
|
05c77d0248 | ||
|
|
2fc4c30bed | ||
|
|
d2590c7651 | ||
|
|
b4c87ad0b9 | ||
|
|
37a35b1827 | ||
|
|
24a0b4445e | ||
|
|
87c1bc4bdb | ||
|
|
e15f5bb432 | ||
|
|
496ba433e9 | ||
|
|
0b0ae756a3 | ||
|
|
0ade0405f5 | ||
|
|
aeed535f1b | ||
|
|
974084a9ca | ||
|
|
88706534c2 | ||
|
|
70def21153 | ||
|
|
46bfb2aacd | ||
|
|
7cf27ac86d | ||
|
|
d24f16563f | ||
|
|
bb68838fa4 | ||
|
|
aed6a8a5ff | ||
|
|
96facc5100 | ||
|
|
6486b24c8b | ||
|
|
75a8f85ebb | ||
|
|
3d8f757712 | ||
|
|
4efd8b7d3f | ||
|
|
5d17933593 | ||
|
|
206778021f | ||
|
|
4a262de550 | ||
|
|
350663b7ce | ||
|
|
f1db0fea03 | ||
|
|
256ed7410f | ||
|
|
09a4cb30ec | ||
|
|
aa3826c389 | ||
|
|
b91042c1e5 | ||
|
|
7eed8c5ee5 | ||
|
|
3207860374 | ||
|
|
b3bb8b6692 | ||
|
|
5b8b13c94c | ||
|
|
e8426006e3 | ||
|
|
116fafd0e1 | ||
|
|
e9fe1800e0 | ||
|
|
82796822d1 | ||
|
|
ce61b783fb | ||
|
|
9b78b2a432 | ||
|
|
321b2c7c23 | ||
|
|
dee397615c | ||
|
|
ef9339f6f1 | ||
|
|
f7f32408fc | ||
|
|
d4e6992442 | ||
|
|
420ece7005 | ||
|
|
741d2b3f3c | ||
|
|
c8bf319b39 | ||
|
|
34df52be5f | ||
|
|
fc2399a885 | ||
|
|
699ec93ca4 | ||
|
|
10598063d1 | ||
|
|
db1e9574cd | ||
|
|
af74a1575b | ||
|
|
03242e1a9c | ||
|
|
dcbd89ff7c | ||
|
|
2312561041 | ||
|
|
b591fbecf0 | ||
|
|
3fbb440436 | ||
|
|
d358185a04 | ||
|
|
8babb59f75 | ||
|
|
3461ec2281 | ||
|
|
3dd94bd362 | ||
|
|
827c2140b7 | ||
|
|
5a5222a97a | ||
|
|
bea3699451 | ||
|
|
93174f54a3 | ||
|
|
e1348725c1 | ||
|
|
528839cde2 | ||
|
|
c5c331ee30 | ||
|
|
69effa37a3 | ||
|
|
4c7a781228 | ||
|
|
62a67bdb94 | ||
|
|
c5c0b36f28 | ||
|
|
0d48c758df | ||
|
|
4856413b24 | ||
|
|
a1586280a9 | ||
|
|
00950b2c97 | ||
|
|
79666bd51a | ||
|
|
ca284a86a3 | ||
|
|
ee5b341d0e | ||
|
|
85cab5031d | ||
|
|
2f7029516b | ||
|
|
a1da4d4233 | ||
|
|
24724e3340 | ||
|
|
048ab9a0fc | ||
|
|
40b005f46e | ||
|
|
ae2a99acb0 | ||
|
|
a81b6db093 | ||
|
|
39b05f5f9f | ||
|
|
979f61df99 | ||
|
|
e665f4e285 | ||
|
|
2c25dfcf1b | ||
|
|
0632028579 | ||
|
|
95b9085258 | ||
|
|
cdc91feb28 | ||
|
|
4caf278da5 | ||
|
|
809a3420c1 | ||
|
|
af8e9058a3 | ||
|
|
2b1c8cafe9 | ||
|
|
1abcd5819b | ||
|
|
76b34bb600 | ||
|
|
67c6a042a0 | ||
|
|
72c2a213b4 | ||
|
|
ec1cc8ec64 | ||
|
|
fbbb079599 | ||
|
|
b8f2f3e34d | ||
|
|
39b29ee3f0 | ||
|
|
5f6cb1e0d7 | ||
|
|
46250e6fab | ||
|
|
fc2a26f249 | ||
|
|
341caa3489 | ||
|
|
38b7474f0b | ||
|
|
c91e2d13c0 | ||
|
|
7134bbf484 | ||
|
|
6b18b258a4 | ||
|
|
86e4826e48 | ||
|
|
6461ebe2b8 | ||
|
|
bfb23ad60b | ||
|
|
637d6a0076 | ||
|
|
cbb8d13570 | ||
|
|
2a6ba79e9a | ||
|
|
1e0dacfe9b | ||
|
|
b194c0c5d8 | ||
|
|
9789b7081a | ||
|
|
03052ddd28 | ||
|
|
779f685f72 | ||
|
|
1dee767762 | ||
|
|
5cac5eb26b | ||
|
|
b26cbeccca | ||
|
|
8d4612c683 | ||
|
|
e352c70013 | ||
|
|
8c3c9a8ca4 | ||
|
|
ada837f7e4 | ||
|
|
1599b2644a | ||
|
|
acc3d00fd5 | ||
|
|
0f459ede50 | ||
|
|
105cb6fa13 | ||
|
|
1797c702a7 | ||
|
|
5f580f10ca | ||
|
|
bd359f42f5 | ||
|
|
34bf73210e | ||
|
|
f1db4aae35 | ||
|
|
7710ffcbf1 | ||
|
|
e9f45a0d0a | ||
|
|
743c2c3385 | ||
|
|
6f714facf9 | ||
|
|
5f81c7bc2d | ||
|
|
72b00fa9af | ||
|
|
449756a0e4 | ||
|
|
75930a0ce9 | ||
|
|
a2c3b0926b | ||
|
|
57666e9173 | ||
|
|
37f4b83d96 | ||
|
|
5576b7568c | ||
|
|
99e067b863 | ||
|
|
5103624fe0 | ||
|
|
26e5d57ced | ||
|
|
b586f2552c | ||
|
|
0fbcec667c | ||
|
|
f40c58c64a | ||
|
|
d66619fe6d | ||
|
|
5c1b007c1b | ||
|
|
86c394f4ce | ||
|
|
90d130a3bc | ||
|
|
f185836fd4 | ||
|
|
4c851856ff | ||
|
|
bc2a0432b9 | ||
|
|
f72e140327 | ||
|
|
a633ed3c9a | ||
|
|
a8a6882708 | ||
|
|
1b76ed97e1 | ||
|
|
04f7169aef | ||
|
|
d83b362c9f | ||
|
|
b1d55348dc | ||
|
|
f8a46c56e9 | ||
|
|
420525cdf0 | ||
|
|
686cacd5ae | ||
|
|
0092806dda | ||
|
|
2f8c63505f | ||
|
|
d892be4971 | ||
|
|
59ed833abc | ||
|
|
110fb2e8db | ||
|
|
9f7a04e330 | ||
|
|
ccbc519c04 | ||
|
|
036860b91b | ||
|
|
7ac2a55315 | ||
|
|
f6cf8d2b1b | ||
|
|
16788d7ab7 | ||
|
|
3142d48f3c | ||
|
|
e1a88ae899 | ||
|
|
a2e80cee47 | ||
|
|
5f14769abc | ||
|
|
406c4063de | ||
|
|
b4bc30e56f | ||
|
|
3482d6c303 | ||
|
|
9dfd5c0bcc | ||
|
|
b2b3de2782 | ||
|
|
50c20e2290 | ||
|
|
a10dcbd918 | ||
|
|
6e0433a34b | ||
|
|
8833e19c7f | ||
|
|
663358bdae | ||
|
|
d9b1c0e2e6 | ||
|
|
39334b81ac | ||
|
|
62f7323acf | ||
|
|
3f89baa1fe | ||
|
|
f0fd1bb40c | ||
|
|
f303d7b33e | ||
|
|
d356a3fa56 | ||
|
|
64e7cad292 | ||
|
|
0766938f98 | ||
|
|
918829ad0a | ||
|
|
540e1fc492 | ||
|
|
ac30754a96 | ||
|
|
b470a0ebbf | ||
|
|
69daee4137 | ||
|
|
3d6c903273 | ||
|
|
bc04a0b9f0 | ||
|
|
bfef94dbfb | ||
|
|
9e06275945 | ||
|
|
6410bcf3c8 | ||
|
|
20baf02726 | ||
|
|
8f6fdf361b | ||
|
|
ffa134f824 | ||
|
|
b4bf6b9235 | ||
|
|
c3e37b0e04 | ||
|
|
374bba763b | ||
|
|
2d00e68b78 | ||
|
|
9a965dc693 | ||
|
|
b96ad5b37f | ||
|
|
07a0f67b32 | ||
|
|
c754a757eb | ||
|
|
dcd53183a8 | ||
|
|
5641132eb9 | ||
|
|
b4c23f3554 | ||
|
|
7385445aa8 | ||
|
|
5409f83167 | ||
|
|
43951aad69 | ||
|
|
9681d8c805 | ||
|
|
ff4d3cfeac | ||
|
|
8e4338c1c9 | ||
|
|
c27d709b6b | ||
|
|
8caa289586 | ||
|
|
f7568f6348 | ||
|
|
6a6f079a84 | ||
|
|
0f04b86316 | ||
|
|
1dae8a0771 | ||
|
|
9bc816fc1c | ||
|
|
11ea4c7aec | ||
|
|
0c53aa158b | ||
|
|
072e4a4f98 | ||
|
|
1b3b3b0ee6 | ||
|
|
d1e4f0de3e | ||
|
|
fd3f304e07 | ||
|
|
9e7551551f | ||
|
|
36bb5cbe01 | ||
|
|
f754c12e8c | ||
|
|
6f5916e334 | ||
|
|
13dd1cb6b6 | ||
|
|
eb4e7e0f0c | ||
|
|
7afb3e2c6d | ||
|
|
9cf5258053 | ||
|
|
56dfdaecb7 | ||
|
|
1d174a4379 | ||
|
|
2aeded1940 | ||
|
|
c23691284c | ||
|
|
f7f6515561 | ||
|
|
438c14d29d | ||
|
|
87351000ae | ||
|
|
0895a69bac | ||
|
|
4285972e41 | ||
|
|
d33542c4dc | ||
|
|
bda97adf4f | ||
|
|
b6f460940f | ||
|
|
aa0e8ed8d6 | ||
|
|
b99fa16b96 | ||
|
|
630d7a3220 | ||
|
|
03c91664cb | ||
|
|
13773d829a | ||
|
|
d9e6e9b017 | ||
|
|
5d8982c734 | ||
|
|
f13c82da2a | ||
|
|
363b0ba997 | ||
|
|
a4c45942c9 | ||
|
|
a86e3a8636 | ||
|
|
db61d579e6 | ||
|
|
e6e3f2cbd5 | ||
|
|
ffdd539233 | ||
|
|
ef17af38a1 | ||
|
|
6dedfaea2f | ||
|
|
cbb3783d84 | ||
|
|
327c095d79 | ||
|
|
88e222420c | ||
|
|
045eec072b | ||
|
|
5f3c381f88 | ||
|
|
6090cf6c68 | ||
|
|
9ac4239c11 | ||
|
|
da2a3b6883 | ||
|
|
b4accaad07 | ||
|
|
edaef0096a | ||
|
|
afba5ff083 | ||
|
|
8b8d614150 | ||
|
|
090dbfda10 | ||
|
|
04f5a6a9f9 | ||
|
|
d8311ac3fa | ||
|
|
9c7f4dfd98 | ||
|
|
8da66e1599 | ||
|
|
2927b0cfc6 | ||
|
|
4663c296cd | ||
|
|
9403f8fd6e | ||
|
|
badb5c6a9b | ||
|
|
e5430259e9 | ||
|
|
50fd44d3f2 | ||
|
|
a8249b73b6 | ||
|
|
a15f867059 | ||
|
|
4216e2e92b | ||
|
|
8ef20be9bd | ||
|
|
6413c9dddd | ||
|
|
eb10f075b9 | ||
|
|
cd55ed7c8d | ||
|
|
2fb96a1b7d | ||
|
|
c48da3d316 | ||
|
|
9488a9a1ad | ||
|
|
2feb22d3bd | ||
|
|
f74569506e | ||
|
|
6633d0b4fb | ||
|
|
6fb35b40d7 | ||
|
|
614d699098 | ||
|
|
8f4fbff40f | ||
|
|
aaf8d2a233 | ||
|
|
0eb2336bc6 | ||
|
|
f9cc9164b3 | ||
|
|
238ec60f89 | ||
|
|
363a82d068 | ||
|
|
4360c1fe4b | ||
|
|
1d575ece06 | ||
|
|
d246788a35 | ||
|
|
e9fa04dd1b | ||
|
|
8e57932966 | ||
|
|
51ea354bcb | ||
|
|
6334520c66 | ||
|
|
6354883a6f | ||
|
|
477f553675 | ||
|
|
1ded4b2b28 | ||
|
|
16c4116c15 | ||
|
|
f5cfbce1c2 | ||
|
|
7bbf98dfb1 | ||
|
|
533cac7881 | ||
|
|
6afc734e91 | ||
|
|
c4fb826d89 | ||
|
|
1321bdeac5 | ||
|
|
e0b7001a09 | ||
|
|
88120b83bd | ||
|
|
a952f7369c | ||
|
|
d4fab3b46c | ||
|
|
06b3499e7d | ||
|
|
fdd66bd513 | ||
|
|
3b5aaf21fa | ||
|
|
59c46833ed | ||
|
|
aee35cb456 | ||
|
|
4a55ee2965 | ||
|
|
4b490e3ca4 | ||
|
|
6727717d1a | ||
|
|
d08891cffa | ||
|
|
799a83ba73 | ||
|
|
261724e555 | ||
|
|
10e7d660ef | ||
|
|
bdfea7cda5 | ||
|
|
fdb1c5e1f9 | ||
|
|
71734df489 | ||
|
|
071b81eadd | ||
|
|
2abe3fde71 | ||
|
|
27e64ccaa8 | ||
|
|
c9185f265c | ||
|
|
79e5716f36 | ||
|
|
9f0e156916 | ||
|
|
d24d45f4cb | ||
|
|
bf55250ae9 | ||
|
|
36016ad9ef | ||
|
|
bf66b81702 | ||
|
|
758ffbf217 | ||
|
|
f24563503a | ||
|
|
a2dbc40571 | ||
|
|
a096b0777f | ||
|
|
87690a534c | ||
|
|
a70e416b0b | ||
|
|
cd1170c543 | ||
|
|
2962dc1faa | ||
|
|
535c1fd007 | ||
|
|
2bd94aff0e | ||
|
|
9ea90c3400 | ||
|
|
0ac5d85117 | ||
|
|
d3a83142ae | ||
|
|
d5886123d8 | ||
|
|
dea090e7d3 | ||
|
|
ba5e345677 | ||
|
|
13ae7b0976 | ||
|
|
39c7bfb49f | ||
|
|
8479a6581d | ||
|
|
e5885e83eb | ||
|
|
914b41fcd4 | ||
|
|
224aa5b89a | ||
|
|
01c89f6554 | ||
|
|
f0e1bc1d59 | ||
|
|
7b487e1bc3 | ||
|
|
c394495a26 | ||
|
|
6dae420254 | ||
|
|
c69d7f50a3 | ||
|
|
e9c654f30d | ||
|
|
ae9b95f81b | ||
|
|
c240b69b5a | ||
|
|
493a7680e0 | ||
|
|
c28a2acfc7 | ||
|
|
60af960f42 | ||
|
|
4c86d5cfe3 | ||
|
|
99a6142134 | ||
|
|
b2211aec59 | ||
|
|
fa09fff2b5 | ||
|
|
0204cdab83 | ||
|
|
445195e9eb | ||
|
|
7f5b0f359c | ||
|
|
d8f4176487 | ||
|
|
220ab22115 | ||
|
|
67776c46d6 | ||
|
|
2d118ecc65 | ||
|
|
4bc2d7444d | ||
|
|
5c6d72b353 | ||
|
|
1839397ebc | ||
|
|
0ee34637f5 | ||
|
|
c6054f7ab2 | ||
|
|
9554dfbc5e | ||
|
|
98d5789b1b | ||
|
|
0cad1a1e7e | ||
|
|
31c07274cd | ||
|
|
37a736339e | ||
|
|
869abcc835 | ||
|
|
a6f05ea8c2 | ||
|
|
6812099900 | ||
|
|
53e6d7ef2a | ||
|
|
c2f604cd01 | ||
|
|
888ffa1bcd | ||
|
|
d06cfed50e | ||
|
|
e06d063946 | ||
|
|
634ec1b113 | ||
|
|
0bf883d5b2 | ||
|
|
c6d0e0124f | ||
|
|
ce115c53e2 | ||
|
|
7014bc7a3c | ||
|
|
219f9c478d | ||
|
|
a9904c6ada | ||
|
|
81cbc33dbb | ||
|
|
24062beebe | ||
|
|
f3548aff8c | ||
|
|
771bb20976 | ||
|
|
8072caeff1 | ||
|
|
d5568fcc25 | ||
|
|
eb1bcfba83 | ||
|
|
a2d3616171 | ||
|
|
a83e37493a | ||
|
|
0feccc9d1c | ||
|
|
e18ecb8c49 | ||
|
|
f5b87a9865 | ||
|
|
3b93434dd3 | ||
|
|
d522096caf | ||
|
|
6c67110dde | ||
|
|
963efc369b | ||
|
|
384f0a05c6 | ||
|
|
a3016b8400 | ||
|
|
0df219c3ad | ||
|
|
a0d527f795 | ||
|
|
e44ea47497 | ||
|
|
9ee5a3159c | ||
|
|
06d41c552b | ||
|
|
7874971550 | ||
|
|
9925716134 | ||
|
|
64c456678b | ||
|
|
16ed09a303 | ||
|
|
1359a1aa7a | ||
|
|
6ae36982b6 | ||
|
|
136d269605 | ||
|
|
932c0ed4ad | ||
|
|
371875440f | ||
|
|
b01a9f2f95 | ||
|
|
0d2def102f | ||
|
|
beff3fe843 | ||
|
|
7a97c94f2b | ||
|
|
ce81908f02 | ||
|
|
f8cecfd61f | ||
|
|
8a2a1d6d70 | ||
|
|
76dafea8a6 | ||
|
|
86bbfde19e | ||
|
|
71d6f7b1a2 | ||
|
|
0c0ab612c0 | ||
|
|
73042115e0 | ||
|
|
0f7166d34a | ||
|
|
f35f2c95f0 | ||
|
|
4d280bdb6d | ||
|
|
47acab766d | ||
|
|
1cc3819e65 | ||
|
|
16fa6259ea | ||
|
|
95717c4c32 | ||
|
|
7564f27f95 | ||
|
|
565046aaa6 | ||
|
|
fb9b023fae | ||
|
|
b05908a760 | ||
|
|
3bbcd235e1 | ||
|
|
9d66984c62 | ||
|
|
9024408ed2 | ||
|
|
2b32e864fd | ||
|
|
626d0cba46 | ||
|
|
2a11e9962d | ||
|
|
7dffddd437 | ||
|
|
6a7600fd52 | ||
|
|
b897f202dd | ||
|
|
eb396f2367 | ||
|
|
95bf3e3af4 | ||
|
|
19944202fb | ||
|
|
2596ad27c3 | ||
|
|
ece914303a | ||
|
|
7a0c12e073 | ||
|
|
14b23b491f | ||
|
|
039b03249b | ||
|
|
3919cf4f86 | ||
|
|
319a9fd2de | ||
|
|
cf1f9f93aa | ||
|
|
0dd805da7f | ||
|
|
e7a1833c44 | ||
|
|
54f8487b46 | ||
|
|
b68d721b39 | ||
|
|
b9ccb4e52c | ||
|
|
c4a11f73a0 | ||
|
|
f96d4198c3 | ||
|
|
fe6a0ec5b8 | ||
|
|
e7b4010eba | ||
|
|
c4947d3737 | ||
|
|
8a8d677f85 | ||
|
|
baf4393310 | ||
|
|
723916d930 | ||
|
|
591d66564d | ||
|
|
79a2d522bf | ||
|
|
4ad34ab5c8 | ||
|
|
33c7847dfc | ||
|
|
7a04f15710 | ||
|
|
7e5b10eb3e | ||
|
|
896a07fa9a | ||
|
|
07e8bb00fb | ||
|
|
5d2742dd37 | ||
|
|
9ae3f1eb68 | ||
|
|
8c6e0cf43a | ||
|
|
1e220fd506 | ||
|
|
4ff7855fd4 | ||
|
|
eb57354109 | ||
|
|
a82a33cecf | ||
|
|
c90fc2a9bf | ||
|
|
c1a40e4aeb | ||
|
|
9999b8bfab | ||
|
|
cf62008acf | ||
|
|
1c959a6653 | ||
|
|
b8043a2432 | ||
|
|
ed5de525aa | ||
|
|
8105d9388b | ||
|
|
8151dcc05f | ||
|
|
25b1c5fe90 | ||
|
|
f566d2a0da | ||
|
|
ea218839e4 | ||
|
|
4c18a1881b | ||
|
|
0be2b2791f | ||
|
|
bf51e3db60 | ||
|
|
abca69f408 | ||
|
|
6eac9102c9 | ||
|
|
0a7da1b7f2 | ||
|
|
b4361cb202 | ||
|
|
d2d4090e27 | ||
|
|
583eb53c9d | ||
|
|
39246f2beb | ||
|
|
cd2d3d5fa3 | ||
|
|
589e646023 | ||
|
|
b7ba3098ae | ||
|
|
631c5ecae3 | ||
|
|
4962e45bd9 | ||
|
|
c57219a356 | ||
|
|
03e6a187c5 | ||
|
|
0bdbbd35e3 | ||
|
|
401afe7c1a | ||
|
|
66b0039566 | ||
|
|
17da51440c | ||
|
|
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 | ||
|
|
40326177fd | ||
|
|
4ab0272fa6 | ||
|
|
fb33366c91 | ||
|
|
75352c9afe | ||
|
|
a0f98e3823 | ||
|
|
bff6768adf | ||
|
|
8ce2eb448c | ||
|
|
7c5d00f8a4 | ||
|
|
30cd499014 | ||
|
|
99d173789e | ||
|
|
ae72db8129 | ||
|
|
9437cc1806 | ||
|
|
0e76aa0265 | ||
|
|
756e654d32 | ||
|
|
78d1c57b7c | ||
|
|
bb27405e8f | ||
|
|
0cfc46b417 | ||
|
|
bfb30fe68d | ||
|
|
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 | ||
|
|
2f8f1f0b9a | ||
|
|
d572fdac9b | ||
|
|
ac41ed1af4 | ||
|
|
f47bb6bcd0 | ||
|
|
a3eb5e2928 | ||
|
|
53cb36dd8a | ||
|
|
9cda361523 | ||
|
|
1a70071405 | ||
|
|
b648fb7446 | ||
|
|
aaef0777b0 | ||
|
|
68d287ed82 | ||
|
|
641e4080bc | ||
|
|
a80120278e | ||
|
|
d4bf3ef6fd | ||
|
|
ca5c374ecd | ||
|
|
69ea8229ca | ||
|
|
4d19b87fff | ||
|
|
8847047fd1 | ||
|
|
6e8a5015c9 | ||
|
|
e8919ee340 | ||
|
|
f8f506a8be | ||
|
|
74756db7e6 | ||
|
|
96d9e101cc | ||
|
|
7eb3693804 | ||
|
|
cad2b831ed | ||
|
|
b2dc849e52 | ||
|
|
6489ad4114 | ||
|
|
0de8bfeba6 | ||
|
|
6710d99878 | ||
|
|
7a32d902ec | ||
|
|
52f699c175 | ||
|
|
ba211e3cbd | ||
|
|
897f41bc7a | ||
|
|
2834850337 | ||
|
|
67cd877281 | ||
|
|
6e18bc9e04 | ||
|
|
6d0b36e9b9 | ||
|
|
bd8aa8163d | ||
|
|
febaec1b1e | ||
|
|
2ac790693a | ||
|
|
08dce3bcdc | ||
|
|
806dc78d2b | ||
|
|
e5d4755619 | ||
|
|
c44befb957 | ||
|
|
871e849660 | ||
|
|
43b34aa279 | ||
|
|
6b1e5b4169 | ||
|
|
952bcd853e | ||
|
|
77446a71e2 | ||
|
|
d722f37468 | ||
|
|
9757836067 | ||
|
|
7d80a5a7f7 | ||
|
|
a9e8115088 | ||
|
|
f92dc6f4b4 | ||
|
|
e43ab51b7d | ||
|
|
6a68e9c118 | ||
|
|
95cb6d132b | ||
|
|
ed95b59003 | ||
|
|
5730769a19 | ||
|
|
2a67008531 | ||
|
|
651230d40f | ||
|
|
28c5fd4583 | ||
|
|
944e7c6e3d | ||
|
|
3094fe2855 | ||
|
|
deb0ee3d29 | ||
|
|
23076727c7 | ||
|
|
42072f2584 | ||
|
|
b50ffa087d | ||
|
|
03b74b582e | ||
|
|
4af5341f81 | ||
|
|
77ab0706be | ||
|
|
1d6094e893 | ||
|
|
af29ca92cc | ||
|
|
c83bfe0b16 | ||
|
|
891ce8a33d | ||
|
|
c356e64be5 | ||
|
|
245f7256e1 | ||
|
|
e0a0b82958 | ||
|
|
2b4a78ea28 | ||
|
|
33a1e29a0c | ||
|
|
8a76d8322f | ||
|
|
1ff9b24818 | ||
|
|
4613aef1c8 | ||
|
|
7ff608ff0b | ||
|
|
87aa4622b4 | ||
|
|
188126a895 | ||
|
|
f57fb5006d | ||
|
|
6c1e13b6e5 | ||
|
|
344622b1c1 | ||
|
|
20b8269766 | ||
|
|
810f868b67 | ||
|
|
9c99ec3410 | ||
|
|
2ea200be78 | ||
|
|
8831f3241c | ||
|
|
3752322c01 | ||
|
|
fa87187849 | ||
|
|
662f87080c | ||
|
|
6003591ecd | ||
|
|
c618317a76 | ||
|
|
5d689551e3 | ||
|
|
c9e7be28af | ||
|
|
346fb8fb11 | ||
|
|
3fdcea78e4 | ||
|
|
fb2d1e7953 | ||
|
|
ce19bcd364 | ||
|
|
610afc7702 | ||
|
|
6557792a98 | ||
|
|
a3e464aea3 | ||
|
|
087f2aee09 | ||
|
|
88d8431985 | ||
|
|
ea22f3f81c | ||
|
|
93d8c171be | ||
|
|
b2e01cd52b | ||
|
|
9afe499075 | ||
|
|
91fe0b0985 | ||
|
|
90aab92a59 | ||
|
|
d613d00bca | ||
|
|
c15c277b03 | ||
|
|
a86c4a8309 | ||
|
|
4b7f82a9d9 | ||
|
|
c33c3fb2fa | ||
|
|
07f3d48a9d | ||
|
|
f5a6159e1d | ||
|
|
3656ab977b | ||
|
|
891506ab52 | ||
|
|
37f9a5d9f2 | ||
|
|
958c5ebcc6 | ||
|
|
b8afdda856 | ||
|
|
2c250a2740 | ||
|
|
512b66cb04 | ||
|
|
a11cec9fdc | ||
|
|
81e5a8c925 | ||
|
|
a12f369bda | ||
|
|
ec2f88ebc0 | ||
|
|
c449492a33 | ||
|
|
5614aceaa8 | ||
|
|
d6e7dfc648 | ||
|
|
b84222e171 | ||
|
|
8e785e62e3 | ||
|
|
4977c22b08 | ||
|
|
5c0bc1cf84 | ||
|
|
ddbaee228a | ||
|
|
c858707c39 | ||
|
|
83bca7fb10 | ||
|
|
7d19518ba8 | ||
|
|
9775b79a0b | ||
|
|
e1dfd91e24 | ||
|
|
b4351208cc | ||
|
|
ae1e9a861b | ||
|
|
ab799c83ee | ||
|
|
4118e53d7d | ||
|
|
384b464f0f | ||
|
|
ecacd47523 | ||
|
|
334ac26f0d | ||
|
|
e94e202cef | ||
|
|
7cf120e2e1 | ||
|
|
0f8e2a9b1b | ||
|
|
c70bc5baff | ||
|
|
e7b3f12b71 | ||
|
|
a03882de76 | ||
|
|
d9a4a8d6de | ||
|
|
4c48f34d61 | ||
|
|
ebb6df4696 | ||
|
|
7033ae4f2e | ||
|
|
0cc600de6d | ||
|
|
c1278194ce | ||
|
|
50bdcea81b | ||
|
|
c5fa8f560c | ||
|
|
6d5276c0c6 | ||
|
|
4405bd95f9 | ||
|
|
3bb3fcfbda | ||
|
|
5e0101e424 | ||
|
|
2c96ecac87 | ||
|
|
9fcddc37f6 | ||
|
|
1fd2b3fff8 | ||
|
|
39066bfee3 | ||
|
|
2d75efbace | ||
|
|
8a8403834f | ||
|
|
e98b88f673 | ||
|
|
d2f8d4a306 | ||
|
|
2138530f3e | ||
|
|
94d94684c8 | ||
|
|
550164cf5e | ||
|
|
5352918ff8 | ||
|
|
57b6807333 | ||
|
|
e3171d9ee5 | ||
|
|
8ef49d2ec4 | ||
|
|
3ce4769e8d | ||
|
|
abb244c940 | ||
|
|
4825efb582 | ||
|
|
2195b8932e | ||
|
|
81c406bb60 | ||
|
|
9d28807796 | ||
|
|
6dbabf2935 | ||
|
|
4018e4df79 | ||
|
|
8835216ca9 | ||
|
|
04ab99c8ad | ||
|
|
1bc210c9a9 | ||
|
|
6250b457ad | ||
|
|
460c824117 | ||
|
|
77c2a98304 | ||
|
|
8ad8196d70 | ||
|
|
fa4410bea3 | ||
|
|
af23d62568 | ||
|
|
e241273a1e | ||
|
|
269efc98c3 | ||
|
|
2c3a3845ac | ||
|
|
4bf0ae0a9d | ||
|
|
a3ead3aa6d | ||
|
|
d965736751 | ||
|
|
437a6cf476 | ||
|
|
a91a57581f | ||
|
|
be0d1c19fa | ||
|
|
447e1bf435 | ||
|
|
6a62f4d3fb | ||
|
|
f507722f43 | ||
|
|
0030b9c3ac | ||
|
|
4db7a6782b | ||
|
|
bb3be3d495 | ||
|
|
fe28648a88 | ||
|
|
2b393355ad | ||
|
|
79f5c6a008 | ||
|
|
f75ee48795 | ||
|
|
81d9727d03 | ||
|
|
4ac3573ab1 | ||
|
|
92b79f1731 | ||
|
|
32b623e82b | ||
|
|
285a0d5f47 | ||
|
|
308fd8d4b0 | ||
|
|
4000855f45 | ||
|
|
ca777790d4 | ||
|
|
e15a212b14 | ||
|
|
8c6863e2ad | ||
|
|
5e329e62b3 | ||
|
|
2582e87ffa | ||
|
|
1c0822ffb3 | ||
|
|
9d0877e985 | ||
|
|
a6fb4a8271 | ||
|
|
9adf0b3611 | ||
|
|
e3896da3c4 | ||
|
|
f5ad7dc2dc | ||
|
|
d0af14c40f | ||
|
|
d8fb575d46 | ||
|
|
aaf0618d24 | ||
|
|
92d1dcb3d4 | ||
|
|
a9e93a5ace | ||
|
|
e9ae59ad00 | ||
|
|
056b80939e | ||
|
|
3a4f63848d | ||
|
|
907f39c73f | ||
|
|
91f60000b3 | ||
|
|
0b6c2df5b6 | ||
|
|
ac27d35ff5 | ||
|
|
c62905b9a8 | ||
|
|
2974fb0f4e | ||
|
|
df73df311b | ||
|
|
b0575e969f | ||
|
|
547a472016 | ||
|
|
d67dd21c56 | ||
|
|
59187f9ff4 | ||
|
|
da8a32047c | ||
|
|
4c93ef4bb3 | ||
|
|
e9b8295bf1 | ||
|
|
14475fdc67 | ||
|
|
21cf845c02 | ||
|
|
2cea7405b5 | ||
|
|
dff067c1a7 | ||
|
|
a507ab0e07 | ||
|
|
1ee1cada9e | ||
|
|
1584f3771b | ||
|
|
1ec423c11d | ||
|
|
c3611c3047 | ||
|
|
41e57bcb6b | ||
|
|
057b0e163c | ||
|
|
3840e4c214 | ||
|
|
715900d0ef | ||
|
|
e9cbfbe7f8 | ||
|
|
a14d8e2b41 | ||
|
|
d57f4cebff | ||
|
|
cbe54d0bbe | ||
|
|
2034f0a7c2 | ||
|
|
ce5429a531 | ||
|
|
df11ef4aca | ||
|
|
8ecc0b3cd9 | ||
|
|
5d2f4bac76 | ||
|
|
bb73ddc58f | ||
|
|
0f91f02508 | ||
|
|
ce072937e4 | ||
|
|
3b5b25fc86 | ||
|
|
170ab9e93b | ||
|
|
a67370bb83 | ||
|
|
42cc97f86b | ||
|
|
e3440ad773 | ||
|
|
5e73e68ef7 | ||
|
|
c16434e608 | ||
|
|
9e39e53488 | ||
|
|
1a7b098282 | ||
|
|
2184286a78 | ||
|
|
3583eb6aa9 | ||
|
|
adff40a4e7 | ||
|
|
f7064c5c0e | ||
|
|
208b6515a4 | ||
|
|
66e54f7bd2 | ||
|
|
d0baf76599 | ||
|
|
1757db3051 | ||
|
|
c7cfca8437 | ||
|
|
3efb50b103 | ||
|
|
be5c382ace | ||
|
|
d06f08e156 | ||
|
|
43f7750658 | ||
|
|
65ad46ab38 | ||
|
|
432d24dc94 | ||
|
|
6331dff484 | ||
|
|
961a7a2e03 | ||
|
|
c7683dfd80 | ||
|
|
de11e85d2b | ||
|
|
0455aaa4cd | ||
|
|
e2cf3a5a98 | ||
|
|
f7712f2b40 | ||
|
|
41bf436c3a | ||
|
|
9aee88f9f1 | ||
|
|
94a3c5853b | ||
|
|
55ea84a056 | ||
|
|
2828ccda7f | ||
|
|
465a25145d | ||
|
|
d64eaab0b9 | ||
|
|
fad9d2fd3a | ||
|
|
dd92e5d773 | ||
|
|
2b35dce037 | ||
|
|
a777e8e42a | ||
|
|
88eb6abdd6 | ||
|
|
e4d4245b6c | ||
|
|
874378869d | ||
|
|
dd6bd6bbff | ||
|
|
d946aceacb | ||
|
|
e3691cc0e3 | ||
|
|
db7518025d | ||
|
|
ac4bfc9bac | ||
|
|
63b95e71a7 | ||
|
|
9e5923004f | ||
|
|
d01eb30ef2 | ||
|
|
b585c2ac22 | ||
|
|
74a09301a7 | ||
|
|
07799d9b01 | ||
|
|
48ba80c6e2 | ||
|
|
74f99f0d48 | ||
|
|
f396ef4fa0 | ||
|
|
de8207c5a6 | ||
|
|
5f114163dc | ||
|
|
5ce2bc862c | ||
|
|
6db144e5ed | ||
|
|
fc383664c7 | ||
|
|
bc3640893c | ||
|
|
5361e42976 | ||
|
|
e81b1b8115 | ||
|
|
421b30c1d8 | ||
|
|
2e6dacf539 | ||
|
|
c22b4a1de2 | ||
|
|
a06a8c648e | ||
|
|
bb719d6211 | ||
|
|
5a49ce2028 | ||
|
|
e8da04d4ab | ||
|
|
112e656f40 | ||
|
|
77a2fd6e36 | ||
|
|
3613e6f3d3 | ||
|
|
eff333cbaf | ||
|
|
ba0f9360f9 | ||
|
|
a8565dc2c2 | ||
|
|
9f5c19244d | ||
|
|
7cc4873dd4 | ||
|
|
03a031091f | ||
|
|
14359d9acf | ||
|
|
bfbc715977 | ||
|
|
6161911ff1 | ||
|
|
162b0cfa6c | ||
|
|
94ccc013d7 | ||
|
|
239ec12529 | ||
|
|
99bcf0484a | ||
|
|
6e80a2f9fb | ||
|
|
4cca8f0600 | ||
|
|
b9ca4e7f9b | ||
|
|
464a686c04 | ||
|
|
f0439da293 | ||
|
|
f545e41d10 | ||
|
|
7d14aef393 | ||
|
|
9a0f6018a7 | ||
|
|
0a44dbd921 | ||
|
|
fa2d0f5ed7 | ||
|
|
d889d39151 | ||
|
|
8daf6e822e | ||
|
|
c40d9d9a7c | ||
|
|
e12a6e65a6 | ||
|
|
a92820e910 | ||
|
|
080dd88509 | ||
|
|
44d64e4831 | ||
|
|
6f2306439c | ||
|
|
2c6b896989 | ||
|
|
4e1d85a5f4 | ||
|
|
09aa28a943 | ||
|
|
faff32203c | ||
|
|
77280961ef | ||
|
|
5f7f88d299 | ||
|
|
50bc1b0347 | ||
|
|
166fdbd406 | ||
|
|
a6920122e6 | ||
|
|
e677692594 | ||
|
|
459c9a3bb1 | ||
|
|
9544ee2140 | ||
|
|
45cd05184b | ||
|
|
e8aa521a1e | ||
|
|
c4c0e105cf | ||
|
|
69031bb8e1 | ||
|
|
19ced21b20 | ||
|
|
46b55822dc | ||
|
|
4f20d22a4f | ||
|
|
8f51450f7e | ||
|
|
94a294e147 | ||
|
|
4acfc15705 | ||
|
|
73b555eb9b | ||
|
|
d93fa72e48 | ||
|
|
bbb26002e4 | ||
|
|
1ab1059b06 | ||
|
|
7b67e05e50 | ||
|
|
c7fcb00b81 | ||
|
|
2b66d0ea06 | ||
|
|
f3b779e50c | ||
|
|
9e348ecc99 | ||
|
|
78fe0ab7e3 | ||
|
|
fc9f2864d8 | ||
|
|
1e642bba8f | ||
|
|
a5994140e2 | ||
|
|
018b47ab6b | ||
|
|
f4f51dbf6b | ||
|
|
43e75401d7 | ||
|
|
8b45ed9c0c | ||
|
|
88a3548d7e | ||
|
|
3ddc95d4b5 | ||
|
|
08a682efc2 | ||
|
|
32afe57e18 | ||
|
|
43465f7c4b | ||
|
|
351daacca0 | ||
|
|
0926fbcbc6 | ||
|
|
59a45530a8 | ||
|
|
cf2998eeec | ||
|
|
6f1508acc1 | ||
|
|
edb88027a4 | ||
|
|
5111551c89 | ||
|
|
6891826c78 | ||
|
|
e68d63ea71 | ||
|
|
d83d241c39 | ||
|
|
9950f464ce | ||
|
|
676a2db1f5 | ||
|
|
62ed2221e9 | ||
|
|
60f6093357 | ||
|
|
ed893b995d | ||
|
|
3ebc94ab8e | ||
|
|
cd7ad03cf0 | ||
|
|
0f6ce233bd | ||
|
|
a14890f163 | ||
|
|
213a8c69fb | ||
|
|
2500486186 | ||
|
|
9cd15fd362 | ||
|
|
efdfbbaf5e | ||
|
|
87aa3fbfe8 | ||
|
|
ea3f2fbfce | ||
|
|
7d68d79fc3 | ||
|
|
6f6a750373 | ||
|
|
993530dbcb | ||
|
|
7b4ca6dcef | ||
|
|
ae1d5667cc | ||
|
|
90a51dc44a | ||
|
|
caf1ef653f | ||
|
|
8cba56b2d5 | ||
|
|
2fed88e840 | ||
|
|
1df407ca96 | ||
|
|
f4c3aa8b89 | ||
|
|
cc92e4be75 | ||
|
|
aa866bbe13 | ||
|
|
18ec8009a1 | ||
|
|
c6d7f0e352 | ||
|
|
038e820815 | ||
|
|
605143ef7e | ||
|
|
3cb9470db2 | ||
|
|
7c21624e09 | ||
|
|
47c58df2a4 | ||
|
|
e6a2cc16a4 | ||
|
|
66f9e98499 | ||
|
|
4f8d82dae7 | ||
|
|
66f88576e1 | ||
|
|
3c2ae03cea | ||
|
|
a2d8518724 | ||
|
|
20e4562c09 | ||
|
|
68eadcb24f | ||
|
|
0a3b244f44 | ||
|
|
19ea7e8b2f | ||
|
|
c447279c75 | ||
|
|
b1477d8087 | ||
|
|
7f7c803d9e | ||
|
|
41b5374027 | ||
|
|
27d28f7baf | ||
|
|
50aef6ab65 | ||
|
|
ecff4c5dce | ||
|
|
c380400578 | ||
|
|
92e07c3b54 | ||
|
|
0756de25f8 | ||
|
|
1773de88f5 | ||
|
|
b534f5b736 | ||
|
|
727d6b78ce | ||
|
|
2dbcb4c2a2 | ||
|
|
a399363b08 | ||
|
|
7ac78cb103 | ||
|
|
ec217d8201 | ||
|
|
013fc2fc9c | ||
|
|
8cf2d4f3a4 | ||
|
|
ebcb820335 | ||
|
|
43963fa09b | ||
|
|
b17067b8da | ||
|
|
66f92405e2 | ||
|
|
288c5c7fc4 | ||
|
|
6383dc0952 | ||
|
|
0008a2aa48 | ||
|
|
d7d56db1af | ||
|
|
60f9b47115 | ||
|
|
4729801fca | ||
|
|
136a48a18f | ||
|
|
5c31830edb | ||
|
|
17ab753c2b | ||
|
|
422f4ee6c2 | ||
|
|
a988292253 | ||
|
|
dcb913d9fa | ||
|
|
d2d1eed68a | ||
|
|
e7085571bf | ||
|
|
28691e2bf2 | ||
|
|
e15d93e8a4 | ||
|
|
a16f4393b9 | ||
|
|
3681c17f4b | ||
|
|
abcd92a6b1 | ||
|
|
dd4930e055 | ||
|
|
4a58a429d4 | ||
|
|
142086b2c3 | ||
|
|
1e25e543b3 | ||
|
|
a1e75c6e03 | ||
|
|
ca2612937e | ||
|
|
1e6673f6b6 | ||
|
|
6d821660c9 | ||
|
|
67138ac629 | ||
|
|
b816e0ed32 | ||
|
|
d9aa94025a | ||
|
|
b464181213 | ||
|
|
ef901dbd5e | ||
|
|
0cb816c16d | ||
|
|
4d3142e826 | ||
|
|
fc29e8fb6b | ||
|
|
fb36ab0e41 | ||
|
|
aa83f1bbd3 | ||
|
|
7bc91e7224 | ||
|
|
ca52f4f8ea | ||
|
|
ede42e42b1 | ||
|
|
f0087e11b0 | ||
|
|
5519cdfd7c | ||
|
|
68e3566b8b | ||
|
|
13131a0226 | ||
|
|
92254a175e | ||
|
|
48747d9553 | ||
|
|
fde6126ac6 | ||
|
|
7db82a6af1 | ||
|
|
7709d219a9 | ||
|
|
3bef80932d | ||
|
|
439e5ee6a1 | ||
|
|
2de1c92ee8 | ||
|
|
f7d0383919 | ||
|
|
84b7a2de0b | ||
|
|
797ba3ef9b | ||
|
|
374653d9f6 | ||
|
|
22c8a2b538 | ||
|
|
47320330ad | ||
|
|
4b2a4c8fa3 | ||
|
|
c2332331ce | ||
|
|
81a604dca2 | ||
|
|
b547f1cd7e | ||
|
|
931deec6bd | ||
|
|
aed94c8e91 | ||
|
|
277ff12822 | ||
|
|
3329216e3c | ||
|
|
c12cbbca2e | ||
|
|
172372d4c0 | ||
|
|
6d427cdc9c | ||
|
|
e1df3efd6e | ||
|
|
2a4849cf8f | ||
|
|
33a2f8d788 | ||
|
|
fee99a081b | ||
|
|
d263dd52e9 | ||
|
|
47e0c2c75b | ||
|
|
5b25a42f32 | ||
|
|
d64dd29ca9 | ||
|
|
6e1e3772b9 | ||
|
|
b0336e1f7b | ||
|
|
ed3d571793 | ||
|
|
3d043adb03 | ||
|
|
7f624b5c61 | ||
|
|
d70910fc0d | ||
|
|
69ac552881 | ||
|
|
275438673d | ||
|
|
404e2bf0b3 | ||
|
|
708ba3d7ac | ||
|
|
9e47318e09 | ||
|
|
f0eaf9aa20 | ||
|
|
cf78861396 | ||
|
|
517751c116 | ||
|
|
2fd6344c44 | ||
|
|
1be993f8b1 | ||
|
|
942c62bf1d | ||
|
|
31fa4a8c8b | ||
|
|
d39694af59 | ||
|
|
68d8a49466 | ||
|
|
99d9d77c63 | ||
|
|
4da3270d34 | ||
|
|
6f07b4ea80 | ||
|
|
29f421d867 | ||
|
|
40ddcb89fc | ||
|
|
e75284ce97 | ||
|
|
7482122964 | ||
|
|
d3345c0fa6 | ||
|
|
237ef2a205 | ||
|
|
0f7596bacf | ||
|
|
6e88d3a04c | ||
|
|
59022904fb | ||
|
|
94c5004c33 | ||
|
|
23d531a664 | ||
|
|
19febde547 | ||
|
|
741d67c30b | ||
|
|
507f3c06e7 | ||
|
|
c16a24a59a | ||
|
|
e446f47e2c | ||
|
|
146394f3ca | ||
|
|
9d7214702f | ||
|
|
861d5f0064 | ||
|
|
34c4f23e49 | ||
|
|
48a094d22d | ||
|
|
f143197f1f | ||
|
|
fdeaac7f65 | ||
|
|
cbb68acd75 | ||
|
|
31165c4ce6 | ||
|
|
77ed530de7 | ||
|
|
f509d9acd0 | ||
|
|
6c47df20af | ||
|
|
dca402eb18 | ||
|
|
fbfe792a93 | ||
|
|
868f18fd21 | ||
|
|
5ae823b25c | ||
|
|
2de16985d3 | ||
|
|
2ca8ff4db1 | ||
|
|
ee6717ef69 | ||
|
|
7c2f0ed7b9 | ||
|
|
161b8cdabb | ||
|
|
1f7ddc081a | ||
|
|
df0dcc587f | ||
|
|
e1ae80583f | ||
|
|
2adc45fc19 | ||
|
|
2e92757df6 | ||
|
|
c6765a48c5 | ||
|
|
6a345c4b8a | ||
|
|
044f1f63c0 | ||
|
|
9945243a23 | ||
|
|
d1261fc841 | ||
|
|
e87dc6d34c | ||
|
|
70cba4bbdf | ||
|
|
93311b8b98 | ||
|
|
3b9201ed0e | ||
|
|
044aef8414 | ||
|
|
3234b19790 | ||
|
|
f14325d12b | ||
|
|
db49c54681 | ||
|
|
c54d9b777d | ||
|
|
52a3cd6ae4 | ||
|
|
9b8d73ef86 | ||
|
|
f12f46981b | ||
|
|
9fb8ba2ff1 | ||
|
|
fe114d2e66 | ||
|
|
56ed474d8c | ||
|
|
a595effbe9 | ||
|
|
ee8c430d85 | ||
|
|
0683c4a963 | ||
|
|
833944bebb | ||
|
|
2a8a0afd09 | ||
|
|
61ad112451 | ||
|
|
129cc86e3b | ||
|
|
645954bc7c | ||
|
|
ecd020eec2 | ||
|
|
8fb5dbb980 | ||
|
|
cef0353642 | ||
|
|
e3727aaefe | ||
|
|
85781c5b7f | ||
|
|
62784d0708 | ||
|
|
48a62ddd2b | ||
|
|
04af15cba5 | ||
|
|
65a3126f15 | ||
|
|
82d7a0163d | ||
|
|
5b200fd6a2 | ||
|
|
b79c986fc9 | ||
|
|
8f4e13072c | ||
|
|
d517daa045 | ||
|
|
3171d9d64d | ||
|
|
0ea2951515 | ||
|
|
0612e71166 | ||
|
|
f9b24bd01c | ||
|
|
65eb8e7c43 | ||
|
|
c7795640e1 | ||
|
|
2eea63dd1a | ||
|
|
7e1ee087d3 | ||
|
|
8fd37dbad5 | ||
|
|
2e68d41dcc | ||
|
|
1eddc9de33 | ||
|
|
ca1a97a52e | ||
|
|
b14c98b76e | ||
|
|
d89bf772a6 | ||
|
|
688d649c4a | ||
|
|
002a6f1e52 | ||
|
|
4a61eba3b9 | ||
|
|
e1161037a5 | ||
|
|
6e840a439e | ||
|
|
29addbe987 | ||
|
|
19f098359b | ||
|
|
5ce450f578 | ||
|
|
fb51580740 | ||
|
|
995017df5a | ||
|
|
c79036aa65 | ||
|
|
fbe2e2a285 | ||
|
|
a63f28a2e5 | ||
|
|
5e2bb3f546 | ||
|
|
60232baffb | ||
|
|
c38117d116 | ||
|
|
d56b758490 | ||
|
|
de394a7d4e | ||
|
|
55b1417df8 | ||
|
|
471cad3ae9 | ||
|
|
299b29b66f | ||
|
|
344a7dfbaa | ||
|
|
56c204509a | ||
|
|
f7ecd4880f | ||
|
|
b2f8a843b5 | ||
|
|
1d01ac72ba | ||
|
|
1ad1f3eb33 | ||
|
|
e3bad997fd | ||
|
|
800f97c5a1 | ||
|
|
abb8d8502b | ||
|
|
dc69d0c7f4 | ||
|
|
56b10d669a | ||
|
|
4991cae5ad | ||
|
|
784a4f8ecd | ||
|
|
2e084cc2a6 | ||
|
|
0f35906930 | ||
|
|
e96d2336cf | ||
|
|
803caf6531 | ||
|
|
cfa47cc7b9 | ||
|
|
043c038dae | ||
|
|
41aede2b50 | ||
|
|
0014bb3d24 | ||
|
|
94405ab72d | ||
|
|
0f9b2923c2 | ||
|
|
60f4f863df | ||
|
|
c1476d0397 | ||
|
|
90d7efe3a9 | ||
|
|
136d00797c | ||
|
|
101027e6b8 | ||
|
|
23f95c2b2b | ||
|
|
baaeb20d6b | ||
|
|
cd313dc2fe | ||
|
|
d86dc608b0 | ||
|
|
6c2b5ff0c7 | ||
|
|
fcda3b557e | ||
|
|
d8104f0d22 | ||
|
|
964dad0d5b | ||
|
|
30819a08f4 | ||
|
|
22b8eb856e | ||
|
|
f8ccd0b120 | ||
|
|
0c0f26bb18 | ||
|
|
9c0dc54cfe | ||
|
|
fb0c1f548b | ||
|
|
7708752ad9 | ||
|
|
9d49d781cc | ||
|
|
a81d20a2d1 | ||
|
|
17229228a3 | ||
|
|
fc619f975c | ||
|
|
5858f3f180 | ||
|
|
d5ff5ea91e | ||
|
|
cf465d93f9 | ||
|
|
521ccc25cf | ||
|
|
dc0765f6b0 | ||
|
|
8cfc2ec21a | ||
|
|
10cad69fac | ||
|
|
b7d3158514 | ||
|
|
4b8334fe1c | ||
|
|
608b5cc9f9 | ||
|
|
42a55015fa | ||
|
|
0a6e0d0f2c | ||
|
|
7846682223 | ||
|
|
5336bbbe65 | ||
|
|
8e5fd5892e | ||
|
|
eaff888486 | ||
|
|
f1383f4dca | ||
|
|
d9c10cea5d | ||
|
|
d48a1ca0b0 | ||
|
|
bfcfe2fd31 | ||
|
|
648c088d02 | ||
|
|
70258e0eee | ||
|
|
5b1e9ec7da | ||
|
|
7a250a170e | ||
|
|
2e438385f3 | ||
|
|
d6f3efb358 | ||
|
|
884410c0d8 | ||
|
|
cdab9ff69c | ||
|
|
1da43bb5b5 | ||
|
|
6f3a08be0c | ||
|
|
e5cb6ebec7 | ||
|
|
f60ad9e559 | ||
|
|
69b23e4000 | ||
|
|
bedfb9a1ee | ||
|
|
e4fb802d7a | ||
|
|
068a099f37 | ||
|
|
fa573f8a24 | ||
|
|
ebb745cc11 | ||
|
|
2b33300d79 | ||
|
|
946d40e6cd | ||
|
|
36285a65d2 | ||
|
|
fc49674997 | ||
|
|
d0a8647186 | ||
|
|
9b875aba21 | ||
|
|
76e43f339a | ||
|
|
32e832eb39 | ||
|
|
60704bca17 | ||
|
|
43e4712b86 | ||
|
|
5359c3a7ed | ||
|
|
81bf68c67c | ||
|
|
4d5231598f | ||
|
|
c1a139fc51 | ||
|
|
1cb18ad7cb | ||
|
|
6f0258c8d4 | ||
|
|
124efc0d88 | ||
|
|
924ecd998f | ||
|
|
07a94de607 | ||
|
|
7bd05d63ac | ||
|
|
bb15924c95 | ||
|
|
1ebce37e17 | ||
|
|
b93dc752fe | ||
|
|
dbbe1f7df2 | ||
|
|
a709c47f6f | ||
|
|
68ed30ff35 | ||
|
|
a65a31810c | ||
|
|
8c50dc0c72 | ||
|
|
a8a036206b | ||
|
|
8313f1d96e | ||
|
|
1898ed215e | ||
|
|
83aceba913 | ||
|
|
c56fb0ea47 | ||
|
|
83a2df3ef3 | ||
|
|
4703f6d5c7 | ||
|
|
8d2797f8a1 | ||
|
|
6cdde84445 | ||
|
|
afa35379b2 | ||
|
|
1099e08b7d | ||
|
|
89cb20ada7 | ||
|
|
32b0fd7b36 | ||
|
|
04670bb5f2 | ||
|
|
8566fe4ac1 | ||
|
|
e607e8315c | ||
|
|
a9b7cf61a5 | ||
|
|
7c7bda669c | ||
|
|
0c82c6f2f5 | ||
|
|
b7cbe49cb2 | ||
|
|
7378089f4a | ||
|
|
62b6b12066 | ||
|
|
39fdff9052 | ||
|
|
32c0913f00 | ||
|
|
7eb90d62b0 | ||
|
|
ec2683417f | ||
|
|
cb23c8b093 | ||
|
|
687f7ddf64 | ||
|
|
992a8e9aef | ||
|
|
6e08c6bc35 | ||
|
|
b71d05935a | ||
|
|
c14dbc19f8 | ||
|
|
1eff1c94c4 | ||
|
|
53be7feee1 | ||
|
|
e182cc4028 | ||
|
|
80309cbff3 | ||
|
|
816db29f9c | ||
|
|
526e0afc70 | ||
|
|
77973af49f | ||
|
|
dc5cff645a | ||
|
|
0ea8e9e750 | ||
|
|
69b4968578 | ||
|
|
b7e266e350 | ||
|
|
b056cc35e4 | ||
|
|
d66452423f | ||
|
|
d85537fa7b | ||
|
|
fc11fb6e3d | ||
|
|
cbdfb4349b | ||
|
|
19ed0b70c2 | ||
|
|
3092747b5f | ||
|
|
0adfc2ddab | ||
|
|
8fd8bc4537 | ||
|
|
e7d6a54907 | ||
|
|
e3c273c84b | ||
|
|
8aedbd1418 | ||
|
|
e713c30785 | ||
|
|
74a168d87e | ||
|
|
ca63ff621a | ||
|
|
d120af2c81 | ||
|
|
95ab5b57b7 | ||
|
|
2e7f90f3cc | ||
|
|
8403352af8 | ||
|
|
526b6e1f03 | ||
|
|
f2fd976934 | ||
|
|
8b9371d7e1 | ||
|
|
948a4038c6 | ||
|
|
57c366ec9a | ||
|
|
3c65f9fe91 | ||
|
|
4f92e68172 | ||
|
|
4e9d599e64 | ||
|
|
650c8bfc9e | ||
|
|
b3f9c3d27e | ||
|
|
240de28567 | ||
|
|
5ff11fdd0a | ||
|
|
2de758a167 | ||
|
|
4ee6c278d9 | ||
|
|
9771db7133 | ||
|
|
464c19bf39 | ||
|
|
1d349ec62b | ||
|
|
334830b826 | ||
|
|
ccf1031fad | ||
|
|
5041020596 | ||
|
|
e2d842ec1a | ||
|
|
5a053d89b7 | ||
|
|
7b82d91a7c | ||
|
|
822bd91323 | ||
|
|
a397ab63f7 | ||
|
|
4afd9e75da | ||
|
|
d1f7bc6198 | ||
|
|
3dd22fd298 | ||
|
|
5ee6897ce6 | ||
|
|
b252b55c85 | ||
|
|
b80295a21c | ||
|
|
6dafc087e9 | ||
|
|
a599835e1f | ||
|
|
fac0354b2d | ||
|
|
26948fb68b | ||
|
|
586d95fb55 | ||
|
|
2456b82e65 | ||
|
|
2145130d21 | ||
|
|
233cd8c3d6 | ||
|
|
5751ac6b4e | ||
|
|
2c05a82204 | ||
|
|
43b8743569 | ||
|
|
c62bc408dc | ||
|
|
8253ef90d0 | ||
|
|
e54b443247 | ||
|
|
b57e63d7d6 | ||
|
|
60ba3eaf03 | ||
|
|
04246936d2 | ||
|
|
5b7ffac74e | ||
|
|
f4bbcdb382 | ||
|
|
c127978dd2 | ||
|
|
e3891df243 | ||
|
|
510d3cfa33 | ||
|
|
676ce9b68d | ||
|
|
0d17d34983 | ||
|
|
cd8a304690 | ||
|
|
1dcd7dc806 | ||
|
|
b2bde8d97e | ||
|
|
afedd397a7 | ||
|
|
1210924562 | ||
|
|
341bb8495a | ||
|
|
b0749b5595 | ||
|
|
393c9cd13c | ||
|
|
b44dfc2d9d | ||
|
|
fa852a1ab8 | ||
|
|
e38d78a7b4 | ||
|
|
bc3275e624 | ||
|
|
bb04181abf | ||
|
|
17d28ed9bc | ||
|
|
2374cf41f8 | ||
|
|
3faa5b4a11 | ||
|
|
41ec622e26 | ||
|
|
c84faeaa72 | ||
|
|
81480f203d | ||
|
|
fd620a858c | ||
|
|
8f1b373c3d | ||
|
|
f72a09b698 | ||
|
|
11ff1994f3 | ||
|
|
0a6db0ff9b | ||
|
|
c4e47a8169 | ||
|
|
fe67bf8fdb | ||
|
|
8d9d711ad8 | ||
|
|
1a4f3f0e18 | ||
|
|
ccafd3a293 | ||
|
|
2359abf8a5 | ||
|
|
2c89b611b5 | ||
|
|
b6f359bcb8 | ||
|
|
b966722899 | ||
|
|
4c5ef5ac8c | ||
|
|
1273336622 | ||
|
|
44eb961c27 | ||
|
|
385c7274a3 | ||
|
|
00ca9755be | ||
|
|
3348370138 | ||
|
|
4b9ac6f1e5 | ||
|
|
1c098d9b04 | ||
|
|
af478c83cd | ||
|
|
bd3921b91b | ||
|
|
849eb7714c | ||
|
|
4da1b46b05 | ||
|
|
ba12a2bc6d | ||
|
|
fac6dd81b9 | ||
|
|
03d8bcaea2 | ||
|
|
686814f537 | ||
|
|
0cfb66ae16 | ||
|
|
1ce68cb1cf | ||
|
|
36eb48c649 | ||
|
|
897b3d3f39 | ||
|
|
b1b1f1f579 | ||
|
|
b9fe8e4b33 | ||
|
|
f7a4f9906c | ||
|
|
fb05999e9e | ||
|
|
60eae40006 | ||
|
|
815d1a906f | ||
|
|
cf77ebde6a | ||
|
|
07d552c86b | ||
|
|
4513033634 | ||
|
|
6a077c967a | ||
|
|
ea03477e8e | ||
|
|
d218d70b8d | ||
|
|
bc655ed9ef | ||
|
|
1c42ace096 | ||
|
|
7ec28c9481 | ||
|
|
09c63c636f | ||
|
|
a42d87742f | ||
|
|
870eff5826 | ||
|
|
7f3ef7bb82 | ||
|
|
c0fb108e06 | ||
|
|
7759418f5d | ||
|
|
884bf57193 | ||
|
|
8236d84dfa | ||
|
|
f8b349814c | ||
|
|
9f581ed10b | ||
|
|
a3ffbeccd0 | ||
|
|
404fae9c7c | ||
|
|
b2bd4bd694 | ||
|
|
a69a35a0b6 | ||
|
|
340d1d43be | ||
|
|
d68286821b | ||
|
|
5d0ad1ada2 | ||
|
|
9d7a814180 | ||
|
|
33c25bfe6d | ||
|
|
c42949b61e | ||
|
|
3e84c9b70f | ||
|
|
592153f968 | ||
|
|
3c7fbb8fd6 | ||
|
|
0bbc006b98 | ||
|
|
5518f561f0 | ||
|
|
7cfe768dbd | ||
|
|
04b0cf6330 | ||
|
|
1b70283c3a | ||
|
|
5c1290d5b3 | ||
|
|
4ee1f9cf2c | ||
|
|
594bceff77 | ||
|
|
4e271603c2 | ||
|
|
47a77ae1e2 | ||
|
|
bea093e8da | ||
|
|
b4ba9d4375 | ||
|
|
66fe0048a5 | ||
|
|
dfc6ebfeb0 | ||
|
|
b0ea9d3096 | ||
|
|
e4eaa74b51 | ||
|
|
716490be26 | ||
|
|
86936a66e0 | ||
|
|
40e54dbbd4 | ||
|
|
f0b9837407 | ||
|
|
11418501a4 | ||
|
|
e240525a35 | ||
|
|
1467fd5dbf | ||
|
|
5d67a6f427 | ||
|
|
d7a5cddcb3 | ||
|
|
83f84e5b6a | ||
|
|
d19dc1cf56 | ||
|
|
27e83342f9 | ||
|
|
9cfefbdb86 | ||
|
|
412a3c923b | ||
|
|
4e43bf5f78 | ||
|
|
ef25f8a721 | ||
|
|
34e5e5c513 | ||
|
|
d8ee07d1e4 | ||
|
|
d494e44df3 | ||
|
|
15edabc977 | ||
|
|
4fbd2e6caa | ||
|
|
b7a90eb4e4 | ||
|
|
af310854fc | ||
|
|
9805f8b9f2 | ||
|
|
dd283381a1 | ||
|
|
c775f5aba0 | ||
|
|
6df976d1f3 | ||
|
|
43d32af540 | ||
|
|
43ac9a9a22 | ||
|
|
91db8a9247 | ||
|
|
e69d402b4f | ||
|
|
f3d5515795 | ||
|
|
bde62473a4 | ||
|
|
87cf91a044 | ||
|
|
0f7372bfb4 | ||
|
|
76b7272a72 | ||
|
|
b3abc9fd6a | ||
|
|
20731be1a4 | ||
|
|
8f57ab343c | ||
|
|
83f43af36e | ||
|
|
32de3d9f1d | ||
|
|
c04af27bf3 | ||
|
|
091ea785e5 | ||
|
|
fe7faf0d0d | ||
|
|
43b1d3ca43 | ||
|
|
6453cb9d11 | ||
|
|
bb5d0b63ef | ||
|
|
999e2f6633 | ||
|
|
fd4c61ece7 | ||
|
|
767f1c7b3d | ||
|
|
a3d8af9a96 | ||
|
|
9ee54d6267 | ||
|
|
cf4a4b2b25 | ||
|
|
3b6c9f9511 | ||
|
|
356572c21b | ||
|
|
cb7499c10a | ||
|
|
28658cae73 | ||
|
|
ba7b2fd270 | ||
|
|
a14686c9f3 | ||
|
|
a450dee7cf | ||
|
|
55a7a34a1d | ||
|
|
4e7a3c09a6 | ||
|
|
292faec46f | ||
|
|
b616efd75c | ||
|
|
ee147612a3 | ||
|
|
69ead3348f | ||
|
|
f66ddcaa2d | ||
|
|
70d464189f | ||
|
|
60e2abde1b | ||
|
|
79fc3cbf12 | ||
|
|
ad2d8c8ee0 | ||
|
|
6a32428ca1 | ||
|
|
f8f90f308e | ||
|
|
678fe3d63e | ||
|
|
f06264ea0a | ||
|
|
25824629f2 | ||
|
|
b999c05d1e | ||
|
|
5f0020a95c | ||
|
|
bb07c4b3b7 | ||
|
|
9043d2574b | ||
|
|
031123b2ca | ||
|
|
3135de3eb3 | ||
|
|
64828c918d | ||
|
|
7aa7257d29 | ||
|
|
c648add963 | ||
|
|
16469daff3 | ||
|
|
d32cacf1da | ||
|
|
77c1163591 | ||
|
|
261cb249d2 | ||
|
|
0c3184ed83 | ||
|
|
f909b316c7 | ||
|
|
4768b023a4 | ||
|
|
d188ac2df4 | ||
|
|
fdd36d48bc | ||
|
|
6f5b18de3a | ||
|
|
df4adfe717 | ||
|
|
ff7330048b | ||
|
|
afabd179fb | ||
|
|
0c30d53d95 | ||
|
|
151e36df0e | ||
|
|
2ece527f9b | ||
|
|
d12b78985e | ||
|
|
2d07df2541 | ||
|
|
27a85ce0da | ||
|
|
f75ec43b71 | ||
|
|
b9e4861f16 | ||
|
|
802f19453d | ||
|
|
5b79928590 | ||
|
|
860a97a769 | ||
|
|
25177898e1 | ||
|
|
195fb3b29d | ||
|
|
234b2c9427 | ||
|
|
f3b5b07796 | ||
|
|
63cc6aecaf | ||
|
|
8aedb0b881 | ||
|
|
8487859fc2 | ||
|
|
20ecc79cd1 | ||
|
|
f83c8d4523 | ||
|
|
33c8743215 | ||
|
|
ab944fb9ae | ||
|
|
3d88749976 | ||
|
|
7d0cf6e8cc | ||
|
|
6fd7feffee | ||
|
|
760eb926bf | ||
|
|
9146642947 | ||
|
|
6c1e2b8eab | ||
|
|
ff6482fa29 | ||
|
|
c99f571296 | ||
|
|
9688bd8408 | ||
|
|
707fa160e8 | ||
|
|
4d9418e620 | ||
|
|
9f12456456 | ||
|
|
31d7aacec1 | ||
|
|
c4720edda7 | ||
|
|
2f0fcaf5d3 | ||
|
|
66606b7309 | ||
|
|
6d328e852d | ||
|
|
3f887f20e9 | ||
|
|
9ae9da8256 | ||
|
|
33b6df01ba | ||
|
|
6af3824293 | ||
|
|
507550edad | ||
|
|
b53fceefb9 | ||
|
|
c1c01aab02 | ||
|
|
e1923468a4 | ||
|
|
84007e6ad1 | ||
|
|
5636881463 | ||
|
|
7f8f8ecd62 | ||
|
|
88c0beddc6 | ||
|
|
37bd43a19f | ||
|
|
9b02889ea5 | ||
|
|
f4cb7d1862 | ||
|
|
4dd9767590 | ||
|
|
dea5cf4b5d | ||
|
|
b4b88bde0b | ||
|
|
9c73444102 | ||
|
|
c5f4ae2242 | ||
|
|
a3c583af1d | ||
|
|
84e95ab4c2 | ||
|
|
f12ade3b67 | ||
|
|
dbb1e6a890 | ||
|
|
38a645ad49 | ||
|
|
4a5e27e641 | ||
|
|
b7353db14e | ||
|
|
0f37c8ecbd | ||
|
|
2c0a2ce750 | ||
|
|
c0bc7553a9 | ||
|
|
067aece437 | ||
|
|
a14a71c222 | ||
|
|
4f6f4eea4c | ||
|
|
f84d0f34e6 | ||
|
|
4849904b0b | ||
|
|
9ed01cc0df | ||
|
|
ea2079f36f | ||
|
|
6fc90e20e9 | ||
|
|
01edf49de0 | ||
|
|
8f37f74d29 | ||
|
|
7e020f967b | ||
|
|
99d3b80033 | ||
|
|
b80332b9b3 | ||
|
|
4ef471919c | ||
|
|
3c336cd8f6 | ||
|
|
4a2db204f1 | ||
|
|
7b458daa98 | ||
|
|
dbf67dc47b | ||
|
|
686e9b64ef | ||
|
|
c674a300c6 | ||
|
|
09bce9c285 | ||
|
|
e26ece57d1 | ||
|
|
9822c52573 | ||
|
|
6ed470ed5f | ||
|
|
4e48b78e03 | ||
|
|
baec7838b4 | ||
|
|
53b5d78cdc | ||
|
|
f4157ba0e5 |
110
.circleci/config.yml
Normal file
@@ -0,0 +1,110 @@
|
||||
version: 2
|
||||
jobs:
|
||||
build:
|
||||
docker:
|
||||
# specify the version you desire here
|
||||
- image: penpotapp/devenv:latest
|
||||
|
||||
# Specify service dependencies here if necessary
|
||||
# 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.3-ram
|
||||
environment:
|
||||
POSTGRES_USER: penpot_test
|
||||
POSTGRES_PASSWORD: penpot_test
|
||||
POSTGRES_DB: penpot_test
|
||||
|
||||
- image: circleci/redis:6.0.8
|
||||
|
||||
working_directory: ~/repo
|
||||
|
||||
environment:
|
||||
# Customize the JVM maximum heap limit
|
||||
JVM_OPTS: -Xmx1g
|
||||
|
||||
steps:
|
||||
- checkout
|
||||
|
||||
# Download and cache dependencies
|
||||
- restore_cache:
|
||||
keys:
|
||||
- v1-dependencies-{{ checksum "backend/deps.edn" }}-{{ checksum "frontend/deps.edn"}}-{{ checksum "common/deps.edn"}}
|
||||
# fallback to using the latest cache if no exact match is found
|
||||
- v1-dependencies-
|
||||
|
||||
- run:
|
||||
name: common lint
|
||||
working_directory: "./common"
|
||||
command: |
|
||||
clj-kondo --version
|
||||
clj-kondo --parallel --lint src/
|
||||
|
||||
- run:
|
||||
name: frontend lint
|
||||
working_directory: "./frontend"
|
||||
command: |
|
||||
clj-kondo --version
|
||||
clj-kondo --parallel --lint src/
|
||||
|
||||
- run:
|
||||
name: frontend styles prettier
|
||||
working_directory: "./frontend"
|
||||
command: |
|
||||
yarn install
|
||||
yarn run lint-scss
|
||||
|
||||
- run:
|
||||
name: backend lint
|
||||
working_directory: "./backend"
|
||||
command: |
|
||||
clj-kondo --version
|
||||
clj-kondo --parallel --lint src/
|
||||
|
||||
# run backend test
|
||||
- run:
|
||||
name: backend test
|
||||
working_directory: "./backend"
|
||||
command: "clojure -X:dev:test"
|
||||
environment:
|
||||
PENPOT_TEST_DATABASE_URI: "postgresql://localhost/penpot_test"
|
||||
PENPOT_TEST_DATABASE_USERNAME: penpot_test
|
||||
PENPOT_TEST_DATABASE_PASSWORD: penpot_test
|
||||
PENPOT_TEST_REDIS_URI: "redis://localhost/1"
|
||||
|
||||
- run:
|
||||
name: frontend tests
|
||||
working_directory: "./frontend"
|
||||
command: |
|
||||
yarn install
|
||||
clojure -M:dev:shadow-cljs compile test
|
||||
node target/tests.js
|
||||
|
||||
environment:
|
||||
PATH: /usr/local/nodejs/bin/:/usr/local/bin:/bin:/usr/bin
|
||||
|
||||
# - run:
|
||||
# working_directory: "./common"
|
||||
# name: common tests (cljs)
|
||||
# command: |
|
||||
# yarn install
|
||||
# yarn run compile-test
|
||||
# node target/test.js
|
||||
#
|
||||
# environment:
|
||||
# PATH: /usr/local/nodejs/bin/:/usr/local/bin:/bin:/usr/bin
|
||||
|
||||
- run:
|
||||
working_directory: "./common"
|
||||
name: common tests (clj)
|
||||
command: |
|
||||
clojure -X:dev:test
|
||||
|
||||
environment:
|
||||
PATH: /usr/local/nodejs/bin/:/usr/local/bin:/bin:/usr/bin
|
||||
|
||||
- save_cache:
|
||||
paths:
|
||||
- ~/.m2
|
||||
key: v1-dependencies-{{ checksum "backend/deps.edn" }}-{{ checksum "frontend/deps.edn"}}-{{ checksum "common/deps.edn"}}
|
||||
|
||||
@@ -1,13 +1,32 @@
|
||||
{:lint-as {potok.core/reify clojure.core/reify
|
||||
promesa.core/let clojure.core/let
|
||||
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
|
||||
app.common.logging/with-context clojure.core/do}
|
||||
|
||||
:hooks
|
||||
{:analyze-call
|
||||
{app.common.data/export hooks.export/export
|
||||
potok.core/reify hooks.export/potok-reify
|
||||
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]}
|
||||
@@ -15,15 +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.services.mutations/defmutation)
|
||||
'(app.services.queries/defquery)
|
||||
'(app.util.dispatcher/defservice)
|
||||
'(mount.core/defstate)
|
||||
]}}}
|
||||
}}
|
||||
|
||||
|
||||
76
.clj-kondo/hooks/export.clj
Normal file
@@ -0,0 +1,76 @@
|
||||
(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}))
|
||||
51
.github/ISSUE_TEMPLATE/bug_report.md
vendored
@@ -8,49 +8,48 @@ assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**Describe the bug**
|
||||
A clear and concise description of what the bug is.
|
||||
|
||||
**To Reproduce**
|
||||
|
||||
Steps to reproduce the behavior:
|
||||
1. Go to '...'
|
||||
2. Click on '....'
|
||||
3. Scroll down to '....'
|
||||
4. See error
|
||||
|
||||
**Expected behavior**
|
||||
|
||||
A clear and concise description of what you expected to happen.
|
||||
|
||||
**Actual behavior**
|
||||
|
||||
A clear and concise description of what happens instead; what the bug is.
|
||||
|
||||
**Screenshots**
|
||||
|
||||
If applicable, add screenshots to help explain your problem.
|
||||
|
||||
**Desktop (please complete the following information):**
|
||||
|
||||
- OS: (e.g. iOS)
|
||||
- Browser (e.g. chrome, safari)
|
||||
- Version (e.g. 22)
|
||||
- OS (e.g. iOS):
|
||||
- Browser & version (e.g. Chrome 89.0):
|
||||
|
||||
**Smartphone (please complete the following information):**
|
||||
|
||||
- Device: (e.g. iPhone6)
|
||||
- OS: (e.g. iOS8.1)
|
||||
- Browser (e.g. stock browser, safari)
|
||||
- Version (e.g. 22)
|
||||
- Device & model (e.g. iPhone 6):
|
||||
- OS & version (e.g. iOS 8.1):
|
||||
- Browser & version (e.g. stock browser 22):
|
||||
|
||||
**Environment (please complete the following information):**
|
||||
Specify if using demo instance or self-hosted instance.
|
||||
- Host (e.g. https://design.penpot.app, local instance):
|
||||
|
||||
If self-hosted instance, add OS and runtime information to help explain your problem.
|
||||
*If self-hosted:*
|
||||
- OS Version (e.g. Ubuntu 16.04):
|
||||
- Docker / Docker-compose version (e.g. Docker version 18.03.0-ce, build 0520e24):
|
||||
- Image version (e.g. Alpine):
|
||||
|
||||
- OS Version: (e.g. Ubuntu 16.04)
|
||||
Docker commands or docker-compose file (if possible and if proceed.x):
|
||||
```
|
||||
|
||||
Also provide Docker commands or docker-compose file if possible.
|
||||
|
||||
- Docker / Docker-compose Version: (e.g. Docker version 18.03.0-ce, build 0520e24)
|
||||
- Image (e.g. alpine)
|
||||
|
||||
**Frontend Stack Trace (if self-hosted)**
|
||||
```
|
||||
|
||||
Frontend Stack Trace:
|
||||
<details>
|
||||
|
||||
```
|
||||
@@ -59,8 +58,7 @@ Also provide Docker commands or docker-compose file if possible.
|
||||
|
||||
</details>
|
||||
|
||||
**Backend Stack Trace (if self-hosted)**
|
||||
|
||||
Backend Stack Trace:
|
||||
<details>
|
||||
|
||||
```
|
||||
@@ -69,5 +67,6 @@ Also provide Docker commands or docker-compose file if possible.
|
||||
|
||||
</details>
|
||||
|
||||
**Additional context**
|
||||
Add any other context about the problem here.
|
||||
**Additional context:**
|
||||
|
||||
Any other context about the problem.
|
||||
|
||||
66
.gitignore
vendored
@@ -1,34 +1,54 @@
|
||||
figwheel_server.log
|
||||
*jar
|
||||
*-init.clj
|
||||
*.jar
|
||||
*.penpot
|
||||
*.orig
|
||||
.calva
|
||||
.clj-kondo
|
||||
.cpcache
|
||||
.lein-deps-sum
|
||||
.lein-failures
|
||||
.lein-repl-history
|
||||
.lein-plugins/
|
||||
.repl
|
||||
.lein-repl-history
|
||||
.lsp
|
||||
.nrepl-port
|
||||
.cpcache
|
||||
.nyc_output
|
||||
.rebel_readline_history
|
||||
/vendor/**/target
|
||||
/cd.md
|
||||
node_modules
|
||||
/backend/target/
|
||||
/backend/resources/public/media
|
||||
.repl
|
||||
/.clj-kondo/.cache
|
||||
/_dump
|
||||
/backend/-
|
||||
/backend/assets/
|
||||
/backend/dist/
|
||||
/backend/logs/
|
||||
/backend/-
|
||||
/frontend/npm-debug.log
|
||||
/frontend/target/
|
||||
/frontend/dist/
|
||||
/frontend/out/
|
||||
/frontend/.shadow-cljs
|
||||
/frontend/resources/public/*
|
||||
/exporter/target
|
||||
/exporter/.shadow-cljs
|
||||
/docker/images/bundle
|
||||
/.clj-kondo/.cache
|
||||
/backend/resources/public/assets
|
||||
/backend/resources/public/media
|
||||
/backend/target/
|
||||
/bundle*
|
||||
/media
|
||||
/cd.md
|
||||
/clj-profiler/
|
||||
/common/.shadow-cljs
|
||||
/common/coverage
|
||||
/common/target
|
||||
/deploy
|
||||
/docker/images/bundle*
|
||||
/exporter/.shadow-cljs
|
||||
/exporter/target
|
||||
/frontend/.shadow-cljs
|
||||
/frontend/package-lock.json
|
||||
/frontend/cypress/videos/*/
|
||||
/frontend/cypress/fixtures/validuser.json
|
||||
/frontend/dist/
|
||||
/frontend/npm-debug.log
|
||||
/frontend/out/
|
||||
/frontend/resources/fonts/experiments
|
||||
/frontend/resources/public/*
|
||||
/frontend/target/
|
||||
/frontend/cypress/videos/*/
|
||||
/media
|
||||
/telemetry/
|
||||
/vendor/**/target
|
||||
/vendor/svgclean/bundle*.js
|
||||
/web
|
||||
/_dump
|
||||
clj-profiler/
|
||||
figwheel_server.log
|
||||
node_modules
|
||||
|
||||
105
.gitpod.yml
Normal file
@@ -0,0 +1,105 @@
|
||||
image:
|
||||
file: docker/gitpod/Dockerfile
|
||||
|
||||
ports:
|
||||
# nginx
|
||||
- port: 3449
|
||||
onOpen: open-preview
|
||||
|
||||
# frontend nREPL
|
||||
- port: 3447
|
||||
onOpen: ignore
|
||||
visibility: private
|
||||
|
||||
# frontend shadow server
|
||||
- port: 3448
|
||||
onOpen: ignore
|
||||
visibility: private
|
||||
|
||||
# backend
|
||||
- port: 6060
|
||||
onOpen: ignore
|
||||
|
||||
# exporter shadow server
|
||||
- port: 9630
|
||||
onOpen: ignore
|
||||
visibility: private
|
||||
|
||||
# exporter http server
|
||||
- port: 6061
|
||||
onOpen: ignore
|
||||
|
||||
# mailhog web interface
|
||||
- port: 8025
|
||||
onOpen: ignore
|
||||
|
||||
# mailhog postfix
|
||||
- port: 1025
|
||||
onOpen: ignore
|
||||
|
||||
# postgres
|
||||
- port: 5432
|
||||
onOpen: ignore
|
||||
|
||||
# redis
|
||||
- port: 6379
|
||||
onOpen: ignore
|
||||
|
||||
# openldap
|
||||
- port: 389
|
||||
onOpen: ignore
|
||||
|
||||
tasks:
|
||||
# https://github.com/gitpod-io/gitpod/issues/666#issuecomment-534347856
|
||||
- name: gulp
|
||||
command: >
|
||||
cd $GITPOD_REPO_ROOT/frontend/;
|
||||
yarn && gp sync-done 'frontend-yarn';
|
||||
npx gulp --theme=${PENPOT_THEME} watch
|
||||
|
||||
- name: frontend shadow watch
|
||||
command: >
|
||||
cd $GITPOD_REPO_ROOT/frontend/;
|
||||
gp sync-await 'frontend-yarn';
|
||||
npx shadow-cljs watch main
|
||||
|
||||
- init: gp await-port 5432 && psql -f $GITPOD_REPO_ROOT/docker/gitpod/files/postgresql_init.sql
|
||||
name: backend
|
||||
command: >
|
||||
cd $GITPOD_REPO_ROOT/backend/;
|
||||
./scripts/start-dev
|
||||
|
||||
- name: exporter shadow watch
|
||||
command:
|
||||
cd $GITPOD_REPO_ROOT/exporter/;
|
||||
gp sync-await 'frontend-yarn';
|
||||
yarn && npx shadow-cljs watch main
|
||||
|
||||
- name: exporter web server
|
||||
command: >
|
||||
cd $GITPOD_REPO_ROOT/exporter/;
|
||||
./scripts/wait-and-start.sh
|
||||
|
||||
- name: signed terminal
|
||||
before: >
|
||||
[[ ! -z ${GNUGPG} ]] &&
|
||||
cd ~ &&
|
||||
rm -rf .gnupg &&
|
||||
echo ${GNUGPG} | base64 -d | tar --no-same-owner -xzvf -
|
||||
init: >
|
||||
[[ ! -z ${GNUGPG_KEY} ]] &&
|
||||
git config --global commit.gpgsign true &&
|
||||
git config --global user.signingkey ${GNUGPG_KEY}
|
||||
command: cd $GITPOD_REPO_ROOT
|
||||
|
||||
- name: redis
|
||||
command: redis-server
|
||||
|
||||
- before: go get github.com/mailhog/MailHog
|
||||
name: mailhog
|
||||
command: MailHog
|
||||
|
||||
- name: Nginx
|
||||
command: >
|
||||
nginx &&
|
||||
multitail /var/log/nginx/access.log -I /var/log/nginx/error.log
|
||||
851
CHANGES.md
Normal file
@@ -0,0 +1,851 @@
|
||||
# CHANGELOG
|
||||
|
||||
## 1.12.2-beta
|
||||
|
||||
### :bug: Bugs fixed
|
||||
|
||||
- Fix issue with guides over shape handlers [Taiga #3032](https://tree.taiga.io/project/penpot/issue/3032)
|
||||
- Fix problem with shift+ctrl+click to select [#1671](https://github.com/penpot/penpot/issues/1671)
|
||||
- Fix ellipsis in long page names [Taiga #2962](https://tree.taiga.io/project/penpot/issue/2962)
|
||||
|
||||
## 1.12.1-beta
|
||||
|
||||
### :bug: Bugs fixed
|
||||
|
||||
- Fix length of names in sidebar [Taiga #2962](https://tree.taiga.io/project/penpot/issue/2962)
|
||||
- Fix issues on loki integration
|
||||
|
||||
|
||||
## 1.12.0-beta
|
||||
|
||||
### :boom: Breaking changes
|
||||
|
||||
### :sparkles: New features
|
||||
|
||||
- Open feedback in a new window [Taiga #2901](https://tree.taiga.io/project/penpot/us/2901)
|
||||
- Improve usage of file menu [Taiga #2853](https://tree.taiga.io/project/penpot/us/2853)
|
||||
- Rotation to snap to 15º intervals with shift [Taiga #2437](https://tree.taiga.io/project/penpot/issue/2437)
|
||||
- Support border radius and stroke properties for images [Taiga #497](https://tree.taiga.io/project/penpot/us/497)
|
||||
- Disallow using same password as user email [Taiga #2454](https://tree.taiga.io/project/penpot/us/2454)
|
||||
- Add configurable nudge amount [Taiga #910](https://tree.taiga.io/project/penpot/us/910)
|
||||
- Add stroke properties for image shapes [Taiga #497](https://tree.taiga.io/project/penpot/us/497)
|
||||
- On user settings, hide the theme selector as long as we only have one theme [Taiga #2610](https://tree.taiga.io/project/penpot/us/2610)
|
||||
- Automatically open comments from dashboard notifications [Taiga #2605](https://tree.taiga.io/project/penpot/us/2605)
|
||||
- Enhance the behaviour of the artboards list on view mode [Taiga #2634](https://tree.taiga.io/project/penpot/us/2634)
|
||||
- Add recent used fonts in font selection widget [Taiga #1381](https://tree.taiga.io/project/penpot/us/1381)
|
||||
- Allow to align items relative to groups [Taiga #2533](https://tree.taiga.io/project/penpot/us/2533)
|
||||
- Scroll bars [Taiga #2550](https://tree.taiga.io/project/penpot/task/2550)
|
||||
- Add select layer option to context menu [Taiga #2474](https://tree.taiga.io/project/penpot/us/2474)
|
||||
- Guides [Taiga #290](https://tree.taiga.io/project/penpot/us/290)
|
||||
- Improve file menu by adding semantically groups [Github #1203](https://github.com/penpot/penpot/issues/1203)
|
||||
- Add update components in bulk option in context menu [Taiga #1975](https://tree.taiga.io/project/penpot/us/1975)
|
||||
- Create first E2E tests [Taiga #2608](https://tree.taiga.io/project/penpot/task/2608), [Taiga #2608](https://tree.taiga.io/project/penpot/task/2608)
|
||||
- Redesign of workspace toolbars [Taiga #2319](https://tree.taiga.io/project/penpot/us/2319)
|
||||
- Graphic Tablet usability improvements [Taiga #1913](https://tree.taiga.io/project/penpot/us/1913)
|
||||
- Improved mouse collision detection for groups and text shapes [Taiga #2452](https://tree.taiga.io/project/penpot/us/2452), [Taiga #2453](https://tree.taiga.io/project/penpot/us/2453)
|
||||
- Add support for alternative S3 storage providers and all aws regions [#1267](https://github.com/penpot/penpot/issues/1267)
|
||||
|
||||
### :bug: Bugs fixed
|
||||
|
||||
- Fixed ungroup typography when editing it [Taiga #2391](https://tree.taiga.io/project/penpot/issue/2391)
|
||||
- Fixed error when trying to post an empty comment [Taiga #2603](https://tree.taiga.io/project/penpot/issue/2603)
|
||||
- Fixed missing translation strings [Taiga #2786](https://tree.taiga.io/project/penpot/issue/2786)
|
||||
- Fixed color palette outside viewport [Taiga #2715](https://tree.taiga.io/project/penpot/issue/2715)
|
||||
- Fixed missing translate string [Taiga #2780](https://tree.taiga.io/project/penpot/issue/2780)
|
||||
- Fixed handoff shadow type text [Taiga #2717](https://tree.taiga.io/project/penpot/issue/2717)
|
||||
- Fixed components get "dirty" marker when moved [Taiga #2764](https://tree.taiga.io/project/penpot/issue/2764)
|
||||
- Fixed cannot align objects in a group that is not part of a frame [Taiga #2762](https://tree.taiga.io/project/penpot/issue/2762)
|
||||
- Fix problem with double click on exit path editing [Taiga #2906](https://tree.taiga.io/project/penpot/issue/2906)
|
||||
- Fixed alignment of layers with children [Taiga #2862](https://tree.taiga.io/project/penpot/issue/2862)
|
||||
|
||||
### :heart: Community contributions by (Thank you!)
|
||||
|
||||
- Cleanup unused static images (by @rhcarvalho) [#1561](https://github.com/penpot/penpot/pull/1561)
|
||||
- Compress static images to save space (by @rhcarvalho) [#1562](https://github.com/penpot/penpot/pull/1562)
|
||||
|
||||
## 1.11.2-beta
|
||||
|
||||
### :bug: Bugs fixed
|
||||
|
||||
- Fix issue on handling empty content on boolean shapes
|
||||
- Fix race condition issue on component renaming
|
||||
- Handle EOF errors on writting streamed response
|
||||
- Handle EOF errors on websocket send/ping methods
|
||||
- Disable parallel upload of file media on import (causes too much
|
||||
contention on the rlimit subsistem that does not works as expected
|
||||
on high load).
|
||||
|
||||
### :sparkles: New features
|
||||
|
||||
- Add health check endpoint on API
|
||||
- Increase default max connection pool size to 60
|
||||
- Reduce resource usage of the error reporter.
|
||||
|
||||
## 1.11.1-beta
|
||||
|
||||
### :bug: Bugs fixed
|
||||
|
||||
- Fix issue related to default http host config value.
|
||||
- Fix issue on rendering frames on firefox.
|
||||
|
||||
### :arrow_up: Deps updates
|
||||
|
||||
- Update nodejs version to 16.13.1 on docker images.
|
||||
|
||||
## 1.11.0-beta
|
||||
|
||||
### :sparkles: New features
|
||||
|
||||
- Add an option to hide artboards names on the viewport [Taiga #2034](https://tree.taiga.io/project/penpot/issue/2034)
|
||||
- Limit pasted object position to container boundaries [Taiga #2449](https://tree.taiga.io/project/penpot/us/2449)
|
||||
- Add new options for zoom widget in workspace and viewer mode [Taiga #896](https://tree.taiga.io/project/penpot/us/896)
|
||||
- Allow decimals on stroke width and positions [Taiga #2035](https://tree.taiga.io/project/penpot/issue/2035)
|
||||
- Ability to ignore background when exporting an artboard [Taiga #1395](https://tree.taiga.io/project/penpot/us/1395)
|
||||
- Show color hex or name on hover [Taiga #2413](https://tree.taiga.io/project/penpot/us/2413)
|
||||
- Add shortcut to create artboard from selected objects [Taiga #2412](https://tree.taiga.io/project/penpot/us/2412)
|
||||
- Add shortcut for opacity [Taiga #2442](https://tree.taiga.io/project/penpot/us/2442)
|
||||
- Setting fill automatically for new texts [Taiga #2441](https://tree.taiga.io/project/penpot/us/2441)
|
||||
- Add shortcut to move action [Github #1213](https://github.com/penpot/penpot/issues/1213)
|
||||
- Add alt as mod key to add stroke color from library menu [Taiga #2207](https://tree.taiga.io/project/penpot/us/2207)
|
||||
- Add detach in bulk option to context menu [Taiga #2210](https://tree.taiga.io/project/penpot/us/2210)
|
||||
- Add penpot look and feel to multiuser cursors [Taiga #1387](https://tree.taiga.io/project/penpot/us/1387)
|
||||
- Add actions to go to main component context menu option [Taiga #2053](https://tree.taiga.io/project/penpot/us/2053)
|
||||
- Add contrast between component select color and shape select color [Taiga #2121](https://tree.taiga.io/project/penpot/issue/2121)
|
||||
- Add animations in interactions [Taiga #2244](https://tree.taiga.io/project/penpot/us/2244)
|
||||
- Add performance improvements on .penpot file import process [Taiga #2497](https://tree.taiga.io/project/penpot/us/2497)
|
||||
- On team settings set color of members count to black [Taiga #2607](https://tree.taiga.io/project/penpot/us/2607)
|
||||
|
||||
### :bug: Bugs fixed
|
||||
|
||||
- Fix remove gradient if any when applying color from library [Taiga #2299](https://tree.taiga.io/project/penpot/issue/2299)
|
||||
- Fix Enter as key action to exit edit path [Taiga #2444](https://tree.taiga.io/project/penpot/issue/2444)
|
||||
- Fix add fill color from palette to groups and components [Taiga #2313](https://tree.taiga.io/project/penpot/issue/2313)
|
||||
- Fix default project name in all languages [Taiga #2280](https://tree.taiga.io/project/penpot/issue/2280)
|
||||
- Fix line-height and letter-spacing inputs to allow negative values [Taiga #2381](https://tree.taiga.io/project/penpot/issue/2381)
|
||||
- Fix typo in Handoff tooltip [Taiga #2428](https://tree.taiga.io/project/penpot/issue/2428)
|
||||
- Fix crash when pressing Shift+1 on empty file [#1435](https://github.com/penpot/penpot/issues/1435)
|
||||
- Fix masked group resize strange behavior [Taiga #2317](https://tree.taiga.io/project/penpot/issue/2317)
|
||||
- Fix problems when exporting all artboards [Taiga #2234](https://tree.taiga.io/project/penpot/issue/2234)
|
||||
- Fix problems with team management [#1353](https://github.com/penpot/penpot/issues/1353)
|
||||
- Fix problem when importing in shared libraries [#1362](https://github.com/penpot/penpot/issues/1362)
|
||||
- Fix problem with join nodes [#1422](https://github.com/penpot/penpot/issues/1422)
|
||||
- After team onboarding importing a file will import into the team drafts [Taiga #2408](https://tree.taiga.io/project/penpot/issue/2408)
|
||||
- Fix problem exporting shapes from handoff mode [Taiga #2386](https://tree.taiga.io/project/penpot/issue/2386)
|
||||
- Fix lock/hide elements in context menu when multiples shapes selected [Taiga #2340](https://tree.taiga.io/project/penpot/issue/2340)
|
||||
- Fix problem with booleans [Taiga #2356](https://tree.taiga.io/project/penpot/issue/2356)
|
||||
- Fix line-height/letter-spacing inputs behaviour [Taiga #2331](https://tree.taiga.io/project/penpot/issue/2331)
|
||||
- Fix dotted style in strokes [Taiga #2312](https://tree.taiga.io/project/penpot/issue/2312)
|
||||
- Fix problem when resizing texts inside groups [Taiga #2310](https://tree.taiga.io/project/penpot/issue/2310)
|
||||
- Fix problem with multiple exports [Taiga #2468](https://tree.taiga.io/project/penpot/issue/2468)
|
||||
- Allow import to continue from recoverable failures [#1412](https://github.com/penpot/penpot/issues/1412)
|
||||
- Improved behaviour on text options when not text is selected [Taiga #2390](https://tree.taiga.io/project/penpot/issue/2390)
|
||||
- Fix decimal numbers in export viewbox [Taiga #2290](https://tree.taiga.io/project/penpot/issue/2290)
|
||||
- Right click over artboard name to open its menu [Taiga #1679](https://tree.taiga.io/project/penpot/issue/1679)
|
||||
- Make the default session cookue use SameSite=Lax instead of Strict (causes some issues in latest versions of Chrome)
|
||||
- Fix "open in new tab" on dashboard [Taiga #2235](https://tree.taiga.io/project/penpot/issue/2355)
|
||||
- Changing pages while comments activated will not close the panel [#1350](https://github.com/penpot/penpot/issues/1350)
|
||||
- Fix navigate comments in right sidebar [Taiga #2163](https://tree.taiga.io/project/penpot/issue/2163)
|
||||
- Fix keep name of component equal to the shape name [Taiga #2341](https://tree.taiga.io/project/penpot/issue/2341)
|
||||
- Fix lossing changes when changing selection and an input was already changed [Taiga #2329](https://tree.taiga.io/project/penpot/issue/2329), [Taiga #2330](https://tree.taiga.io/project/penpot/issue/2330)
|
||||
- Fix blur input field when click on viewport [Taiga #2164](https://tree.taiga.io/project/penpot/issue/2164)
|
||||
- Fix default page id in workspace [Taiga #2205](https://tree.taiga.io/project/penpot/issue/2205)
|
||||
- Fix problem when importing a file with grids [Taiga #2314](https://tree.taiga.io/project/penpot/issue/2314)
|
||||
- Fix problem with imported svgs with filters [Taiga #2478](https://tree.taiga.io/project/penpot/issue/2478)
|
||||
- Fix issues when updating selrect in paths [Taiga #2366](https://tree.taiga.io/project/penpot/issue/2366)
|
||||
- Fix scroll jumps in handoff mode [Taiga #2383](https://tree.taiga.io/project/penpot/issue/2383)
|
||||
- Fix handoff text with opacity [Taiga #2384](https://tree.taiga.io/project/penpot/issue/2384)
|
||||
- Restored rules color [Taiga #2460](https://tree.taiga.io/project/penpot/issue/2460)
|
||||
- Fix thumbnail not taking frame blending mode [Taiga #2301](https://tree.taiga.io/project/penpot/issue/2301)
|
||||
- Fix import/export with SVG edge cases [Taiga #2389](https://tree.taiga.io/project/penpot/issue/2389)
|
||||
- Avoid modifying component when moving into a group [Taiga #2534](https://tree.taiga.io/project/penpot/issue/2534)
|
||||
- Show correctly group types label in handoff [Taiga #2482](https://tree.taiga.io/project/penpot/issue/2482)
|
||||
- Display view mode buttons always centered in viewer [#Taiga 2466](https://tree.taiga.io/project/penpot/issue/2466)
|
||||
- Fix default profile image generation issue [Taiga #2601](https://tree.taiga.io/project/penpot/issue/2601)
|
||||
- Fix edit blur attributes for multiselection [Taiga #2625](https://tree.taiga.io/project/penpot/issue/2625)
|
||||
- Fix auto hide header in viewer full screen [Taiga #2632](https://tree.taiga.io/project/penpot/issue/2632)
|
||||
- Fix zoom in/out after fit or fill [Taiga #2630](https://tree.taiga.io/project/penpot/issue/2630)
|
||||
- Normalize zoom levels in workspace and viewer [Taiga #2631](https://tree.taiga.io/project/penpot/issue/2631)
|
||||
- Avoid empty names in projects, files and pages [Taiga #2594](https://tree.taiga.io/project/penpot/issue/2594)
|
||||
- Fix "move to" menu when duplicated team or project names [Taiga #2655](https://tree.taiga.io/project/penpot/issue/2655)
|
||||
- Fix ungroup a component leaves an asterisk in layers [Taiga #2694](https://tree.taiga.io/project/penpot/issue/2694)
|
||||
|
||||
### :arrow_up: Deps updates
|
||||
|
||||
- Update devenv docker image dependencies
|
||||
|
||||
### :heart: Community contributions by (Thank you!)
|
||||
|
||||
- Spelling fixes (by @jsoref) [#1340](https://github.com/penpot/penpot/pull/1340)
|
||||
- Explain folders in components (by @candideu) [Penpot-docs #42](https://github.com/penpot/penpot-docs/pull/42)
|
||||
- Readability improvements of user guide (by @PaulSchulz) [Penpot-docs #50](https://github.com/penpot/penpot-docs/pull/50)
|
||||
|
||||
## 1.10.4-beta
|
||||
|
||||
### :sparkles: Enhacements
|
||||
|
||||
- Allow parametrice file snapshoting interval
|
||||
|
||||
### :bug: Bugs fixed
|
||||
|
||||
- Fix issue on :mov-object change impl
|
||||
- Minor fix on how file changes log is persisted
|
||||
- Fix many issues on error reporting
|
||||
|
||||
## 1.10.3-beta
|
||||
|
||||
### :sparkles: Enhacements
|
||||
|
||||
- Make all logging asynchronous, this avoid some overhead on jetty threads at cost of logging latency.
|
||||
- Increase default session time to 15 days.
|
||||
|
||||
### :bug: Bugs fixed
|
||||
|
||||
- Fix unexpected exception on saving pages with default grids [#2409](https://tree.taiga.io/project/penpot/issue/2409)
|
||||
- Fix react warnings on setting size 1 on row and column grids.
|
||||
- Fix minor issues on ZMQ logging listener (used in error reporting service)
|
||||
- Remove "ALPHA" from the code.
|
||||
- Fix value and nil handling on numeric-input component. This fixes many issues related to typography, components, etc. renaming.
|
||||
- Fix NPE on email complains processing.
|
||||
- Fix white page after leaving a team.
|
||||
- Fix missing leave team button outside members page.
|
||||
|
||||
### :arrow_up: Deps updates
|
||||
|
||||
- Update log4j2 dependency.
|
||||
|
||||
## 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
|
||||
|
||||
- Allow ungroup groups in bulk [Taiga #2211](https://tree.taiga.io/project/penpot/us/2211)
|
||||
- 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)
|
||||
- Fix problem with styles in the viewer [Taiga #2467](https://tree.taiga.io/project/penpot/issue/2467)
|
||||
- Fix default state in viewer [Taiga #2465](https://tree.taiga.io/project/penpot/issue/2465)
|
||||
- Fix division by zero in bool operation [Taiga #2349](https://tree.taiga.io/project/penpot/issue/2349)
|
||||
|
||||
### :heart: Community contributions by (Thank you!)
|
||||
|
||||
- To the translation community for the hard work on making penpot
|
||||
available on so many languages.
|
||||
- Guide to integrate with Azure Directory (by @skrzyneckik) [Penpot-docs #33](https://github.com/penpot/penpot-docs/pull/33)
|
||||
- Improve libraries section readability (by @PaulSchulz) [Penpot-docs #39](https://github.com/penpot/penpot-docs/pull/39)
|
||||
|
||||
## 1.9.0-alpha
|
||||
|
||||
### :boom: Breaking changes
|
||||
|
||||
- Some stroke-caps can change behaviour.
|
||||
- Text display bug fix could potentially 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 artboards [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 unnecessary 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 partially 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
|
||||
|
||||
### :bug: Bugs fixed
|
||||
|
||||
- Add better auth module logging.
|
||||
- Add missing `email` scope to OIDC backend.
|
||||
- Add missing cause prop on error loging.
|
||||
- Fix empty font-family handling on custom fonts page.
|
||||
- Fix incorrect unicode code points handling on draft-to-penpot conversion.
|
||||
- Fix some problems with paths.
|
||||
- Fix unexpected exception on duplicate project.
|
||||
- Fix unexpected exception when user leaves typography name empty.
|
||||
- Improve error report on uploading invalid image to library.
|
||||
- Minor fix on previous commit.
|
||||
- Minor improvements on svg uploading on libraries.
|
||||
|
||||
## 1.6.1-alpha
|
||||
|
||||
### :bug: Bugs fixed
|
||||
|
||||
- Add safety check on reg-objects change impl.
|
||||
- Fix custom fonts embedding issue.
|
||||
- Fix dashboard ordering issue.
|
||||
- Fix problem when creating a component with empty data.
|
||||
- Fix problem with moving shapes into frames.
|
||||
- Fix problems with mov-objects.
|
||||
- Fix unexpected exception related to rounding integers.
|
||||
- Fix wrong type usage on libraries changes.
|
||||
- Improve editor lifecycle management.
|
||||
- Make the navigation async by default.
|
||||
|
||||
## 1.6.0-alpha
|
||||
|
||||
### :sparkles: New features
|
||||
|
||||
- Add improved workspace font selector [Taiga US #292](https://tree.taiga.io/project/penpot/us/292)
|
||||
- Add option to interactively scale text [Taiga #1527](https://tree.taiga.io/project/penpot/us/1527)
|
||||
- Add performance improvements on dashboard data loading.
|
||||
- Add performance improvements to indexes handling on workspace.
|
||||
- Add the ability to upload/use custom fonts (and automatically generate all needed webfonts) [Taiga US #292](https://tree.taiga.io/project/penpot/us/292)
|
||||
- Transform shapes to path on double click
|
||||
- Translate automatic names of new files and projects.
|
||||
- Use shift instead of ctrl/cmd to keep aspect ratio [Taiga 1697](https://tree.taiga.io/project/penpot/issue/1697)
|
||||
- New translations: Portuguese (Brazil) and Romanias.
|
||||
|
||||
### :bug: Bugs fixed
|
||||
|
||||
- Remove interactions when the destination artboard is deleted [Taiga #1656](https://tree.taiga.io/project/penpot/issue/1656)
|
||||
- Fix problem with fonts that ends with numbers [#940](https://github.com/penpot/penpot/issues/940)
|
||||
- Fix problem with imported SVG on editing paths [#971](https://github.com/penpot/penpot/issues/971)
|
||||
- Fix problem with color picker positioning
|
||||
- Fix order on color palette [#961](https://github.com/penpot/penpot/issues/961)
|
||||
- Fix issue when group creation leaves an empty group [#1724](https://tree.taiga.io/project/penpot/issue/1724)
|
||||
- Fix problem with :multiple for colors and typographies [#1668](https://tree.taiga.io/project/penpot/issue/1668)
|
||||
- Fix problem with locked shapes when change parents [#974](https://github.com/penpot/penpot/issues/974)
|
||||
- Fix problem with new nodes in paths [#978](https://github.com/penpot/penpot/issues/978)
|
||||
|
||||
### :arrow_up: Deps updates
|
||||
|
||||
- Update exporter dependencies (puppeteer), that fixes some unexpected exceptions.
|
||||
- Update string manipulation library.
|
||||
|
||||
### :boom: Breaking changes
|
||||
|
||||
- The OIDC setting `PENPOT_OIDC_SCOPES` has changed the default semantics. Before this
|
||||
configuration added scopes to the default set. Now it replaces it, so use with care, because
|
||||
penpot requires at least `name` and `email` props found on the user info object.
|
||||
|
||||
### :heart: Community contributions by (Thank you!)
|
||||
|
||||
- Translations: Portuguese (Brazil) and Romanias.
|
||||
|
||||
## 1.5.4-alpha
|
||||
|
||||
### :bug: Bugs fixed
|
||||
|
||||
- Fix issues on group rendering.
|
||||
- Fix problem with text editing auto-height [Taiga #1683](https://tree.taiga.io/project/penpot/issue/1683)
|
||||
|
||||
## 1.5.3-alpha
|
||||
|
||||
### :bug: Bugs fixed
|
||||
|
||||
- Fix problem undo/redo.
|
||||
|
||||
## 1.5.2-alpha
|
||||
|
||||
### :bug: Bugs fixed
|
||||
|
||||
- Fix problem with `close-path` command [#917](https://github.com/penpot/penpot/issues/917)
|
||||
- Fix wrong query for obtain the profile default project-id
|
||||
- Fix problems with empty paths and shortcuts [#923](https://github.com/penpot/penpot/issues/923)
|
||||
|
||||
## 1.5.1-alpha
|
||||
|
||||
### :bug: Bugs fixed
|
||||
|
||||
- Fix issue with bitmap image clipboard.
|
||||
- Fix issue when removing all path points.
|
||||
- Increase default team invitation token expiration to 48h.
|
||||
- Fix wrong error message when an expired token is used.
|
||||
|
||||
## 1.5.0-alpha
|
||||
|
||||
### :sparkles: New features
|
||||
|
||||
- Add integration with gitpod.io (an online IDE) [#807](https://github.com/penpot/penpot/pull/807)
|
||||
- Allow basic math operations in inputs [Taiga 1383](https://tree.taiga.io/project/penpot/us/1383)
|
||||
- Autocomplete color names in hex inputs [Taiga 1596](https://tree.taiga.io/project/penpot/us/1596)
|
||||
- Allow to group assets (components and graphics) [Taiga #1289](https://tree.taiga.io/project/penpot/us/1289)
|
||||
- Change icon of pinned projects [Taiga 1298](https://tree.taiga.io/project/penpot/us/1298)
|
||||
- Internal: refactor of http client, replace internal xhr usage with more modern Fetch API.
|
||||
- New features for paths: snap points on edition, add/remove nodes, merge/join/split nodes. [Taiga #907](https://tree.taiga.io/project/penpot/us/907)
|
||||
- Add OpenID-Connect support.
|
||||
- Reimplement social auth providers on top of the generic openid impl.
|
||||
|
||||
### :bug: Bugs fixed
|
||||
|
||||
- Fix problem with pan and space [#811](https://github.com/penpot/penpot/issues/811)
|
||||
- Fix issue when parsing exponential numbers in paths
|
||||
- Remove legacy system user and team [#843](https://github.com/penpot/penpot/issues/843)
|
||||
- Fix ordering of copy pasted objects [Taiga #1618](https://tree.taiga.io/project/penpot/issue/1617)
|
||||
- Fix problems with blending modes [#837](https://github.com/penpot/penpot/issues/837)
|
||||
- Fix problem with zoom an selection rect [#845](https://github.com/penpot/penpot/issues/845)
|
||||
- Fix problem displaying team statistics [#859](https://github.com/penpot/penpot/issues/859)
|
||||
- Fix problems with text editor selection [Taiga #1546](https://tree.taiga.io/project/penpot/issue/1546)
|
||||
- Fix problem when opening the context menu in dashboard at the bottom [#856](https://github.com/penpot/penpot/issues/856)
|
||||
- Fix problem when clicking an interactive group in view mode [#863](https://github.com/penpot/penpot/issues/863)
|
||||
- Fix visibility of pages in sitemap when changing page [Taiga #1618](https://tree.taiga.io/project/penpot/issue/1618)
|
||||
- Fix visual problem with group invite [Taiga #1290](https://tree.taiga.io/project/penpot/issue/1290)
|
||||
- Fix issues with promote owner panel [Taiga #763](https://tree.taiga.io/project/penpot/issue/763)
|
||||
- Allow use library colors when defining gradients [Taiga #1614](https://tree.taiga.io/project/penpot/issue/1614)
|
||||
- Fix group selrect not updating after alignment [#895](https://github.com/penpot/penpot/issues/895)
|
||||
|
||||
### :arrow_up: Deps updates
|
||||
|
||||
### :boom: Breaking changes
|
||||
|
||||
- Translations refactor: now penpot uses gettext instead of a custom
|
||||
JSON, and each locale has its own separated file. All translations
|
||||
should be contributed via the weblate.org service.
|
||||
|
||||
### :heart: Community contributions by (Thank you!)
|
||||
|
||||
- madmath03 (by [Monogramm](https://github.com/Monogramm)) [#807](https://github.com/penpot/penpot/pull/807)
|
||||
- zzkt [#814](https://github.com/penpot/penpot/pull/814)
|
||||
|
||||
## 1.4.1-alpha
|
||||
|
||||
### :bug: Bugs fixed
|
||||
|
||||
- Fix typography unlinking.
|
||||
- Fix incorrect measures on shapes outside artboard.
|
||||
- Fix issues on svg parsing related to numbers with exponents.
|
||||
- Fix some race conditions on removing shape from workspace.
|
||||
- Fix incorrect state management of user lang selection.
|
||||
- Fix email validation usability issue on team invitation lightbox.
|
||||
|
||||
## 1.4.0-alpha
|
||||
|
||||
### :sparkles: New features
|
||||
|
||||
- Add blob-encoding v3 (uses ZSTD+transit) [#738](https://github.com/penpot/penpot/pull/738)
|
||||
- Add http caching layer on top of Query RPC.
|
||||
- Add layer opacity and blend mode to shapes [Taiga #937](https://tree.taiga.io/project/penpot/us/937)
|
||||
- Add more chinese translations [#726](https://github.com/penpot/penpot/pull/726)
|
||||
- Add native support for text-direction (RTL, LTR & auto)
|
||||
- Add several enhancements in shape selection [Taiga #1195](https://tree.taiga.io/project/penpot/us/1195)
|
||||
- Add thumbnail in memory caching mechanism.
|
||||
- Add turkish translation strings [#759](https://github.com/penpot/penpot/pull/759), [#794](https://github.com/penpot/penpot/pull/794)
|
||||
- Duplicate and move files and projects [Taiga #267](https://tree.taiga.io/project/penpot/us/267)
|
||||
- Hide viewer navbar on fullscreen [Taiga 1375](https://tree.taiga.io/project/penpot/us/1375)
|
||||
- Import SVG will create Penpot's shapes [Taiga #1006](https://tree.taiga.io/project/penpot/us/1066)
|
||||
- Improve french translations [#731](https://github.com/penpot/penpot/pull/731)
|
||||
- Reimplement workspace presence (remove database state)
|
||||
- Remember last visited team when you re-enter the application [Taiga #1376](https://tree.taiga.io/project/penpot/us/1376)
|
||||
- Rename artboard with double click on the title [Taiga #1392](https://tree.taiga.io/project/penpot/us/1392)
|
||||
- Replace Slate-Editor with DraftJS [Taiga #1346](https://tree.taiga.io/project/penpot/us/1346)
|
||||
- Set proper page title [Taiga #1377](https://tree.taiga.io/project/penpot/us/1377)
|
||||
|
||||
### :bug: Bugs fixed
|
||||
|
||||
- Disable buttons in view mode for users without permissions [Taiga #1328](https://tree.taiga.io/project/penpot/issue/1328)
|
||||
- Fix broken profile and profile options form.
|
||||
- Fix calculate size of some animated gifs [Taiga #1487](https://tree.taiga.io/project/penpot/issue/1487)
|
||||
- Fix error with the "Navigate to" button on prototypes [Taiga #1268](https://tree.taiga.io/project/penpot/issue/1268)
|
||||
- Fix issue when undo after changing the artboard of a shape [Taiga #1304](https://tree.taiga.io/project/penpot/issue/1304)
|
||||
- Fix issue with Alt key in distance measurement [#672](https://github.com/penpot/penpot/issues/672)
|
||||
- Fix issue with blending modes in masks [Taiga #1476](https://tree.taiga.io/project/penpot/issue/1476)
|
||||
- Fix issue with blocked shapes [Taiga #1480](https://tree.taiga.io/project/penpot/issue/1480)
|
||||
- Fix issue with comments styles on dashboard [Taiga #1405](https://tree.taiga.io/project/penpot/issue/1405)
|
||||
- Fix issue with default square grid [Taiga #1344](https://tree.taiga.io/project/penpot/issue/1344)
|
||||
- Fix issue with enter key shortcut [#775](https://github.com/penpot/penpot/issues/775)
|
||||
- Fix issue with enter to edit paths [Taiga #1481](https://tree.taiga.io/project/penpot/issue/1481)
|
||||
- Fix issue with mask and flip [#715](https://github.com/penpot/penpot/issues/715)
|
||||
- Fix issue with masks interactions outside bounds [#718](https://github.com/penpot/penpot/issues/718)
|
||||
- Fix issue with middle mouse button press moving the canvas when not moving mouse [#717](https://github.com/penpot/penpot/issues/717)
|
||||
- Fix issue with resolved comments [Taiga #1406](https://tree.taiga.io/project/penpot/issue/1406)
|
||||
- Fix issue with rotated blur [Taiga #1370](https://tree.taiga.io/project/penpot/issue/1370)
|
||||
- Fix issue with rotation degree input [#741](https://github.com/penpot/penpot/issues/741)
|
||||
- Fix issue with system shortcuts and application [#737](https://github.com/penpot/penpot/issues/737)
|
||||
- Fix issue with team management in dashboard [Taiga #1475](https://tree.taiga.io/project/penpot/issue/1475)
|
||||
- Fix issue with typographies panel cannot be collapsed [#707](https://github.com/penpot/penpot/issues/707)
|
||||
- Fix text selection in comments [#745](https://github.com/penpot/penpot/issues/745)
|
||||
- Update Work-Sans font [#744](https://github.com/penpot/penpot/issues/744)
|
||||
- Fix issue with recent files not showing [Taiga #1493](https://tree.taiga.io/project/penpot/issue/1493)
|
||||
- Fix issue when promoting to owner [Taiga #1494](https://tree.taiga.io/project/penpot/issue/1494)
|
||||
- Fix cannot click on blocked elements in viewer [Taiga #1430](https://tree.taiga.io/project/penpot/issue/1430)
|
||||
- Fix SVG not showing properties at code [Taiga #1437](https://tree.taiga.io/project/penpot/issue/1437)
|
||||
- Fix shadows when exporting groups [Taiga #1495](https://tree.taiga.io/project/penpot/issue/1495)
|
||||
- Fix drag-select when renaming layer text [Taiga #1307](https://tree.taiga.io/project/penpot/issue/1307)
|
||||
- Fix layout problem for editable selects [Taiga #1488](https://tree.taiga.io/project/penpot/issue/1488)
|
||||
- Fix artboard title wasn't move when resizing [Taiga #1479](https://tree.taiga.io/project/penpot/issue/1479)
|
||||
- Fix titles in viewer thumbnails too long [Taiga #1474](https://tree.taiga.io/project/penpot/issue/1474)
|
||||
- Fix when right click on a selected text shows artboard contextual menu [Taiga #1226](https://tree.taiga.io/project/penpot/issue/1226)
|
||||
|
||||
### :boom: Breaking changes
|
||||
|
||||
- The LDAP configuration variables interpolation starts using `:`
|
||||
(example `:username`) instead of `$`. The main reason is avoid
|
||||
unnecessary conflict with bash interpolation.
|
||||
|
||||
### :arrow_up: Deps updates
|
||||
|
||||
- Update backend to JDK16.
|
||||
- Update exporter nodejs to v14.16.0
|
||||
|
||||
### :heart: Community contributions by (Thank you!)
|
||||
|
||||
- iblueer [#726](https://github.com/penpot/penpot/pull/726)
|
||||
- gizembln [#759](https://github.com/penpot/penpot/pull/759)
|
||||
- girafic [#748](https://github.com/penpot/penpot/pull/748)
|
||||
- mbrksntrk [#794](https://github.com/penpot/penpot/pull/794)
|
||||
|
||||
## 1.3.0-alpha
|
||||
|
||||
### :sparkles: New features
|
||||
|
||||
- Add emailcatcher and ldap test containers to devenv. [#506](https://github.com/penpot/penpot/pull/506)
|
||||
- Add major refactor of internal pubsub/redis code; improves scalability and performance [#640](https://github.com/penpot/penpot/pull/640)
|
||||
- Add more chinese translations [#687](https://github.com/penpot/penpot/pull/687)
|
||||
- Add more presets for artboard [#654](https://github.com/penpot/penpot/pull/654)
|
||||
- Add optional loki integration [#645](https://github.com/penpot/penpot/pull/645)
|
||||
- Add proper http session lifecycle handling.
|
||||
- Allow to set border radius of each rect corner individually
|
||||
- Bounce & Complaint handling [#635](https://github.com/penpot/penpot/pull/635)
|
||||
- Disable groups interactions when holding "Ctrl" key (deep selection)
|
||||
- New action in context menu to "edit" some shapes (bound to key "Enter")
|
||||
|
||||
### :bug: Bugs fixed
|
||||
|
||||
- Add more improvements to french translation strings [#591](https://github.com/penpot/penpot/pull/591)
|
||||
- Add some missing database indexes (mainly improves performance on large databases on file-update rpc method, and some background tasks)
|
||||
- Disables filters in masking elements (issue with Firefox rendering)
|
||||
- Drawing tool will have priority over resize/rotate handlers [Taiga #1225](https://tree.taiga.io/project/penpot/issue/1225)
|
||||
- Fix broken bounding box on editing paths [Taiga #1254](https://tree.taiga.io/project/penpot/issue/1254)
|
||||
- Fix corner cases on invitation/signup flows.
|
||||
- Fix errors on onboarding file [Taiga #1287](https://tree.taiga.io/project/penpot/issue/1287)
|
||||
- Fix infinite recursion on logout.
|
||||
- Fix issues with frame selection [Taiga #1300](https://tree.taiga.io/project/penpot/issue/1300), [Taiga #1255](https://tree.taiga.io/project/penpot/issue/1255)
|
||||
- Fix local fonts error [#691](https://github.com/penpot/penpot/issues/691)
|
||||
- Fix issue width handoff code generation [Taiga #1204](https://tree.taiga.io/project/penpot/issue/1204)
|
||||
- Fix issue with indices refreshing on page changes [#646](https://github.com/penpot/penpot/issues/646)
|
||||
- Have language change notification written in the new language [Taiga #1205](https://tree.taiga.io/project/penpot/issue/1205)
|
||||
- Hide register screen when registration is disabled [#598](https://github.com/penpot/penpot/issues/598)
|
||||
- Properly handle errors on github, gitlab and ldap auth backends.
|
||||
- Properly mark profile auth backend (on first register/ auth with 3rd party auth provider)
|
||||
- Refactor LDAP auth backend.
|
||||
|
||||
### :heart: Community contributions by (Thank you!)
|
||||
|
||||
- girafic [#538](https://github.com/penpot/penpot/pull/654)
|
||||
- arkhi [#591](https://github.com/penpot/penpot/pull/591)
|
||||
|
||||
## 1.2.0-alpha
|
||||
|
||||
### :sparkles: New features
|
||||
|
||||
- Add horizontal/vertical flip
|
||||
- Add images lock proportions by default [#541](https://github.com/penpot/penpot/discussions/541), [#609](https://github.com/penpot/penpot/issues/609)
|
||||
- Add new blob storage format (Zstd+nippy)
|
||||
- Add user feedback form
|
||||
- Improve French translations
|
||||
- Improve component testing
|
||||
- Increase default deletion delay to 7 days
|
||||
- Show a pixel grid when zoom greater than 800% [#519](https://github.com/penpot/penpot/discussions/519)
|
||||
- Fix behavior of select all command when there are objects outside frames [Taiga #1209](https://tree.taiga.io/project/penpot/issue/1209)
|
||||
|
||||
### :bug: Bugs fixed
|
||||
|
||||
- Fix 404 when access shared link [#615](https://github.com/penpot/penpot/issues/615)
|
||||
- Fix 500 when requestion password reset
|
||||
- Fix issue when transforming path shapes [Taiga #1170](https://tree.taiga.io/project/penpot/issue/1170)
|
||||
- Fix apply a color to a text selection from color palette was not working [Taiga #1189](https://tree.taiga.io/project/penpot/issue/1189)
|
||||
- Fix issues when moving shapes outside groups [Taiga #1138](https://tree.taiga.io/project/penpot/issue/1138)
|
||||
- Fix ldap function called on login click
|
||||
- Fix logo icon in viewer should go to dashboard [Taiga #1149](https://tree.taiga.io/project/penpot/issue/1149)
|
||||
- Fix ordering when restoring deleted shapes in sync [Taiga #1163](https://tree.taiga.io/project/penpot/issue/1163)
|
||||
- Fix issue when editing text immediately after creating [Taiga #1207](https://tree.taiga.io/project/penpot/issue/1207)
|
||||
- Fix issue when pasting URL's copied from the browser url bar [Taiga #1187](https://tree.taiga.io/project/penpot/issue/1187)
|
||||
- Fix issue with multiple selection and groups [Taiga #1128](https://tree.taiga.io/project/penpot/issue/1128)
|
||||
- Fix issue with red handler indicator on resize [Taiga #1188](https://tree.taiga.io/project/penpot/issue/1188)
|
||||
- Fix show correct error when google auth is disabled [Taiga #1119](https://tree.taiga.io/project/penpot/issue/1119)
|
||||
- Fix text alignment in preview [#594](https://github.com/penpot/penpot/issues/594)
|
||||
- Fix unexpected exception when uploading image [Taiga #1120](https://tree.taiga.io/project/penpot/issue/1120)
|
||||
- Fix updates on collaborative editing not updating selection rectangles [Taiga #1127](https://tree.taiga.io/project/penpot/issue/1127)
|
||||
- Make the team deletion deferred (in the same way other objects)
|
||||
|
||||
### :heart: Community contributions by (Thank you!)
|
||||
|
||||
- abtinmo [#538](https://github.com/penpot/penpot/pull/538)
|
||||
- kdrag0n [#585](https://github.com/penpot/penpot/pull/585)
|
||||
- nisrulz [#586](https://github.com/penpot/penpot/pull/586)
|
||||
- tomer [#575](https://github.com/penpot/penpot/pull/575)
|
||||
- violoncelloCH [#554](https://github.com/penpot/penpot/pull/554)
|
||||
|
||||
## 1.1.0-alpha
|
||||
|
||||
- Bugfixing and stabilization post-launch
|
||||
- Some changes to the register flow
|
||||
- Improved MacOS shortcuts and helpers
|
||||
- Small changes to shape creation
|
||||
|
||||
## 1.0.0-alpha
|
||||
|
||||
Initial release
|
||||
@@ -1,8 +1,8 @@
|
||||
# Contributing Guide #
|
||||
|
||||
Thank you for your interest in contributing to Penpot. This is a
|
||||
generic guide that details how to contribute to Penpot in a way that is
|
||||
efficient for everyone. If you want a specific documentation for
|
||||
generic guide that details how to contribute to Penpot in a way that
|
||||
is efficient for everyone. If you want a specific documentation for
|
||||
different parts of the platform, please refer to `docs/` directory.
|
||||
|
||||
|
||||
@@ -19,12 +19,20 @@ If you found a bug, please report it, as far as possible with:
|
||||
- a browser and the browser version used
|
||||
- a dev tools console exception stack trace (if it is available)
|
||||
|
||||
If you found a bug that you consider better discuss in private (for
|
||||
example: security bugs), consider first send an email to
|
||||
`support@penpot.app`.
|
||||
|
||||
**We don't have formal bug bounty program for security reports; this
|
||||
is an open source application and your contribution will be recognized
|
||||
in the changelog.**
|
||||
|
||||
|
||||
## Pull requests ##
|
||||
|
||||
If you want propose a change or bug fix with the Pull-Request system
|
||||
firstly you should carefully read the **Contributor License Aggreement**
|
||||
section and format your commits accordingly.
|
||||
firstly you should carefully read the **DCO** section and format your
|
||||
commits accordingly.
|
||||
|
||||
If you intend to fix a bug it's fine to submit a pull request right
|
||||
away but we still recommend to file an issue detailing what you're
|
||||
@@ -46,7 +54,7 @@ We will use the `easy fix` mark for tag for indicate issues that are
|
||||
easy for beginners.
|
||||
|
||||
|
||||
## Commit Message Guidelines ##
|
||||
## Commit Guidelines ##
|
||||
|
||||
We have very precise rules over how our git commit messages can be formatted.
|
||||
|
||||
@@ -70,7 +78,6 @@ Where type is:
|
||||
- :ambulance: `:ambulance:` a commit that fixes critical bug
|
||||
- :books: `:books:` a commit that improves or adds documentation
|
||||
- :construction: `:construction:`: a wip commit
|
||||
- :construction_worker: `:construction_worker:` a commit with CI related stuff
|
||||
- :boom: `:boom:` a commit with breaking changes
|
||||
- :wrench: `:wrench:` a commit for config updates
|
||||
- :zap: `:zap:` a commit with performance improvements
|
||||
@@ -83,13 +90,14 @@ More info:
|
||||
- https://gist.github.com/parmentf/035de27d6ed1dce0b36a
|
||||
- https://gist.github.com/rxaviers/7360908
|
||||
|
||||
The subject should be:
|
||||
|
||||
- Use the imperative mood.
|
||||
- Capitalize the first letter.
|
||||
- Don't put a period at the end of the subject line.
|
||||
- Put a blank line between the subject line and the body.
|
||||
Each commit should have:
|
||||
|
||||
- A concise subject using imperative mood.
|
||||
- The subject should have capitalized the first letter and without
|
||||
period at the end.
|
||||
- A blank line between the subject line and the body.
|
||||
- An entry on the CHANGES.md file if applicable, referencing the
|
||||
github or taiga issue/user-story using the these same rules.
|
||||
|
||||
## Code of conduct ##
|
||||
|
||||
@@ -127,7 +135,7 @@ This Code of Conduct is adapted from the Contributor Covenant, version
|
||||
1.1.0, available from http://contributor-covenant.org/version/1/1/0/
|
||||
|
||||
|
||||
## Contributor License Agreement ##
|
||||
## Developer's Certificate of Origin (DCO) ##
|
||||
|
||||
By submitting code you are agree and can certify the below:
|
||||
|
||||
@@ -157,9 +165,9 @@ By submitting code you are agree and can certify the below:
|
||||
maintained indefinitely and may be redistributed consistent with
|
||||
this project or the open source license(s) involved.
|
||||
|
||||
Then, all your patches should contain a sign-off at the end of the
|
||||
patch/commit description body. It can be automatically added on adding
|
||||
`-s` parameter to `git commit`.
|
||||
Then, all your code patches (**documentation are excluded**) should
|
||||
contain a sign-off at the end of the patch/commit description body. It
|
||||
can be automatically added on adding `-s` parameter to `git commit`.
|
||||
|
||||
This is an example of the aspect of the line:
|
||||
|
||||
|
||||
75
README.md
@@ -2,35 +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://tree.taiga.io/project/uxbox/ "Managed with Taiga.io")
|
||||
<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? ##
|
||||
|
||||
We’re excited to share that Uxbox is now Penpot! We’re changing the name, but keeping the same project essence. Stay in the loop for more news coming early 2021. Alpha release is close!
|
||||
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 ##
|
||||
|
||||
## Introduction ##
|
||||
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.
|
||||
|
||||
The open-source solution for design and prototyping. PENPOT is
|
||||
currently at an early development stage but we are working hard to
|
||||
bring you the beta version as soon as possible. Follow the project
|
||||
progress in Twitter or Github and stay tuned!
|
||||
✏️ [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**.
|
||||
|
||||
## SVG based ##
|
||||
🐳 [Install docker](https://help.penpot.app/technical-guide/getting-started/)
|
||||
|
||||
Penpot works with SVG, a standard format, for all your designs and
|
||||
prototypes . This means that all your stuff in Penpot is portable and
|
||||
editable in many other vector tools and easy to use on the web.
|
||||
## Help center ##
|
||||
|
||||
[See SVG specification](https://www.w3.org/Graphics/SVG/)
|
||||
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
|
||||
@@ -39,6 +64,24 @@ and improve Penpot. All your awesome ideas and code are welcome!
|
||||
|
||||
Please refer to the [Contributing Guide](./CONTRIBUTING.md)
|
||||
|
||||
## Give feedback ##
|
||||
|
||||
You can ask and answer questions, have open-ended conversations, and follow along on decisions affecting the project.
|
||||
|
||||
✉️ [Mail us](mailto:info@penpot.app)
|
||||
|
||||
💬 [GitHub discussions](https://github.com/penpot/penpot/discussions)
|
||||
|
||||
🐞 [GitHub issues](https://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 ##
|
||||
|
||||
@@ -46,4 +89,6 @@ Please refer to the [Contributing Guide](./CONTRIBUTING.md)
|
||||
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
|
||||
```
|
||||
|
||||
36
backend/build.clj
Normal file
@@ -0,0 +1,36 @@
|
||||
(ns build
|
||||
(:refer-clojure :exclude [compile])
|
||||
(:require
|
||||
[clojure.tools.build.api :as b]
|
||||
[clojure.java.io]))
|
||||
|
||||
(def class-dir "target/classes")
|
||||
(def basis (b/create-basis {:project "deps.edn"}))
|
||||
(def jar-file "target/penpot.jar")
|
||||
|
||||
(defn clean [_]
|
||||
(b/delete {:path "target"}))
|
||||
|
||||
(defn jar [_]
|
||||
(b/copy-dir
|
||||
{:src-dirs ["src" "resources"]
|
||||
:target-dir class-dir})
|
||||
|
||||
(b/compile-clj
|
||||
{:basis basis
|
||||
:src-dirs ["src"]
|
||||
:class-dir class-dir})
|
||||
|
||||
(b/uber
|
||||
{:class-dir class-dir
|
||||
:uber-file jar-file
|
||||
:main 'clojure.main
|
||||
:exclude [#"goog.*" #"^javasist.*"]
|
||||
:basis basis}))
|
||||
|
||||
(defn compile [_]
|
||||
(b/javac
|
||||
{:src-dirs ["dev/java"]
|
||||
:class-dir class-dir
|
||||
:basis basis
|
||||
:javac-opts ["-source" "11" "-target" "11"]}))
|
||||
136
backend/deps.edn
@@ -1,104 +1,78 @@
|
||||
{: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.1"}
|
||||
org.clojure/clojurescript {:mvn/version "1.10.773"}
|
||||
org.clojure/data.json {:mvn/version "1.0.0"}
|
||||
org.clojure/core.async {:mvn/version "1.3.610"}
|
||||
{:deps
|
||||
{penpot/common {:local/root "../common"}
|
||||
org.clojure/core.async {:mvn/version "1.5.648"}
|
||||
|
||||
;; Logging
|
||||
org.clojure/tools.logging {:mvn/version "1.1.0"}
|
||||
org.apache.logging.log4j/log4j-api {:mvn/version "2.13.3"}
|
||||
org.apache.logging.log4j/log4j-core {:mvn/version "2.13.3"}
|
||||
org.apache.logging.log4j/log4j-web {:mvn/version "2.13.3"}
|
||||
org.apache.logging.log4j/log4j-jul {:mvn/version "2.13.3"}
|
||||
org.apache.logging.log4j/log4j-slf4j-impl {:mvn/version "2.13.3"}
|
||||
org.slf4j/slf4j-api {:mvn/version "1.7.30"}
|
||||
org.zeromq/jeromq {:mvn/version "0.5.2"}
|
||||
|
||||
io.prometheus/simpleclient {:mvn/version "0.9.0"}
|
||||
io.prometheus/simpleclient_hotspot {:mvn/version "0.9.0"}
|
||||
io.prometheus/simpleclient_httpserver {:mvn/version "0.9.0"}
|
||||
com.taoensso/nippy {:mvn/version "3.1.1"}
|
||||
com.github.luben/zstd-jni {:mvn/version "1.5.2-1"}
|
||||
org.clojure/data.fressian {:mvn/version "1.0.0"}
|
||||
|
||||
selmer/selmer {:mvn/version "1.12.28"}
|
||||
expound/expound {:mvn/version "0.8.5"}
|
||||
com.cognitect/transit-clj {:mvn/version "1.0.324"}
|
||||
io.prometheus/simpleclient {:mvn/version "0.14.1"}
|
||||
io.prometheus/simpleclient_hotspot {:mvn/version "0.14.1"}
|
||||
io.prometheus/simpleclient_jetty {:mvn/version "0.14.1"
|
||||
:exclusions [org.eclipse.jetty/jetty-server
|
||||
org.eclipse.jetty/jetty-servlet]}
|
||||
io.prometheus/simpleclient_httpserver {:mvn/version "0.14.1"}
|
||||
|
||||
io.lettuce/lettuce-core {:mvn/version "5.2.2.RELEASE"}
|
||||
java-http-clj/java-http-clj {:mvn/version "0.4.1"}
|
||||
io.lettuce/lettuce-core {:mvn/version "6.1.6.RELEASE"}
|
||||
java-http-clj/java-http-clj {:mvn/version "0.4.3"}
|
||||
|
||||
info.sunng/ring-jetty9-adapter {:mvn/version "0.14.0"}
|
||||
seancorfield/next.jdbc {:mvn/version "1.1.588"}
|
||||
metosin/reitit-ring {:mvn/version "0.5.5"}
|
||||
org.postgresql/postgresql {:mvn/version "42.2.16"}
|
||||
com.zaxxer/HikariCP {:mvn/version "3.4.5"}
|
||||
funcool/yetti {:git/tag "v4.0" :git/sha "59ed2a7"
|
||||
:git/url "https://github.com/funcool/yetti.git"
|
||||
:exclusions [org.slf4j/slf4j-api]}
|
||||
|
||||
funcool/log4j2-clojure {:mvn/version "2020.11.23-1"}
|
||||
funcool/datoteka {:mvn/version "1.2.0"}
|
||||
funcool/promesa {:mvn/version "5.1.0"}
|
||||
funcool/cuerdas {:mvn/version "2020.03.26-3"}
|
||||
com.github.seancorfield/next.jdbc {:mvn/version "1.2.761"}
|
||||
metosin/reitit-ring {:mvn/version "0.5.15"}
|
||||
org.postgresql/postgresql {:mvn/version "42.3.2"}
|
||||
com.zaxxer/HikariCP {:mvn/version "5.0.1"}
|
||||
funcool/datoteka {:mvn/version "2.0.0"}
|
||||
|
||||
buddy/buddy-core {:mvn/version "1.9.0"}
|
||||
buddy/buddy-hashers {:mvn/version "1.7.0"}
|
||||
buddy/buddy-sign {:mvn/version "3.3.0"}
|
||||
buddy/buddy-hashers {:mvn/version "1.8.158"}
|
||||
buddy/buddy-sign {:mvn/version "3.4.333"}
|
||||
|
||||
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.3"}
|
||||
org.im4java/im4java {:mvn/version "1.4.0"}
|
||||
org.lz4/lz4-java {:mvn/version "1.7.1"}
|
||||
com.github.spullara.mustache.java/compiler {:mvn/version "0.9.6"}
|
||||
commons-io/commons-io {:mvn/version "2.8.0"}
|
||||
com.draines/postal {:mvn/version "2.0.3"
|
||||
:exclusions [commons-codec/commons-codec]}
|
||||
org.lz4/lz4-java {:mvn/version "1.8.0"}
|
||||
|
||||
puppetlabs/clj-ldap {:mvn/version"0.3.0"}
|
||||
org.clojars.pntblnk/clj-ldap {:mvn/version "0.0.17"}
|
||||
integrant/integrant {:mvn/version "0.8.0"}
|
||||
|
||||
;; exception printing
|
||||
io.aviso/pretty {:mvn/version "0.1.37"}
|
||||
io.sentry/sentry {:mvn/version "5.6.1"}
|
||||
|
||||
mount/mount {:mvn/version "0.1.16"}
|
||||
environ/environ {:mvn/version "1.2.0"}}
|
||||
:paths ["src" "resources" "../common" "common"]
|
||||
;; Pretty Print specs
|
||||
pretty-spec/pretty-spec {:mvn/version "0.1.4"}
|
||||
software.amazon.awssdk/s3 {:mvn/version "2.17.122"}}
|
||||
|
||||
:paths ["src" "resources" "target/classes"]
|
||||
:aliases
|
||||
{:dev
|
||||
{:extra-deps
|
||||
{com.bhauman/rebel-readline {:mvn/version "0.1.4"}
|
||||
org.clojure/tools.namespace {:mvn/version "1.0.0"}
|
||||
org.clojure/test.check {:mvn/version "1.0.0"}
|
||||
clj-kondo/clj-kondo {:mvn/version "RELEASE"}
|
||||
{com.bhauman/rebel-readline {:mvn/version "RELEASE"}
|
||||
org.clojure/tools.namespace {:mvn/version "RELEASE"}
|
||||
org.clojure/test.check {:mvn/version "RELEASE"}
|
||||
clojure-humanize/clojure-humanize {:mvn/version "0.2.2"}
|
||||
org.clojure/data.csv {:mvn/version "RELEASE"}
|
||||
com.clojure-goes-fast/clj-async-profiler {:mvn/version "RELEASE"}
|
||||
mockery/mockery {:mvn/version "RELEASE"}}
|
||||
:extra-paths ["test" "dev"]}
|
||||
|
||||
fipp/fipp {:mvn/version "0.6.21"}
|
||||
criterium/criterium {:mvn/version "0.4.5"}
|
||||
mockery/mockery {:mvn/version "0.1.4"}}
|
||||
:extra-paths ["tests"]}
|
||||
:build
|
||||
{:extra-deps
|
||||
{io.github.clojure/tools.build {:git/tag "v0.7.5" :git/sha "34727f7"}}
|
||||
:ns-default build}
|
||||
|
||||
;; :fn-media-loader
|
||||
;; {:exec-fn app.cli.media-loader/run
|
||||
;; :args {}}
|
||||
|
||||
:fn-fixtures
|
||||
{:exec-fn app.cli.fixtures/run
|
||||
:args {}}
|
||||
|
||||
:lint
|
||||
{:main-opts ["-m" "clj-kondo.main"]}
|
||||
|
||||
:tests
|
||||
{:extra-deps {lambdaisland/kaocha {:mvn/version "0.0-581"}}
|
||||
:main-opts ["-m" "kaocha.runner"]}
|
||||
:test
|
||||
{:extra-paths ["test"]
|
||||
:extra-deps
|
||||
{io.github.cognitect-labs/test-runner
|
||||
{:git/tag "v0.5.0" :git/sha "b3fd0d2"}}
|
||||
:exec-fn cognitect.test-runner.api/test}
|
||||
|
||||
:outdated
|
||||
{:extra-deps {olical/depot {:mvn/version "1.8.4"}}
|
||||
:main-opts ["-m" "depot.outdated.main"]}
|
||||
|
||||
:jar
|
||||
{:extra-deps {seancorfield/depstar {:mvn/version "RELEASE"}}
|
||||
:main-opts ["-m" "hf.depstar.jar" "-S" "target/app.jar"]}
|
||||
{:extra-deps {com.github.liquidz/antq {:mvn/version "RELEASE"}}
|
||||
:main-opts ["-m" "antq.core"]}
|
||||
|
||||
:jmx-remote
|
||||
{:jvm-opts ["-Dcom.sun.management.jmxremote"
|
||||
|
||||
95
backend/dev/user.clj
Normal file
@@ -0,0 +1,95 @@
|
||||
;; 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 user
|
||||
(:require
|
||||
[app.common.data :as d]
|
||||
[app.common.exceptions :as ex]
|
||||
[app.common.geom.matrix :as gmt]
|
||||
[app.common.perf :as perf]
|
||||
[app.common.transit :as t]
|
||||
[app.config :as cfg]
|
||||
[app.main :as main]
|
||||
[app.util.blob :as blob]
|
||||
[app.util.fressian :as fres]
|
||||
[app.util.json :as json]
|
||||
[app.util.time :as dt]
|
||||
[clj-async-profiler.core :as prof]
|
||||
[clojure.contrib.humanize :as hum]
|
||||
[clojure.java.io :as io]
|
||||
[clojure.pprint :refer [pprint print-table]]
|
||||
[clojure.repl :refer :all]
|
||||
[clojure.spec.alpha :as s]
|
||||
[clojure.spec.gen.alpha :as sgen]
|
||||
[clojure.test :as test]
|
||||
[clojure.test :as test]
|
||||
[clojure.tools.namespace.repl :as repl]
|
||||
[clojure.walk :refer [macroexpand-all]]
|
||||
[datoteka.core]
|
||||
[integrant.core :as ig]))
|
||||
|
||||
(repl/disable-reload! (find-ns 'integrant.core))
|
||||
(set! *warn-on-reflection* true)
|
||||
|
||||
(defonce system nil)
|
||||
|
||||
;; --- Development Stuff
|
||||
|
||||
(defn- run-tests
|
||||
([] (run-tests #"^app.*-test$"))
|
||||
([o]
|
||||
(repl/refresh)
|
||||
(cond
|
||||
(instance? java.util.regex.Pattern o)
|
||||
(test/run-all-tests o)
|
||||
|
||||
(symbol? o)
|
||||
(if-let [sns (namespace o)]
|
||||
(do (require (symbol sns))
|
||||
(test/test-vars [(resolve o)]))
|
||||
(test/test-ns o)))))
|
||||
|
||||
(defn- start
|
||||
[]
|
||||
(alter-var-root #'system (fn [sys]
|
||||
(when sys (ig/halt! sys))
|
||||
(-> main/system-config
|
||||
(ig/prep)
|
||||
(ig/init))))
|
||||
:started)
|
||||
|
||||
(defn- stop
|
||||
[]
|
||||
(alter-var-root #'system (fn [sys]
|
||||
(when sys (ig/halt! sys))
|
||||
nil))
|
||||
:stoped)
|
||||
|
||||
(defn restart
|
||||
[]
|
||||
(stop)
|
||||
(repl/refresh :after 'user/start))
|
||||
|
||||
(defn restart-all
|
||||
[]
|
||||
(stop)
|
||||
(repl/refresh-all :after 'user/start))
|
||||
|
||||
(defn compression-bench
|
||||
[data]
|
||||
(let [humanize (fn [v] (hum/filesize v :binary true :format " %.4f "))]
|
||||
(print-table
|
||||
[{:v1 (humanize (alength (blob/encode data {:version 1})))
|
||||
:v2 (humanize (alength (blob/encode data {:version 2})))
|
||||
:v3 (humanize (alength (blob/encode data {:version 3})))
|
||||
:v4 (humanize (alength (blob/encode data {:version 4})))
|
||||
}])))
|
||||
|
||||
(defonce debug-tap
|
||||
(do
|
||||
(add-tap #(locking debug-tap
|
||||
(prn "tap debug:" %)))
|
||||
1))
|
||||
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
@@ -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
@@ -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>
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
{:icons
|
||||
[{:name "Material Design (Action)"
|
||||
:path "./material/action/svg/production"
|
||||
:regex #"^.*_48px\.svg$"}]
|
||||
|
||||
:images
|
||||
[{:name "Generic Collection 1"
|
||||
:path "./my-images/collection1/"
|
||||
:regex #"^.*\.(png|jpg|webp)$"}]}
|
||||
|
||||
|
||||
@@ -1,44 +0,0 @@
|
||||
{;; A secret key used for create tokens
|
||||
;; WARNING: this is a default secret key and
|
||||
;; it should be overwritten in production env.
|
||||
:secret "5qjiAn-QUpawUNqGP10UZKklSqbLKcdGY3sJpq0UUACpVXGg2HOFJCBejDWVHskhRyp7iHb4rjOLXX2ZjF-5cw"
|
||||
|
||||
:registration
|
||||
{
|
||||
:enabled true}
|
||||
|
||||
:smtp
|
||||
{:host "localhost" ;; Hostname of the desired SMTP server.
|
||||
:port 25 ;; Port of SMTP server.
|
||||
:user nil ;; Username to authenticate with (if authenticating).
|
||||
:pass nil ;; Password to authenticate with (if authenticating).
|
||||
:ssl false ;; Enables SSL encryption if value is truthy.
|
||||
:tls false ;; Enables TLS encryption if value is truthy.
|
||||
:enabled false ;; Enables SMTP if value is truthy.
|
||||
:noop true}
|
||||
|
||||
:auth-options {:alg :a256kw :enc :a128cbc-hs256}
|
||||
|
||||
:email {:reply-to "no-reply@uxbox.io"
|
||||
:from "no-reply@uxbox.io"
|
||||
:support "support@uxbox.io"}
|
||||
|
||||
:http {:port 6060
|
||||
:max-body-size 52428800
|
||||
:debug true}
|
||||
|
||||
:media
|
||||
{:directory "resources/public/media"
|
||||
:uri "http://localhost:6060/media/"}
|
||||
|
||||
:static
|
||||
{:directory "resources/public/static"
|
||||
:uri "http://localhost:6060/static/"}
|
||||
|
||||
:database
|
||||
{:adapter "postgresql"
|
||||
:username nil
|
||||
:password nil
|
||||
:database-name "uxbox"
|
||||
:server-name "localhost"
|
||||
:port-number 5432}}
|
||||
@@ -1,18 +0,0 @@
|
||||
{:migrations
|
||||
{:verbose false}
|
||||
|
||||
:media
|
||||
{:directory "/tmp/uxbox/media"
|
||||
:uri "http://localhost:6060/media/"}
|
||||
|
||||
:static
|
||||
{:directory "/tmp/uxbox/static"
|
||||
:uri "http://localhost:6060/static/"}
|
||||
|
||||
:database
|
||||
{:adapter "postgresql"
|
||||
:username nil
|
||||
:password nil
|
||||
:database-name "test"
|
||||
:server-name "localhost"
|
||||
:port-number 5432}}
|
||||
@@ -30,14 +30,14 @@
|
||||
for security reasons.
|
||||
</mj-text>
|
||||
<mj-text>Enjoy!</mj-text>
|
||||
<mj-text>The UXBOX team.</mj-text>
|
||||
<mj-text>The Penpot team.</mj-text>
|
||||
</mj-column>
|
||||
</mj-section>
|
||||
|
||||
<mj-section padding="24px 0 0 0">
|
||||
<mj-column width="425px">
|
||||
<mj-text align="center" font-size="14px" color="#64666A">
|
||||
UXBOX is the first Open Source prototyping platform that will be embraced by multidisciplinary teams.
|
||||
Penpot is the first Open Source prototyping platform that will be embraced by multidisciplinary teams.
|
||||
</mj-text>
|
||||
</mj-column>
|
||||
</mj-section>
|
||||
@@ -57,7 +57,7 @@
|
||||
<mj-section padding="0 0 24px 0">
|
||||
<mj-column>
|
||||
<mj-text align="center" font-size="14px" color="#64666A" line-height="150%">
|
||||
UXBOX © 2020 | Made with <3 and Open Source
|
||||
Penpot © 2020 | Made with <3 and Open Source
|
||||
</mj-text>
|
||||
</mj-column>
|
||||
</mj-section>
|
||||
|
||||
@@ -23,14 +23,14 @@
|
||||
Accept invite
|
||||
</mj-button>
|
||||
<mj-text>Enjoy!</mj-text>
|
||||
<mj-text>The UXBOX team.</mj-text>
|
||||
<mj-text>The Penpot team.</mj-text>
|
||||
</mj-column>
|
||||
</mj-section>
|
||||
|
||||
<mj-section padding="24px 0 0 0">
|
||||
<mj-column width="425px">
|
||||
<mj-text align="center" font-size="14px" color="#64666A">
|
||||
UXBOX is the first Open Source prototyping platform that will be embraced by multidisciplinary teams.
|
||||
Penpot is the first Open Source prototyping platform that will be embraced by multidisciplinary teams.
|
||||
</mj-text>
|
||||
</mj-column>
|
||||
</mj-section>
|
||||
@@ -50,7 +50,7 @@
|
||||
<mj-section padding="0 0 24px 0">
|
||||
<mj-column>
|
||||
<mj-text align="center" font-size="14px" color="#64666A" line-height="150%">
|
||||
UXBOX © 2020 | Made with <3 and Open Source
|
||||
Penpot © 2020 | Made with <3 and Open Source
|
||||
</mj-text>
|
||||
</mj-column>
|
||||
</mj-section>
|
||||
|
||||
@@ -32,14 +32,14 @@
|
||||
it. Your password won't be changed.
|
||||
</mj-text>
|
||||
<mj-text>Enjoy!</mj-text>
|
||||
<mj-text>The UXBOX team.</mj-text>
|
||||
<mj-text>The Penpot team.</mj-text>
|
||||
</mj-column>
|
||||
</mj-section>
|
||||
|
||||
<mj-section padding="24px 0 0 0">
|
||||
<mj-column width="425px">
|
||||
<mj-text align="center" font-size="14px" color="#64666A">
|
||||
UXBOX is the first Open Source prototyping platform that will be embraced by multidisciplinary teams.
|
||||
Penpot is the first Open Source prototyping platform that will be embraced by multidisciplinary teams.
|
||||
</mj-text>
|
||||
</mj-column>
|
||||
</mj-section>
|
||||
@@ -59,7 +59,7 @@
|
||||
<mj-section padding="0 0 24px 0">
|
||||
<mj-column>
|
||||
<mj-text align="center" font-size="14px" color="#64666A" line-height="150%">
|
||||
UXBOX © 2020 | Made with <3 and Open Source
|
||||
Penpot © 2020 | Made with <3 and Open Source
|
||||
</mj-text>
|
||||
</mj-column>
|
||||
</mj-section>
|
||||
|
||||
@@ -21,22 +21,22 @@
|
||||
<mj-column>
|
||||
<mj-text font-size="24px" font-weight="600">Hello {{name}}!</mj-text>
|
||||
<mj-text>
|
||||
Thanks for signing up for your UXBOX account! Please verify your
|
||||
email using the link below adn get started building mockups and
|
||||
Thanks for signing up for your Penpot account! Please verify your
|
||||
email using the link below and get started building mockups and
|
||||
prototypes today!
|
||||
</mj-text>
|
||||
<mj-button href="{{ public-uri }}/#/auth/verify-token?token={{token}}">
|
||||
Verify email
|
||||
</mj-button>
|
||||
<mj-text>Enjoy!</mj-text>
|
||||
<mj-text>The UXBOX team.</mj-text>
|
||||
<mj-text>The Penpot team.</mj-text>
|
||||
</mj-column>
|
||||
</mj-section>
|
||||
|
||||
<mj-section padding="24px 0 0 0">
|
||||
<mj-column width="425px">
|
||||
<mj-text align="center" font-size="14px" color="#64666A">
|
||||
UXBOX is the first Open Source prototyping platform that will be embraced by multidisciplinary teams.
|
||||
Penpot is the first Open Source prototyping platform that will be embraced by multidisciplinary teams.
|
||||
</mj-text>
|
||||
</mj-column>
|
||||
</mj-section>
|
||||
@@ -56,7 +56,7 @@
|
||||
<mj-section padding="0 0 24px 0">
|
||||
<mj-column>
|
||||
<mj-text align="center" font-size="14px" color="#64666A" line-height="150%">
|
||||
UXBOX © 2020 | Made with <3 and Open Source
|
||||
Penpot © 2020 | Made with <3 and Open Source
|
||||
</mj-text>
|
||||
</mj-column>
|
||||
</mj-section>
|
||||
|
||||
@@ -450,7 +450,7 @@
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;" width="100%">
|
||||
<tr>
|
||||
<td align="center" style="font-size:0px;padding:10px 25px;word-break:break-word;">
|
||||
<div style="font-family:Source Sans Pro, sans-serif;font-size:14px;line-height:150%;text-align:center;color:#64666A;">Penpot © 2020 | Made with <3 and Open Source</div>
|
||||
<div style="font-family:Source Sans Pro, sans-serif;font-size:14px;line-height:150%;text-align:center;color:#64666A;">Penpot @ 2021 | Made with <3 and Open Source</div>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
@@ -10,4 +10,4 @@ If you received this email by mistake, please consider changing your password
|
||||
for security reasons.
|
||||
|
||||
Enjoy!
|
||||
The UXBOX team.
|
||||
The Penpot team.
|
||||
|
||||
45
backend/resources/emails/feedback/en.html
Normal file
@@ -0,0 +1,45 @@
|
||||
<!doctype html>
|
||||
<html xmlns="http://www.w3.org/1999/xhtml">
|
||||
<head>
|
||||
<title></title>
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
|
||||
</head>
|
||||
<body>
|
||||
<p>
|
||||
<strong>Feedback from:</strong><br />
|
||||
{% if profile %}
|
||||
<span>
|
||||
<span>Name: </span>
|
||||
<span><code>{{profile.fullname}}</code></span>
|
||||
</span>
|
||||
<br />
|
||||
|
||||
<span>
|
||||
<span>Email: </span>
|
||||
<span>{{profile.email}}</span>
|
||||
</span>
|
||||
<br />
|
||||
|
||||
<span>
|
||||
<span>ID: </span>
|
||||
<span><code>{{profile.id}}</code></span>
|
||||
</span>
|
||||
{% else %}
|
||||
<span>
|
||||
<span>Email: </span>
|
||||
<span>{{profile.email}}</span>
|
||||
</span>
|
||||
{% endif %}
|
||||
</p>
|
||||
<p>
|
||||
<strong>Subject:</strong><br />
|
||||
<span>{{subject}}</span>
|
||||
</p>
|
||||
|
||||
<p>
|
||||
<strong>Message:</strong><br />
|
||||
{{content|linebreaks-br|safe}}
|
||||
</p>
|
||||
</body>
|
||||
</html>
|
||||
1
backend/resources/emails/feedback/en.subj
Normal file
@@ -0,0 +1 @@
|
||||
[PENPOT FEEDBACK]: {{subject}}
|
||||
9
backend/resources/emails/feedback/en.txt
Normal file
@@ -0,0 +1,9 @@
|
||||
{% if profile %}
|
||||
Feedback profile: {{profile.fullname}} <{{profile.email}}> / {{profile.id}}
|
||||
{% else %}
|
||||
Feedback from: {{email}}
|
||||
{% endif %}
|
||||
|
||||
Subject: {{subject}}
|
||||
|
||||
{{content}}
|
||||
@@ -440,7 +440,7 @@
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;" width="100%">
|
||||
<tr>
|
||||
<td align="center" style="font-size:0px;padding:10px 25px;word-break:break-word;">
|
||||
<div style="font-family:Source Sans Pro, sans-serif;font-size:14px;line-height:150%;text-align:center;color:#64666A;">Penpot © 2020 | Made with <3 and Open Source</div>
|
||||
<div style="font-family:Source Sans Pro, sans-serif;font-size:14px;line-height:150%;text-align:center;color:#64666A;">Penpot @ 2021 | Made with <3 and Open Source</div>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
@@ -1 +1 @@
|
||||
Inviation to join {{team}}
|
||||
Invitation to join {{team}}
|
||||
|
||||
@@ -7,4 +7,4 @@ Accept invitation using this link:
|
||||
{{ public-uri }}/#/auth/verify-token?token={{token}}
|
||||
|
||||
Enjoy!
|
||||
The UXBOX team.
|
||||
The Penpot team.
|
||||
|
||||
@@ -445,7 +445,7 @@
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;" width="100%">
|
||||
<tr>
|
||||
<td align="center" style="font-size:0px;padding:10px 25px;word-break:break-word;">
|
||||
<div style="font-family:Source Sans Pro, sans-serif;font-size:14px;line-height:150%;text-align:center;color:#64666A;">Penpot © 2020 | Made with <3 and Open Source</div>
|
||||
<div style="font-family:Source Sans Pro, sans-serif;font-size:14px;line-height:150%;text-align:center;color:#64666A;">Penpot @ 2021 | Made with <3 and Open Source</div>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
@@ -9,4 +9,4 @@ If you received this email by mistake, you can safely ignore it. Your password
|
||||
won't be changed.
|
||||
|
||||
Enjoy!
|
||||
The UXBOX team.
|
||||
The Penpot team.
|
||||
|
||||
@@ -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>
|
||||
@@ -440,7 +440,7 @@
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;" width="100%">
|
||||
<tr>
|
||||
<td align="center" style="font-size:0px;padding:10px 25px;word-break:break-word;">
|
||||
<div style="font-family:Source Sans Pro, sans-serif;font-size:14px;line-height:150%;text-align:center;color:#64666A;">Penpot © 2020 | Made with <3 and Open Source</div>
|
||||
<div style="font-family:Source Sans Pro, sans-serif;font-size:14px;line-height:150%;text-align:center;color:#64666A;">Penpot @ 2021 | Made with <3 and Open Source</div>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
@@ -465,4 +465,4 @@
|
||||
</div>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
</html>
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
Hello {{name}}!
|
||||
|
||||
Thanks for signing up for your UXBOX account! Please verify your email using the
|
||||
link below adn get started building mockups and prototypes today!
|
||||
Thanks for signing up for your Penpot account! Please verify your email using the
|
||||
link below and get started building mockups and prototypes today!
|
||||
|
||||
{{ public-uri }}/#/auth/verify-token?token={{token}}
|
||||
|
||||
Enjoy!
|
||||
The UXBOX team.
|
||||
The Penpot team.
|
||||
|
||||
@@ -1,26 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Configuration status="info" monitorInterval="60">
|
||||
<Appenders>
|
||||
<Console name="console" target="SYSTEM_OUT">
|
||||
<PatternLayout pattern="[%t] %level{length=1} %logger{36} - %msg%n"/>
|
||||
</Console>
|
||||
</Appenders>
|
||||
|
||||
<Loggers>
|
||||
<Logger name="com.zaxxer.hikari" level="error" additivity="false">
|
||||
<AppenderRef ref="console"/>
|
||||
</Logger>
|
||||
|
||||
<Logger name="org.eclipse.jetty" level="info" additivity="false">
|
||||
<AppenderRef ref="console"/>
|
||||
</Logger>
|
||||
|
||||
<Logger name="app" level="debug" additivity="false">
|
||||
<AppenderRef ref="console"/>
|
||||
</Logger>
|
||||
|
||||
<Root level="info">
|
||||
<AppenderRef ref="console"/>
|
||||
</Root>
|
||||
</Loggers>
|
||||
</Configuration>
|
||||
54
backend/resources/log4j2-devenv.xml
Normal file
@@ -0,0 +1,54 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Configuration status="info" monitorInterval="30">
|
||||
<Appenders>
|
||||
<Console name="console" target="SYSTEM_OUT">
|
||||
<PatternLayout pattern="[%d{YYYY-MM-dd HH:mm:ss.SSS}] %level{length=1} %logger{36} - %msg%n"/>
|
||||
</Console>
|
||||
|
||||
<RollingFile name="main" fileName="logs/main.log" filePattern="logs/main-%i.log">
|
||||
<PatternLayout pattern="[%d{YYYY-MM-dd HH:mm:ss.SSS}] %level{length=1} %logger{36} - %msg%n"/>
|
||||
<Policies>
|
||||
<SizeBasedTriggeringPolicy size="50M"/>
|
||||
</Policies>
|
||||
<DefaultRolloverStrategy max="9"/>
|
||||
</RollingFile>
|
||||
|
||||
<JeroMQ name="zmq">
|
||||
<Property name="endpoint">tcp://localhost:45556</Property>
|
||||
<JsonLayout complete="false" compact="true" includeTimeMillis="true" stacktraceAsString="true" properties="true" />
|
||||
</JeroMQ>
|
||||
</Appenders>
|
||||
|
||||
<Loggers>
|
||||
<Logger name="com.zaxxer.hikari" level="error"/>
|
||||
<Logger name="io.lettuce" level="error" />
|
||||
<Logger name="org.eclipse.jetty" level="error" />
|
||||
<Logger name="org.postgresql" level="error" />
|
||||
|
||||
<Logger name="app.cli" level="debug" additivity="false">
|
||||
<AppenderRef ref="console"/>
|
||||
</Logger>
|
||||
|
||||
<Logger name="app.loggers" level="debug" additivity="false">
|
||||
<AppenderRef ref="main" level="debug" />
|
||||
</Logger>
|
||||
|
||||
<Logger name="app" level="all" additivity="false">
|
||||
<AppenderRef ref="main" level="trace" />
|
||||
<AppenderRef ref="zmq" level="debug" />
|
||||
</Logger>
|
||||
|
||||
<Logger name="penpot" level="debug" additivity="false">
|
||||
<AppenderRef ref="main" level="debug" />
|
||||
<AppenderRef ref="zmq" level="debug" />
|
||||
</Logger>
|
||||
|
||||
<Logger name="user" level="trace" additivity="false">
|
||||
<AppenderRef ref="main" level="trace" />
|
||||
</Logger>
|
||||
|
||||
<Root level="info">
|
||||
<AppenderRef ref="main" />
|
||||
</Root>
|
||||
</Loggers>
|
||||
</Configuration>
|
||||
@@ -1,43 +1,25 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Configuration status="info" monitorInterval="30">
|
||||
<Configuration status="info" monitorInterval="60">
|
||||
<Appenders>
|
||||
<Console name="console" target="SYSTEM_OUT">
|
||||
<PatternLayout pattern="[%d{YYYY-MM-dd HH:mm:ss.SSS}] [%t] %level{length=1} %logger{36} - %msg%n"/>
|
||||
<PatternLayout pattern="[%d{YYYY-MM-dd HH:mm:ss.SSS}] %level{length=1} %logger{36} - %msg%n"/>
|
||||
</Console>
|
||||
|
||||
<RollingFile name="main" fileName="logs/main.log" filePattern="logs/main-%i.log">
|
||||
<PatternLayout pattern="[%d{YYYY-MM-dd HH:mm:ss.SSS}] [%t] %level{length=1} %logger{36} - %msg%n"/>
|
||||
<Policies>
|
||||
<SizeBasedTriggeringPolicy size="50M"/>
|
||||
</Policies>
|
||||
<DefaultRolloverStrategy max="9"/>
|
||||
</RollingFile>
|
||||
|
||||
<CljFn name="error-reporter" ns="app.error-reporter" fn="enqueue">
|
||||
<PatternLayout pattern="[%d{YYYY-MM-dd HH:mm:ss.SSS}] [%t] %level{length=1} %logger{36} - %msg%n"/>
|
||||
</CljFn>
|
||||
</Appenders>
|
||||
|
||||
<Loggers>
|
||||
<Logger name="com.zaxxer.hikari" level="error" additivity="false" />
|
||||
<Logger name="org.eclipse.jetty" level="error" additivity="false" />
|
||||
<Logger name="io.lettuce" level="error" additivity="false" />
|
||||
|
||||
<Logger name="app.cli" level="debug" additivity="false">
|
||||
<AppenderRef ref="console"/>
|
||||
</Logger>
|
||||
|
||||
<Logger name="app.error-reporter" level="debug" additivity="false">
|
||||
<AppenderRef ref="console"/>
|
||||
</Logger>
|
||||
<Logger name="com.zaxxer.hikari" level="error" />
|
||||
<Logger name="org.eclipse.jetty" level="error" />
|
||||
|
||||
<Logger name="app" level="debug" additivity="false">
|
||||
<AppenderRef ref="main" level="debug" />
|
||||
<AppenderRef ref="error-reporter" level="error" />
|
||||
<AppenderRef ref="console" />
|
||||
</Logger>
|
||||
|
||||
<Logger name="penpot" level="fatal" additivity="false">
|
||||
<AppenderRef ref="console" />
|
||||
</Logger>
|
||||
|
||||
<Root level="info">
|
||||
<AppenderRef ref="main" />
|
||||
<AppenderRef ref="console" />
|
||||
</Root>
|
||||
</Loggers>
|
||||
</Configuration>
|
||||
|
||||
|
Before Width: | Height: | Size: 789 B |
|
Before Width: | Height: | Size: 38 KiB |
|
Before Width: | Height: | Size: 901 B |
|
Before Width: | Height: | Size: 6.8 KiB |
|
Before Width: | Height: | Size: 604 B |
|
Before Width: | Height: | Size: 668 B |
|
Before Width: | Height: | Size: 746 B |
18
backend/resources/templates/base.tmpl
Normal file
@@ -0,0 +1,18 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="robots" content="noindex,nofollow">
|
||||
<meta http-equiv="x-ua-compatible" content="ie=edge" />
|
||||
<title>{% block title %}{% endblock %}</title>
|
||||
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=JetBrains+Mono">
|
||||
<style>
|
||||
{% include "templates/styles.css" %}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
{% block content %}
|
||||
{% endblock %}
|
||||
</body>
|
||||
</html>
|
||||
|
||||
32
backend/resources/templates/debug.tmpl
Normal file
@@ -0,0 +1,32 @@
|
||||
{% extends "templates/base.tmpl" %}
|
||||
|
||||
{% block title %}
|
||||
Debug Main Page
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<nav>
|
||||
<h1>Debug INDEX:</h1>
|
||||
<div>[<a href="/dbg/error">ERRORS</a>]</div>
|
||||
</nav>
|
||||
<main class="index">
|
||||
<section>
|
||||
<h2>Download file data:</h2>
|
||||
<desc>Given an FILE-ID, downloads the file data as file. The file data is encoded using transit.</desc>
|
||||
<form method="get" action="/dbg/file/data">
|
||||
<input type="text" style="width:300px" name="file-id" placeholder="file-id" />
|
||||
<input type="hidden" name="download" value="1" />
|
||||
<input type="submit" value="Download" />
|
||||
</form>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2>Upload File Data:</h2>
|
||||
<desc>Create a new file on your draft projects using the file downloaded from the previous section.</desc>
|
||||
<form method="post" enctype="multipart/form-data" action="/dbg/file/data">
|
||||
<input type="file" name="file" value="" />
|
||||
<input type="submit" value="Upload" />
|
||||
</form>
|
||||
</section>
|
||||
</main>
|
||||
{% endblock %}
|
||||
18
backend/resources/templates/error-list.tmpl
Normal file
@@ -0,0 +1,18 @@
|
||||
{% extends "templates/base.tmpl" %}
|
||||
|
||||
{% block title %}
|
||||
penpot - error list
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<nav>
|
||||
<h1>Latest error reports:</h1>
|
||||
</nav>
|
||||
<main class="horizontal-list">
|
||||
<ul>
|
||||
{% for item in items %}
|
||||
<li><a href="/dbg/error/{{item.id}}">{{item.created-at}}</a></li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</main>
|
||||
{% endblock %}
|
||||
98
backend/resources/templates/error-report.tmpl
Normal file
@@ -0,0 +1,98 @@
|
||||
{% extends "templates/base.tmpl" %}
|
||||
|
||||
{% block title %}
|
||||
penpot - error report {{id}}
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<nav>
|
||||
<div>[<a href="/dbg/error">⮜</a>]</div>
|
||||
<div>[<a href="#context">context</a>]</div>
|
||||
<div>[<a href="#params">request params</a>]</div>
|
||||
{% if data %}
|
||||
<div>[<a href="#edata">error data</a>]</div>
|
||||
{% endif %}
|
||||
{% if spec-explain %}
|
||||
<div>[<a href="#spec-explain">spec explain</a>]</div>
|
||||
{% endif %}
|
||||
{% if spec-problems %}
|
||||
<div>[<a href="#spec-problems">spec problems</a>]</div>
|
||||
{% endif %}
|
||||
{% if spec-value %}
|
||||
<div>[<a href="#spec-value">spec value</a>]</div>
|
||||
{% endif %}
|
||||
|
||||
{% if trace %}
|
||||
<div>[<a href="#trace">error trace</a>]</div>
|
||||
{% endif %}
|
||||
</nav>
|
||||
<main>
|
||||
<div class="table">
|
||||
<div class="table-row multiline">
|
||||
<div id="context" class="table-key">CONTEXT: </div>
|
||||
|
||||
<div class="table-val">
|
||||
<h1>{{hint}}</h1>
|
||||
</div>
|
||||
|
||||
<div class="table-val">
|
||||
<pre>{{context}}</pre>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if params %}
|
||||
<div class="table-row multiline">
|
||||
<div id="params" class="table-key">REQUEST PARAMS: </div>
|
||||
<div class="table-val">
|
||||
<pre>{{params}}</pre>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if data %}
|
||||
<div class="table-row multiline">
|
||||
<div id="edata" class="table-key">ERROR DATA: </div>
|
||||
<div class="table-val">
|
||||
<pre>{{data}}</pre>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if spec-explain %}
|
||||
<div class="table-row multiline">
|
||||
<div id="spec-explain" class="table-key">SPEC EXPLAIN: </div>
|
||||
<div class="table-val">
|
||||
<pre>{{spec-explain}}</pre>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if spec-problems %}
|
||||
<div class="table-row multiline">
|
||||
<div id="spec-problems" class="table-key">SPEC PROBLEMS: </div>
|
||||
<div class="table-val">
|
||||
<pre>{{spec-problems}}</pre>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if spec-value %}
|
||||
<div class="table-row multiline">
|
||||
<div id="spec-value" class="table-key">SPEC VALUE: </div>
|
||||
<div class="table-val">
|
||||
<pre>{{spec-value}}</pre>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if trace %}
|
||||
<div class="table-row multiline">
|
||||
<div id="trace" class="table-key">TRACE:</div>
|
||||
<div class="table-val">
|
||||
<pre>{{trace}}</pre>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</main>
|
||||
{% endblock %}
|
||||
150
backend/resources/templates/styles.css
Normal file
@@ -0,0 +1,150 @@
|
||||
* {
|
||||
font-family: "JetBrains Mono", monospace;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0px;
|
||||
padding: 0px;
|
||||
}
|
||||
|
||||
pre {
|
||||
margin: 0px;
|
||||
line-height: 16px;
|
||||
}
|
||||
|
||||
desc {
|
||||
display: flex;
|
||||
margin-bottom: 10px;
|
||||
font-size: 10px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
input[type=text], input[type=submit] {
|
||||
padding: 3px;
|
||||
}
|
||||
|
||||
main {
|
||||
margin: 20px;
|
||||
}
|
||||
|
||||
nav {
|
||||
position: fixed;
|
||||
width: 100vw;
|
||||
top: 0;
|
||||
left: 0;
|
||||
padding: 5px 20px;
|
||||
display: flex;
|
||||
background: #e3e3e3;
|
||||
}
|
||||
|
||||
nav > h1 {
|
||||
padding: 0px;
|
||||
margin: 0px;
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
nav > div {
|
||||
text-transform: uppercase;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
nav > div:not(:last-child) {
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.table {
|
||||
margin-top: 25px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.table-row {
|
||||
display: flex;
|
||||
padding-bottom: 15px;
|
||||
/* width: 100%; */
|
||||
/* border: 1px solid red; */
|
||||
}
|
||||
|
||||
.table-key {
|
||||
font-weight: 600;
|
||||
width: 60px;
|
||||
padding: 4px;
|
||||
|
||||
padding-top: 40px;
|
||||
margin-top: -40px;
|
||||
}
|
||||
|
||||
.table-val {
|
||||
font-weight: 200;
|
||||
color: #333;
|
||||
padding: 4px;
|
||||
}
|
||||
|
||||
.multiline {
|
||||
margin-top: 15px;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.multiline .table-key {
|
||||
margin-bottom: 10px;
|
||||
border-bottom: 1px dashed #dddddd;
|
||||
/* padding: 4px; */
|
||||
width: unset;
|
||||
}
|
||||
|
||||
.index {
|
||||
margin-top: 40px;
|
||||
}
|
||||
|
||||
.index > section {
|
||||
padding: 10px;
|
||||
background-color: #e3e3e3;
|
||||
}
|
||||
|
||||
.index > section:not(:last-child) {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
|
||||
.index > section > h2 {
|
||||
margin-top: 0px;
|
||||
}
|
||||
|
||||
.horizontal-list {
|
||||
margin: 20px;
|
||||
margin-top: 40px;
|
||||
}
|
||||
|
||||
.horizontal-list ul {
|
||||
display: flex;
|
||||
margin: 0px;
|
||||
padding: 0px;
|
||||
flex-direction: column;
|
||||
flex-wrap: wrap;
|
||||
height: calc(100vh - 75px);
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
.horizontal-list li {
|
||||
list-style: none;
|
||||
padding: 0px;
|
||||
margin: 0px;
|
||||
line-height: 18px;
|
||||
min-width: 210px;
|
||||
margin: 0px 20px;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.horizontal-list li:hover {
|
||||
background-color: #e9e9e9;
|
||||
}
|
||||
|
||||
.horizontal-list li > a {
|
||||
text-decoration: none;
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
20
backend/scripts/build
Executable file
@@ -0,0 +1,20 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
CURRENT_VERSION=$1;
|
||||
|
||||
set -ex
|
||||
|
||||
rm -rf target;
|
||||
mkdir -p target/classes;
|
||||
mkdir -p target/dist;
|
||||
echo "$CURRENT_VERSION" > target/classes/version.txt;
|
||||
|
||||
clojure -T:build jar;
|
||||
mv target/penpot.jar target/dist/penpot.jar
|
||||
cp scripts/run.template.sh target/dist/run.sh;
|
||||
cp scripts/manage.template.sh target/dist/manage.sh;
|
||||
chmod +x target/dist/run.sh;
|
||||
chmod +x target/dist/manage.sh;
|
||||
|
||||
|
||||
|
||||
@@ -1,53 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
CLASSPATH=`(clojure -Spath)`
|
||||
NEWCP="./main:./common"
|
||||
|
||||
rm -rf ./target/dist
|
||||
mkdir -p ./target/dist/deps
|
||||
|
||||
for item in $(echo $CLASSPATH | tr ":" "\n"); do
|
||||
if [ "${item: -4}" == ".jar" ]; then
|
||||
cp $item ./target/dist/deps/;
|
||||
BN="$(basename -- $item)"
|
||||
NEWCP+=":./deps/$BN"
|
||||
fi
|
||||
done
|
||||
|
||||
cp ./resources/log4j2-bundle.xml ./target/dist/log4j2.xml
|
||||
cp -r ./src ./target/dist/main
|
||||
cp -r ./resources/emails ./target/dist/main/
|
||||
cp -r ../common ./target/dist/common
|
||||
|
||||
echo $NEWCP > ./target/dist/classpath;
|
||||
|
||||
tee -a ./target/dist/run.sh >> /dev/null <<EOF
|
||||
#!/usr/bin/env bash
|
||||
|
||||
CP="$NEWCP"
|
||||
|
||||
# Exports
|
||||
|
||||
# Find java executable
|
||||
set +e
|
||||
JAVA_CMD=\$(type -p java)
|
||||
|
||||
set -e
|
||||
if [[ ! -n "\$JAVA_CMD" ]]; then
|
||||
if [[ -n "\$JAVA_HOME" ]] && [[ -x "\$JAVA_HOME/bin/java" ]]; then
|
||||
JAVA_CMD="\$JAVA_HOME/bin/java"
|
||||
else
|
||||
>&2 echo "Couldn't find 'java'. Please set JAVA_HOME."
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ -f ./environ ]; then
|
||||
source ./environ
|
||||
fi
|
||||
|
||||
set -x
|
||||
exec \$JAVA_CMD \$JVM_OPTS -Dapp.enable-asserts=false -classpath \$CP -Dlog4j.configurationFile=./log4j2.xml "\$@" clojure.main -m app.main
|
||||
EOF
|
||||
|
||||
chmod +x ./target/dist/run.sh
|
||||
@@ -1,4 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
clojure -Adev -m app.cli.collimp $@
|
||||
|
||||
19
backend/scripts/manage.template.sh
Normal file
@@ -0,0 +1,19 @@
|
||||
#!/usr/bin/env bash
|
||||
set +e
|
||||
JAVA_CMD=$(type -p java)
|
||||
|
||||
set -e
|
||||
if [[ ! -n "$JAVA_CMD" ]]; then
|
||||
if [[ -n "$JAVA_HOME" ]] && [[ -x "$JAVA_HOME/bin/java" ]]; then
|
||||
JAVA_CMD="$JAVA_HOME/bin/java"
|
||||
else
|
||||
>&2 echo "Couldn't find 'java'. Please set JAVA_HOME."
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ -f ./environ ]; then
|
||||
source ./environ
|
||||
fi
|
||||
|
||||
exec $JAVA_CMD $JVM_OPTS -jar penpot.jar -m app.cli.manage "$@"
|
||||
@@ -1,16 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
if [ "$#" -e 0 ]; then
|
||||
echo "Expecting parameters: 1=path to backend; 2=destination directory"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
rm -rf $2 || exit 1;
|
||||
|
||||
rsync -avr \
|
||||
--exclude="/test" \
|
||||
--exclude="/resources/public/media" \
|
||||
--exclude="/target" \
|
||||
--exclude="/scripts" \
|
||||
--exclude="/.*" \
|
||||
$1 $2;
|
||||
@@ -1,2 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
PGPASSWORD=$APP_DATABASE_PASSWORD psql $APP_DATABASE_URI -U $APP_DATABASE_USERNAME
|
||||
@@ -1,6 +1,40 @@
|
||||
#!/usr/bin/env bash
|
||||
set -ex
|
||||
|
||||
# clojure -Ojmx-remote -A:dev -e "(set! *warn-on-reflection* true)" -m rebel-readline.main
|
||||
# clojure -Ojmx-remote -A:dev -J-XX:+UnlockExperimentalVMOptions -J-XX:+UseZGC -J-Xms128m -J-Xmx128m -m rebel-readline.main
|
||||
clojure -A:jmx-remote:dev -J-Xms128m -J-Xmx128m -M -m rebel-readline.main
|
||||
# export PENPOT_DATABASE_URI="postgresql://172.17.0.1:5432/penpot"
|
||||
# export PENPOT_DATABASE_USERNAME="penpot"
|
||||
# export PENPOT_DATABASE_PASSWORD="penpot"
|
||||
# export PENPOT_DATABASE_READONLY=true
|
||||
|
||||
# export PENPOT_DATABASE_URI="postgresql://172.17.0.1:5432/penpot_pre"
|
||||
# export PENPOT_DATABASE_USERNAME="penpot_pre"
|
||||
# export PENPOT_DATABASE_PASSWORD="penpot_pre"
|
||||
# export PENPOT_FLAGS="enable-asserts enable-audit-log $PENPOT_FLAGS"
|
||||
|
||||
# Initialize MINIO config
|
||||
# mc alias set penpot-s3/ http://minio:9000 minioadmin minioadmin
|
||||
# mc admin user add penpot-s3 penpot-devenv penpot-devenv
|
||||
# mc admin policy set penpot-s3 readwrite user=penpot-devenv
|
||||
# mc mb penpot-s3/penpot -p
|
||||
# export AWS_ACCESS_KEY_ID=penpot-devenv
|
||||
# export AWS_SECRET_ACCESS_KEY=penpot-devenv
|
||||
# export PENPOT_ASSETS_STORAGE_BACKEND=assets-s3
|
||||
# export PENPOT_STORAGE_ASSETS_S3_ENDPOINT=http://minio:9000
|
||||
# export PENPOT_STORAGE_ASSETS_S3_REGION=eu-central-1
|
||||
# export PENPOT_STORAGE_ASSETS_S3_BUCKET=penpot
|
||||
|
||||
export OPTIONS="
|
||||
-A:dev \
|
||||
-J-Djava.util.logging.manager=org.apache.logging.log4j.jul.LogManager \
|
||||
-J-Dlog4j2.configurationFile=log4j2-devenv.xml \
|
||||
-J-XX:+UseZGC \
|
||||
-J-XX:-OmitStackTraceInFastThrow \
|
||||
-J-Xms50m -J-Xmx1024m \
|
||||
-J-Djdk.attach.allowAttachSelf \
|
||||
-J-XX:+UnlockDiagnosticVMOptions \
|
||||
-J-XX:+DebugNonSafepoints";
|
||||
|
||||
export OPTIONS_EVAL="nil"
|
||||
# export OPTIONS_EVAL="(set! *warn-on-reflection* true)"
|
||||
|
||||
set -ex
|
||||
exec clojure $OPTIONS -M -e "$OPTIONS_EVAL" -m rebel-readline.main
|
||||
|
||||
@@ -1,4 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -xe
|
||||
clojure -Adev -m app.tests.main;
|
||||
20
backend/scripts/run.template.sh
Normal file
@@ -0,0 +1,20 @@
|
||||
#!/usr/bin/env bash
|
||||
set +e
|
||||
JAVA_CMD=$(type -p java)
|
||||
|
||||
set -e
|
||||
if [[ ! -n "$JAVA_CMD" ]]; then
|
||||
if [[ -n "$JAVA_HOME" ]] && [[ -x "$JAVA_HOME/bin/java" ]]; then
|
||||
JAVA_CMD="$JAVA_HOME/bin/java"
|
||||
else
|
||||
>&2 echo "Couldn't find 'java'. Please set JAVA_HOME."
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ -f ./environ ]; then
|
||||
source ./environ
|
||||
fi
|
||||
|
||||
set -x
|
||||
exec $JAVA_CMD $JVM_OPTS "$@" -jar penpot.jar -m app.main
|
||||
@@ -1,2 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
python -m smtpd -n -c DebuggingServer localhost:25
|
||||
@@ -1,13 +1,23 @@
|
||||
#!/bin/sh
|
||||
#!/usr/bin/env bash
|
||||
|
||||
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 -M -m app.main
|
||||
|
||||
|
||||
|
||||
@@ -1,238 +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/.
|
||||
;;
|
||||
;; This Source Code Form is "Incompatible With Secondary Licenses", as
|
||||
;; defined by the Mozilla Public License, v. 2.0.
|
||||
;;
|
||||
;; Copyright (c) 2020 UXBOX Labs SL
|
||||
|
||||
(ns app.cli.fixtures
|
||||
"A initial fixtures."
|
||||
(:require
|
||||
[app.common.pages :as cp]
|
||||
[app.common.uuid :as uuid]
|
||||
[app.config :as cfg]
|
||||
[app.db :as db]
|
||||
[app.migrations]
|
||||
[app.services.mutations.profile :as profile]
|
||||
[app.util.blob :as blob]
|
||||
[buddy.hashers :as hashers]
|
||||
[clojure.tools.logging :as log]
|
||||
[mount.core :as mount]))
|
||||
|
||||
(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
|
||||
[opts]
|
||||
(let [rng (java.util.Random. 1)]
|
||||
(letfn [(create-profile [conn index]
|
||||
(let [id (mk-uuid "profile" index)
|
||||
_ (log/info "create profile" id)
|
||||
|
||||
prof (register-profile conn
|
||||
{:id id
|
||||
:fullname (str "Profile " index)
|
||||
:password "123123"
|
||||
:demo? true
|
||||
:email (str "profile" index ".test@penpot.app")})
|
||||
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]
|
||||
(log/info "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)]
|
||||
(log/info "create team" id)
|
||||
(db/insert! conn :team {:id id
|
||||
:name name
|
||||
:photo ""})
|
||||
id))
|
||||
|
||||
(create-teams [conn]
|
||||
(log/info "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)]
|
||||
(log/info "create file" 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]
|
||||
(log/info "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 (mk-uuid "project" team-id index)
|
||||
name (str "project " index)]
|
||||
(log/info "create project" id)
|
||||
(db/insert! conn :project
|
||||
{:id id
|
||||
:team-id team-id
|
||||
: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]
|
||||
(log/info "create projects")
|
||||
(let [owner-id (rng-nth rng profile-ids)
|
||||
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)))
|
||||
|
||||
(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]
|
||||
(log/info "setup team" team-id 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]
|
||||
(log/info "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)]
|
||||
|
||||
(log/info "create draft file" 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 db/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*
|
||||
[preset]
|
||||
(let [preset (if (map? preset)
|
||||
preset
|
||||
(case preset
|
||||
(nil "small" :small) preset-small
|
||||
;; "medium" preset-medium
|
||||
;; "big" preset-big
|
||||
preset-small))]
|
||||
(impl-run preset)))
|
||||
|
||||
(defn run
|
||||
[{:keys [preset]
|
||||
:or {preset :small}}]
|
||||
(try
|
||||
(-> (mount/only #{#'app.config/config
|
||||
#'app.db/pool
|
||||
#'app.migrations/migrations})
|
||||
(mount/start))
|
||||
(run* preset)
|
||||
(catch Exception e
|
||||
(log/errorf e "Unhandled exception."))
|
||||
(finally
|
||||
(mount/stop))))
|
||||
168
backend/src/app/cli/manage.clj
Normal file
@@ -0,0 +1,168 @@
|
||||
;; 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.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]]
|
||||
[clojure.string :as str]
|
||||
[clojure.tools.cli :refer [parse-opts]]
|
||||
[integrant.core :as ig])
|
||||
(:import
|
||||
java.io.Console))
|
||||
|
||||
;; --- IMPL
|
||||
|
||||
(defn init-system
|
||||
[]
|
||||
(let [data (-> main/system-config
|
||||
(select-keys [:app.db/pool :app.metrics/metrics])
|
||||
(assoc :app.migrations/all {}))]
|
||||
(-> data ig/prep ig/init)))
|
||||
|
||||
(defn- read-from-console
|
||||
[{:keys [label type] :or {type :text}}]
|
||||
(let [^Console console (System/console)]
|
||||
(when-not console
|
||||
(l/error :hint "no console found, can proceed")
|
||||
(System/exit 1))
|
||||
|
||||
(binding [*out* (.writer console)]
|
||||
(print label " ")
|
||||
(.flush *out*))
|
||||
|
||||
(case type
|
||||
:text (.readLine console)
|
||||
:password (String. (.readPassword console)))))
|
||||
|
||||
(defn create-profile
|
||||
[options]
|
||||
(let [system (init-system)
|
||||
email (or (:email options)
|
||||
(read-from-console {:label "Email:"}))
|
||||
fullname (or (:fullname options)
|
||||
(read-from-console {:label "Full Name:"}))
|
||||
password (or (:password options)
|
||||
(read-from-console {:label "Password:"
|
||||
:type :password}))]
|
||||
(try
|
||||
(db/with-atomic [conn (:app.db/pool system)]
|
||||
(->> (profile/create-profile conn
|
||||
{:fullname fullname
|
||||
:email email
|
||||
:password password
|
||||
:is-active true
|
||||
:is-demo false})
|
||||
(profile/create-profile-relations conn)))
|
||||
|
||||
(when (pos? (:verbosity options))
|
||||
(println "User created successfully."))
|
||||
(System/exit 0)
|
||||
|
||||
(catch Exception _e
|
||||
(when (pos? (:verbosity options))
|
||||
(println "Unable to create user, already exists."))
|
||||
(System/exit 1)))))
|
||||
|
||||
(defn reset-password
|
||||
[options]
|
||||
(let [system (init-system)]
|
||||
(try
|
||||
(db/with-atomic [conn (:app.db/pool system)]
|
||||
(let [email (or (:email options)
|
||||
(read-from-console {:label "Email:"}))
|
||||
profile (retrieve-profile-data-by-email conn email)]
|
||||
(when-not profile
|
||||
(when (pos? (:verbosity options))
|
||||
(println "Profile does not exists."))
|
||||
(System/exit 1))
|
||||
|
||||
(let [password (or (:password options)
|
||||
(read-from-console {:label "Password:"
|
||||
:type :password}))]
|
||||
(profile/update-profile-password! conn (assoc profile :password password))
|
||||
(when (pos? (:verbosity options))
|
||||
(println "Password changed successfully.")))))
|
||||
(System/exit 0)
|
||||
(catch Exception e
|
||||
(when (pos? (:verbosity options))
|
||||
(println "Unable to change password."))
|
||||
(when (= 2 (:verbosity options))
|
||||
(.printStackTrace e))
|
||||
(System/exit 1)))))
|
||||
|
||||
;; --- CLI PARSE
|
||||
|
||||
(def cli-options
|
||||
;; An option with a required argument
|
||||
[["-u" "--email EMAIL" "Email Address"]
|
||||
["-p" "--password PASSWORD" "Password"]
|
||||
["-n" "--name FULLNAME" "Full Name"
|
||||
:id :fullname]
|
||||
["-v" nil "Verbosity level"
|
||||
:id :verbosity
|
||||
:default 1
|
||||
:update-fn inc]
|
||||
["-q" nil "Dont' print to console"
|
||||
:id :verbosity
|
||||
:update-fn (constantly 0)]
|
||||
["-h" "--help"]])
|
||||
|
||||
(defn usage
|
||||
[options-summary]
|
||||
(->> ["Penpot CLI management."
|
||||
""
|
||||
"Usage: manage [options] action"
|
||||
""
|
||||
"Options:"
|
||||
options-summary
|
||||
""
|
||||
"Actions:"
|
||||
" create-profile Create new profile."
|
||||
" reset-password Reset profile password."
|
||||
""]
|
||||
(str/join \newline)))
|
||||
|
||||
(defn error-msg [errors]
|
||||
(str "The following errors occurred while parsing your command:\n\n"
|
||||
(str/join \newline errors)))
|
||||
|
||||
(defn validate-args
|
||||
"Validate command line arguments. Either return a map indicating the program
|
||||
should exit (with a error message, and optional ok status), or a map
|
||||
indicating the action the program should take and the options provided."
|
||||
[args]
|
||||
(let [{:keys [options arguments errors summary] :as opts} (parse-opts args cli-options)]
|
||||
(cond
|
||||
(:help options) ; help => exit OK with usage summary
|
||||
{:exit-message (usage summary) :ok? true}
|
||||
|
||||
errors ; errors => exit with description of errors
|
||||
{:exit-message (error-msg errors)}
|
||||
|
||||
;; custom validation on arguments
|
||||
:else
|
||||
(let [action (first arguments)]
|
||||
(if (#{"create-profile" "reset-password"} action)
|
||||
{:action (first arguments) :options options}
|
||||
{:exit-message (usage summary)})))))
|
||||
|
||||
(defn exit [status msg]
|
||||
(println msg)
|
||||
(System/exit status))
|
||||
|
||||
(defn -main
|
||||
[& args]
|
||||
(let [{:keys [action options exit-message ok?]} (validate-args args)]
|
||||
(if exit-message
|
||||
(exit (if ok? 0 1) exit-message)
|
||||
(case action
|
||||
"create-profile" (create-profile options)
|
||||
"reset-password" (reset-password options)))))
|
||||
@@ -1,232 +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/.
|
||||
;;
|
||||
;; This Source Code Form is "Incompatible With Secondary Licenses", as
|
||||
;; defined by the Mozilla Public License, v. 2.0.
|
||||
;;
|
||||
;; Copyright (c) 2020 UXBOX Labs SL
|
||||
|
||||
(ns app.cli.media-loader
|
||||
"Media libraries importer (command line helper)."
|
||||
#_(:require
|
||||
[app.common.spec :as us]
|
||||
[app.common.uuid :as uuid]
|
||||
[app.config]
|
||||
[app.db :as db]
|
||||
[app.media-storage]
|
||||
[app.media]
|
||||
[app.migrations]
|
||||
[app.services.mutations.files :as files]
|
||||
[app.services.mutations.media :as media]
|
||||
[app.services.mutations.projects :as projects]
|
||||
[clojure.java.io :as io]
|
||||
[clojure.spec.alpha :as s]
|
||||
[clojure.tools.logging :as log]
|
||||
[datoteka.core :as fs]
|
||||
[mount.core :as mount])
|
||||
#_(:import
|
||||
java.io.PushbackReader))
|
||||
|
||||
;; --- Constants & Helpers
|
||||
|
||||
;; (def ^:const +graphics-uuid-ns+ #uuid "3642a582-565f-4070-beba-af797ab27a6a")
|
||||
;; (def ^:const +colors-uuid-ns+ #uuid "3642a582-565f-4070-beba-af797ab27a6c")
|
||||
|
||||
;; (s/def ::id ::us/uuid)
|
||||
;; (s/def ::name ::us/string)
|
||||
;; (s/def ::path ::us/string)
|
||||
;; (s/def ::regex #(instance? java.util.regex.Pattern %))
|
||||
|
||||
;; (s/def ::import-graphics
|
||||
;; (s/keys :req-un [::path ::regex]))
|
||||
|
||||
;; (s/def ::import-color
|
||||
;; (s/* (s/cat :name ::us/string :color ::us/color)))
|
||||
|
||||
;; (s/def ::import-colors (s/coll-of ::import-color))
|
||||
|
||||
;; (s/def ::import-library
|
||||
;; (s/keys :req-un [::name]
|
||||
;; :opt-un [::import-graphics ::import-colors]))
|
||||
|
||||
;; (defn exit!
|
||||
;; ([] (exit! 0))
|
||||
;; ([code]
|
||||
;; (System/exit code)))
|
||||
|
||||
;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;; ;; Graphics Importer
|
||||
;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
|
||||
;; (defn- create-media-object
|
||||
;; [conn file-id media-object-id localpath]
|
||||
;; (s/assert fs/path? localpath)
|
||||
;; (s/assert ::us/uuid file-id)
|
||||
;; (s/assert ::us/uuid media-object-id)
|
||||
;; (let [filename (fs/name localpath)
|
||||
;; extension (second (fs/split-ext filename))
|
||||
;; file (io/as-file localpath)
|
||||
;; mtype (case extension
|
||||
;; ".jpg" "image/jpeg"
|
||||
;; ".png" "image/png"
|
||||
;; ".webp" "image/webp"
|
||||
;; ".svg" "image/svg+xml")]
|
||||
;; (log/info "Creating image" filename media-object-id)
|
||||
;; (media/create-media-object conn {:content {:tempfile localpath
|
||||
;; :filename filename
|
||||
;; :content-type mtype
|
||||
;; :size (.length file)}
|
||||
;; :id media-object-id
|
||||
;; :file-id file-id
|
||||
;; :name filename
|
||||
;; :is-local false})))
|
||||
|
||||
;; (defn- media-object-exists?
|
||||
;; [conn id]
|
||||
;; (s/assert ::us/uuid id)
|
||||
;; (let [row (db/get-by-id conn :media-object id)]
|
||||
;; (if row true false)))
|
||||
|
||||
;; (defn- import-media-object-if-not-exists
|
||||
;; [conn file-id fpath]
|
||||
;; (s/assert ::us/uuid file-id)
|
||||
;; (s/assert fs/path? fpath)
|
||||
;; (let [media-object-id (uuid/namespaced +graphics-uuid-ns+ (str file-id (fs/name fpath)))]
|
||||
;; (when-not (media-object-exists? conn media-object-id)
|
||||
;; (create-media-object conn file-id media-object-id fpath))
|
||||
;; media-object-id))
|
||||
|
||||
;; (defn- import-graphics
|
||||
;; [conn file-id {:keys [path regex]}]
|
||||
;; (run! (fn [fpath]
|
||||
;; (when (re-matches regex (str fpath))
|
||||
;; (import-media-object-if-not-exists conn file-id fpath)))
|
||||
;; (->> (fs/list-dir path)
|
||||
;; (filter fs/regular-file?))))
|
||||
|
||||
;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;; ;; Colors Importer
|
||||
;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
|
||||
;; (defn- create-color
|
||||
;; [conn file-id name content]
|
||||
;; (s/assert ::us/uuid file-id)
|
||||
;; (s/assert ::us/color content)
|
||||
;; (let [color-id (uuid/namespaced +colors-uuid-ns+ (str file-id content))]
|
||||
;; (log/info "Creating color" color-id "-" name content)
|
||||
;; (colors/create-color conn {:id color-id
|
||||
;; :file-id file-id
|
||||
;; :name name
|
||||
;; :content content})
|
||||
;; color-id))
|
||||
|
||||
;; (defn- import-colors
|
||||
;; [conn file-id colors]
|
||||
;; (db/delete! conn :color {:file-id file-id})
|
||||
;; (run! (fn [[name content]]
|
||||
;; (create-color conn file-id name content))
|
||||
;; (partition-all 2 colors)))
|
||||
|
||||
|
||||
;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;; ;; Library files Importer
|
||||
;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
|
||||
;; (defn- library-file-exists?
|
||||
;; [conn id]
|
||||
;; (s/assert ::us/uuid id)
|
||||
;; (let [row (db/get-by-id conn :file id)]
|
||||
;; (if row true false)))
|
||||
|
||||
;; (defn- create-library-file-if-not-exists
|
||||
;; [conn project-id {:keys [name]}]
|
||||
;; (let [id (uuid/namespaced +colors-uuid-ns+ name)]
|
||||
;; (when-not (library-file-exists? conn id)
|
||||
;; (log/info "Creating library-file:" name)
|
||||
;; (files/create-file conn {:id id
|
||||
;; :profile-id uuid/zero
|
||||
;; :project-id project-id
|
||||
;; :name name
|
||||
;; :is-shared true})
|
||||
;; (files/create-page conn {:file-id id}))
|
||||
;; id))
|
||||
|
||||
;; (defn- process-library
|
||||
;; [conn basedir project-id {:keys [graphics colors] :as library}]
|
||||
;; (us/verify ::import-library library)
|
||||
;; (let [library-file-id (create-library-file-if-not-exists conn project-id library)]
|
||||
;; (when graphics
|
||||
;; (->> (assoc graphics :path (fs/join basedir (:path graphics)))
|
||||
;; (import-graphics conn library-file-id)))
|
||||
;; (when colors
|
||||
;; (import-colors conn library-file-id colors))))
|
||||
|
||||
|
||||
;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;; ;; Entry Point
|
||||
;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
|
||||
;; (defn- project-exists?
|
||||
;; [conn id]
|
||||
;; (s/assert ::us/uuid id)
|
||||
;; (let [row (db/get-by-id conn :project id)]
|
||||
;; (if row true false)))
|
||||
|
||||
;; (defn- create-project-if-not-exists
|
||||
;; [conn {:keys [name] :as project}]
|
||||
;; (let [id (uuid/namespaced +colors-uuid-ns+ name)]
|
||||
;; (when-not (project-exists? conn id)
|
||||
;; (log/info "Creating project" name)
|
||||
;; (projects/create-project conn {:id id
|
||||
;; :team-id uuid/zero
|
||||
;; :name name
|
||||
;; :default? false}))
|
||||
;; id))
|
||||
|
||||
;; (defn- validate-path
|
||||
;; [path]
|
||||
;; (let [path (if (symbol? path) (str path) path)]
|
||||
;; (log/infof "Trying to load config from '%s'." path)
|
||||
;; (when-not path
|
||||
;; (log/error "No path is provided")
|
||||
;; (exit! -1))
|
||||
;; (when-not (fs/exists? path)
|
||||
;; (log/error "Path does not exists.")
|
||||
;; (exit! -1))
|
||||
;; (when (fs/directory? path)
|
||||
;; (log/error "The provided path is a directory.")
|
||||
;; (exit! -1))
|
||||
;; (fs/path path)))
|
||||
|
||||
;; (defn- read-file
|
||||
;; [path]
|
||||
;; (let [reader (PushbackReader. (io/reader path))]
|
||||
;; [(fs/parent path)
|
||||
;; (read reader)]))
|
||||
|
||||
;; (defn run*
|
||||
;; [path]
|
||||
;; (let [[basedir libraries] (read-file path)]
|
||||
;; (db/with-atomic [conn db/pool]
|
||||
;; (let [project-id (create-project-if-not-exists conn {:name "System libraries"})]
|
||||
;; (run! #(process-library conn basedir project-id %) libraries)))))
|
||||
|
||||
;; (defn run
|
||||
;; [{:keys [path] :as params}]
|
||||
;; (log/infof "Starting media loader.")
|
||||
;; (let [path (validate-path path)]
|
||||
|
||||
;; (try
|
||||
;; (-> (mount/only #{#'app.config/config
|
||||
;; #'app.db/pool
|
||||
;; #'app.migrations/migrations
|
||||
;; #'app.media/semaphore
|
||||
;; #'app.media-storage/media-storage})
|
||||
;; (mount/start))
|
||||
;; (run* path)
|
||||
;; (catch Exception e
|
||||
;; (log/errorf e "Unhandled exception."))
|
||||
;; (finally
|
||||
;; (mount/stop)))))
|
||||
|
||||
129
backend/src/app/cli/migrate_media.clj
Normal file
@@ -0,0 +1,129 @@
|
||||
;; 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.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]
|
||||
[cuerdas.core :as str]
|
||||
[datoteka.core :as fs]
|
||||
[integrant.core :as ig]))
|
||||
|
||||
(declare migrate-profiles)
|
||||
(declare migrate-teams)
|
||||
(declare migrate-file-media)
|
||||
|
||||
(defn run-in-system
|
||||
[system]
|
||||
(db/with-atomic [conn (:app.db/pool system)]
|
||||
(let [system (assoc system ::conn conn)]
|
||||
(migrate-profiles system)
|
||||
(migrate-teams system)
|
||||
(migrate-file-media system))
|
||||
system))
|
||||
|
||||
(defn run
|
||||
[]
|
||||
(let [config (select-keys main/system-config
|
||||
[:app.db/pool
|
||||
:app.migrations/migrations
|
||||
:app.metrics/metrics
|
||||
:app.storage.s3/backend
|
||||
:app.storage.db/backend
|
||||
:app.storage.fs/backend
|
||||
:app.storage/storage])]
|
||||
(ig/load-namespaces config)
|
||||
(try
|
||||
(-> (ig/prep config)
|
||||
(ig/init)
|
||||
(run-in-system)
|
||||
(ig/halt!))
|
||||
(catch Exception e
|
||||
(l/error :hint "unhandled exception" :cause e)))))
|
||||
|
||||
|
||||
;; --- IMPL
|
||||
|
||||
(defn migrate-profiles
|
||||
[{:keys [::conn] :as system}]
|
||||
(letfn [(retrieve-profiles [conn]
|
||||
(->> (db/exec! conn ["select * from profile"])
|
||||
(filter #(not (str/empty? (:photo %))))
|
||||
(seq)))]
|
||||
(let [base (fs/path (cf/get :storage-fs-old-directory))
|
||||
storage (-> (:app.storage/storage system)
|
||||
(assoc :conn conn))]
|
||||
(doseq [profile (retrieve-profiles conn)]
|
||||
(let [path (fs/path (:photo profile))
|
||||
full (-> (fs/join base path)
|
||||
(fs/normalize))
|
||||
ext (fs/ext path)
|
||||
mtype (cm/format->mtype (keyword ext))
|
||||
obj (sto/put-object storage {:content (sto/content full)
|
||||
:content-type mtype})]
|
||||
(db/update! conn :profile
|
||||
{:photo-id (:id obj)}
|
||||
{:id (:id profile)}))))))
|
||||
|
||||
(defn migrate-teams
|
||||
[{:keys [::conn] :as system}]
|
||||
(letfn [(retrieve-teams [conn]
|
||||
(->> (db/exec! conn ["select * from team"])
|
||||
(filter #(not (str/empty? (:photo %))))
|
||||
(seq)))]
|
||||
(let [base (fs/path (cf/get :storage-fs-old-directory))
|
||||
storage (-> (:app.storage/storage system)
|
||||
(assoc :conn conn))]
|
||||
(doseq [team (retrieve-teams conn)]
|
||||
(let [path (fs/path (:photo team))
|
||||
full (-> (fs/join base path)
|
||||
(fs/normalize))
|
||||
ext (fs/ext path)
|
||||
mtype (cm/format->mtype (keyword ext))
|
||||
obj (sto/put-object storage {:content (sto/content full)
|
||||
:content-type mtype})]
|
||||
(db/update! conn :team
|
||||
{:photo-id (:id obj)}
|
||||
{:id (:id team)}))))))
|
||||
|
||||
|
||||
|
||||
(defn migrate-file-media
|
||||
[{:keys [::conn] :as system}]
|
||||
(letfn [(retrieve-media-objects [conn]
|
||||
(->> (db/exec! conn ["select fmo.id, fmo.path, fth.path as thumbnail_path
|
||||
from file_media_object as fmo
|
||||
join file_media_thumbnail as fth on (fth.media_object_id = fmo.id)"])
|
||||
(seq)))]
|
||||
(let [base (fs/path (cf/get :storage-fs-old-directory))
|
||||
storage (-> (:app.storage/storage system)
|
||||
(assoc :conn conn))]
|
||||
(doseq [mobj (retrieve-media-objects conn)]
|
||||
(let [img-path (fs/path (:path mobj))
|
||||
thm-path (fs/path (:thumbnail-path mobj))
|
||||
img-path (-> (fs/join base img-path)
|
||||
(fs/normalize))
|
||||
thm-path (-> (fs/join base thm-path)
|
||||
(fs/normalize))
|
||||
img-ext (fs/ext img-path)
|
||||
thm-ext (fs/ext thm-path)
|
||||
|
||||
img-mtype (cm/format->mtype (keyword img-ext))
|
||||
thm-mtype (cm/format->mtype (keyword thm-ext))
|
||||
|
||||
img-obj (sto/put-object storage {:content (sto/content img-path)
|
||||
:content-type img-mtype})
|
||||
thm-obj (sto/put-object storage {:content (sto/content thm-path)
|
||||
:content-type thm-mtype})]
|
||||
|
||||
(db/update! conn :file-media-object
|
||||
{:media-id (:id img-obj)
|
||||
:thumbnail-id (:id thm-obj)}
|
||||
{:id (:id mobj)}))))))
|
||||
@@ -2,220 +2,365 @@
|
||||
;; 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/.
|
||||
;;
|
||||
;; This Source Code Form is "Incompatible With Secondary Licenses", as
|
||||
;; defined by the Mozilla Public License, v. 2.0.
|
||||
;;
|
||||
;; Copyright (c) 2020 UXBOX Labs SL
|
||||
;; Copyright (c) UXBOX Labs SL
|
||||
|
||||
(ns app.config
|
||||
"A configuration management."
|
||||
(:refer-clojure :exclude [get])
|
||||
(: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]
|
||||
[clojure.core :as c]
|
||||
[clojure.java.io :as io]
|
||||
[clojure.pprint :as pprint]
|
||||
[clojure.spec.alpha :as s]
|
||||
[cuerdas.core :as str]
|
||||
[environ.core :refer [env]]
|
||||
[mount.core :refer [defstate]]))
|
||||
[integrant.core :as ig]))
|
||||
|
||||
(prefer-method print-method
|
||||
clojure.lang.IRecord
|
||||
clojure.lang.IDeref)
|
||||
|
||||
(prefer-method pprint/simple-dispatch
|
||||
clojure.lang.IPersistentMap
|
||||
clojure.lang.IDeref)
|
||||
|
||||
(defmethod ig/init-key :default
|
||||
[_ data]
|
||||
(d/without-nils data))
|
||||
|
||||
(defmethod ig/prep-key :default
|
||||
[_ data]
|
||||
(if (map? data)
|
||||
(d/without-nils data)
|
||||
data))
|
||||
|
||||
(def defaults
|
||||
{:http-server-port 6060
|
||||
:http-server-cors "http://localhost:3449"
|
||||
:database-uri "postgresql://127.0.0.1/penpot"
|
||||
{:host "devenv"
|
||||
:tenant "dev"
|
||||
:database-uri "postgresql://postgres/penpot"
|
||||
:database-username "penpot"
|
||||
:database-password "penpot"
|
||||
:secret-key "default"
|
||||
|
||||
:media-directory "resources/public/media"
|
||||
:assets-directory "resources/public/static"
|
||||
:default-blob-version 3
|
||||
:loggers-zmq-uri "tcp://localhost:45556"
|
||||
|
||||
:public-uri "http://localhost:3449/"
|
||||
:redis-uri "redis://localhost/0"
|
||||
:media-uri "http://localhost:3449/media/"
|
||||
:assets-uri "http://localhost:3449/static/"
|
||||
:file-change-snapshot-every 5
|
||||
:file-change-snapshot-timeout "3h"
|
||||
|
||||
:image-process-max-threads 2
|
||||
:public-uri "http://localhost:3449"
|
||||
:redis-uri "redis://redis/0"
|
||||
|
||||
:smtp-enabled false
|
||||
:smtp-default-reply-to "no-reply@example.com"
|
||||
:smtp-default-from "no-reply@example.com"
|
||||
:srepl-host "127.0.0.1"
|
||||
:srepl-port 6062
|
||||
|
||||
:host "devenv"
|
||||
:assets-storage-backend :assets-fs
|
||||
:storage-assets-fs-directory "assets"
|
||||
|
||||
:allow-demo-users true
|
||||
:registration-enabled true
|
||||
:registration-domain-whitelist ""
|
||||
:debug-humanize-transit true
|
||||
:assets-path "/internal/assets/"
|
||||
|
||||
;; This is the time should transcurr after the last page
|
||||
;; modification in order to make the file ellegible for
|
||||
;; trimming. The value only supports s(econds) m(inutes) and
|
||||
;; h(ours) as time unit.
|
||||
:file-trimming-threshold "72h"
|
||||
:rlimit-password 10
|
||||
:rlimit-image 2
|
||||
:rlimit-font 5
|
||||
|
||||
;; LDAP auth disabled by default. Set ldap-auth-host to enable
|
||||
;:ldap-auth-host "ldap.mysupercompany.com"
|
||||
;:ldap-auth-port 389
|
||||
;:ldap-bind-dn "cn=admin,dc=ldap,dc=mysupercompany,dc=com"
|
||||
;:ldap-bind-password "verysecure"
|
||||
;:ldap-auth-ssl false
|
||||
;:ldap-auth-starttls false
|
||||
;:ldap-auth-base-dn "ou=People,dc=ldap,dc=mysupercompany,dc=com"
|
||||
:smtp-default-reply-to "Penpot <no-reply@example.com>"
|
||||
:smtp-default-from "Penpot <no-reply@example.com>"
|
||||
|
||||
:ldap-auth-user-query "(|(uid=$username)(mail=$username))"
|
||||
:ldap-auth-username-attribute "uid"
|
||||
:ldap-auth-email-attribute "mail"
|
||||
:ldap-auth-fullname-attribute "displayName"
|
||||
:ldap-auth-avatar-attribute "jpegPhoto"})
|
||||
:profile-complaint-max-age (dt/duration {:days 7})
|
||||
:profile-complaint-threshold 2
|
||||
|
||||
(s/def ::http-server-port ::us/integer)
|
||||
(s/def ::http-server-debug ::us/boolean)
|
||||
(s/def ::http-server-cors ::us/string)
|
||||
(s/def ::database-username (s/nilable ::us/string))
|
||||
:profile-bounce-max-age (dt/duration {:days 7})
|
||||
:profile-bounce-threshold 10
|
||||
|
||||
:telemetry-uri "https://telemetry.penpot.app/"
|
||||
|
||||
:ldap-user-query "(|(uid=:username)(mail=:username))"
|
||||
:ldap-attrs-username "uid"
|
||||
:ldap-attrs-email "mail"
|
||||
:ldap-attrs-fullname "cn"
|
||||
:ldap-attrs-photo "jpegPhoto"
|
||||
|
||||
;; a server prop key where initial project is stored.
|
||||
:initial-project-skey "initial-project"})
|
||||
|
||||
(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 ::admins ::us/set-of-str)
|
||||
(s/def ::file-change-snapshot-every ::us/integer)
|
||||
(s/def ::file-change-snapshot-timeout ::dt/duration)
|
||||
|
||||
(s/def ::default-executor-parallelism ::us/integer)
|
||||
(s/def ::blocking-executor-parallelism ::us/integer)
|
||||
(s/def ::worker-executor-parallelism ::us/integer)
|
||||
|
||||
(s/def ::secret-key ::us/string)
|
||||
(s/def ::allow-demo-users ::us/boolean)
|
||||
(s/def ::assets-path ::us/string)
|
||||
(s/def ::authenticated-cookie-domain ::us/string)
|
||||
(s/def ::database-password (s/nilable ::us/string))
|
||||
(s/def ::database-uri ::us/string)
|
||||
(s/def ::redis-uri ::us/string)
|
||||
(s/def ::assets-uri ::us/string)
|
||||
(s/def ::assets-directory ::us/string)
|
||||
(s/def ::media-uri ::us/string)
|
||||
(s/def ::media-directory ::us/string)
|
||||
(s/def ::secret-key ::us/string)
|
||||
(s/def ::database-username (s/nilable ::us/string))
|
||||
(s/def ::database-readonly ::us/boolean)
|
||||
(s/def ::database-min-pool-size ::us/integer)
|
||||
(s/def ::database-max-pool-size ::us/integer)
|
||||
|
||||
(s/def ::host ::us/string)
|
||||
(s/def ::default-blob-version ::us/integer)
|
||||
(s/def ::error-report-webhook ::us/string)
|
||||
(s/def ::smtp-enabled ::us/boolean)
|
||||
(s/def ::smtp-default-reply-to ::us/email)
|
||||
(s/def ::smtp-default-from ::us/email)
|
||||
(s/def ::smtp-host ::us/string)
|
||||
(s/def ::smtp-port ::us/integer)
|
||||
(s/def ::smtp-username (s/nilable ::us/string))
|
||||
(s/def ::smtp-password (s/nilable ::us/string))
|
||||
(s/def ::smtp-tls ::us/boolean)
|
||||
(s/def ::smtp-ssl ::us/boolean)
|
||||
(s/def ::allow-demo-users ::us/boolean)
|
||||
(s/def ::registration-enabled ::us/boolean)
|
||||
(s/def ::registration-domain-whitelist ::us/string)
|
||||
(s/def ::debug-humanize-transit ::us/boolean)
|
||||
(s/def ::public-uri ::us/string)
|
||||
(s/def ::backend-uri ::us/string)
|
||||
|
||||
(s/def ::image-process-max-threads ::us/integer)
|
||||
(s/def ::file-trimming-threshold ::dt/duration)
|
||||
|
||||
(s/def ::google-client-id ::us/string)
|
||||
(s/def ::google-client-secret ::us/string)
|
||||
|
||||
(s/def ::user-feedback-destination ::us/string)
|
||||
(s/def ::github-client-id ::us/string)
|
||||
(s/def ::github-client-secret ::us/string)
|
||||
(s/def ::gitlab-base-uri ::us/string)
|
||||
(s/def ::gitlab-client-id ::us/string)
|
||||
(s/def ::gitlab-client-secret ::us/string)
|
||||
(s/def ::gitlab-base-uri ::us/string)
|
||||
|
||||
(s/def ::ldap-auth-host ::us/string)
|
||||
(s/def ::ldap-auth-port ::us/integer)
|
||||
(s/def ::google-client-id ::us/string)
|
||||
(s/def ::google-client-secret ::us/string)
|
||||
(s/def ::oidc-client-id ::us/string)
|
||||
(s/def ::oidc-client-secret ::us/string)
|
||||
(s/def ::oidc-base-uri ::us/string)
|
||||
(s/def ::oidc-token-uri ::us/string)
|
||||
(s/def ::oidc-auth-uri ::us/string)
|
||||
(s/def ::oidc-user-uri ::us/string)
|
||||
(s/def ::oidc-scopes ::us/set-of-str)
|
||||
(s/def ::oidc-roles ::us/set-of-str)
|
||||
(s/def ::oidc-roles-attr ::us/keyword)
|
||||
(s/def ::host ::us/string)
|
||||
(s/def ::http-server-port ::us/integer)
|
||||
(s/def ::http-server-host ::us/string)
|
||||
(s/def ::http-server-min-threads ::us/integer)
|
||||
(s/def ::http-server-max-threads ::us/integer)
|
||||
(s/def ::http-session-idle-max-age ::dt/duration)
|
||||
(s/def ::http-session-updater-batch-max-age ::dt/duration)
|
||||
(s/def ::http-session-updater-batch-max-size ::us/integer)
|
||||
(s/def ::initial-project-skey ::us/string)
|
||||
(s/def ::ldap-attrs-email ::us/string)
|
||||
(s/def ::ldap-attrs-fullname ::us/string)
|
||||
(s/def ::ldap-attrs-photo ::us/string)
|
||||
(s/def ::ldap-attrs-username ::us/string)
|
||||
(s/def ::ldap-base-dn ::us/string)
|
||||
(s/def ::ldap-bind-dn ::us/string)
|
||||
(s/def ::ldap-bind-password ::us/string)
|
||||
(s/def ::ldap-auth-ssl ::us/boolean)
|
||||
(s/def ::ldap-auth-starttls ::us/boolean)
|
||||
(s/def ::ldap-auth-base-dn ::us/string)
|
||||
(s/def ::ldap-auth-user-query ::us/string)
|
||||
(s/def ::ldap-auth-username-attribute ::us/string)
|
||||
(s/def ::ldap-auth-email-attribute ::us/string)
|
||||
(s/def ::ldap-auth-fullname-attribute ::us/string)
|
||||
(s/def ::ldap-auth-avatar-attribute ::us/string)
|
||||
(s/def ::ldap-host ::us/string)
|
||||
(s/def ::ldap-port ::us/integer)
|
||||
(s/def ::ldap-ssl ::us/boolean)
|
||||
(s/def ::ldap-starttls ::us/boolean)
|
||||
(s/def ::ldap-user-query ::us/string)
|
||||
(s/def ::loggers-loki-uri ::us/string)
|
||||
(s/def ::loggers-zmq-uri ::us/string)
|
||||
(s/def ::media-directory ::us/string)
|
||||
(s/def ::media-uri ::us/string)
|
||||
(s/def ::profile-bounce-max-age ::dt/duration)
|
||||
(s/def ::profile-bounce-threshold ::us/integer)
|
||||
(s/def ::profile-complaint-max-age ::dt/duration)
|
||||
(s/def ::profile-complaint-threshold ::us/integer)
|
||||
(s/def ::public-uri ::us/string)
|
||||
(s/def ::redis-uri ::us/string)
|
||||
(s/def ::registration-domain-whitelist ::us/set-of-str)
|
||||
(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-host ::us/string)
|
||||
(s/def ::smtp-password (s/nilable ::us/string))
|
||||
(s/def ::smtp-port ::us/integer)
|
||||
(s/def ::smtp-ssl ::us/boolean)
|
||||
(s/def ::smtp-tls ::us/boolean)
|
||||
(s/def ::smtp-username (s/nilable ::us/string))
|
||||
(s/def ::srepl-host ::us/string)
|
||||
(s/def ::srepl-port ::us/integer)
|
||||
(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-assets-s3-endpoint ::us/string)
|
||||
(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 ::storage-fdata-s3-endpoint ::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 [::http-server-cors
|
||||
::http-server-debug
|
||||
::http-server-port
|
||||
::google-client-id
|
||||
::google-client-secret
|
||||
::gitlab-client-id
|
||||
::gitlab-client-secret
|
||||
::gitlab-base-uri
|
||||
::redis-uri
|
||||
::public-uri
|
||||
::database-username
|
||||
(s/keys :opt-un [::secret-key
|
||||
::flags
|
||||
::admins
|
||||
::allow-demo-users
|
||||
::audit-log-archive-uri
|
||||
::audit-log-gc-max-age
|
||||
::authenticated-cookie-domain
|
||||
::database-password
|
||||
::database-uri
|
||||
::assets-directory
|
||||
::assets-uri
|
||||
::media-directory
|
||||
::media-uri
|
||||
::database-username
|
||||
::database-readonly
|
||||
::database-min-pool-size
|
||||
::database-max-pool-size
|
||||
::default-blob-version
|
||||
::error-report-webhook
|
||||
::secret-key
|
||||
::default-executor-parallelism
|
||||
::blocking-executor-parallelism
|
||||
::worker-executor-parallelism
|
||||
::file-change-snapshot-every
|
||||
::file-change-snapshot-timeout
|
||||
::user-feedback-destination
|
||||
::github-client-id
|
||||
::github-client-secret
|
||||
::gitlab-base-uri
|
||||
::gitlab-client-id
|
||||
::gitlab-client-secret
|
||||
::google-client-id
|
||||
::google-client-secret
|
||||
::oidc-client-id
|
||||
::oidc-client-secret
|
||||
::oidc-base-uri
|
||||
::oidc-token-uri
|
||||
::oidc-auth-uri
|
||||
::oidc-user-uri
|
||||
::oidc-scopes
|
||||
::oidc-roles-attr
|
||||
::oidc-roles
|
||||
::host
|
||||
::http-server-host
|
||||
::http-server-port
|
||||
::http-server-max-threads
|
||||
::http-server-min-threads
|
||||
::http-session-idle-max-age
|
||||
::http-session-updater-batch-max-age
|
||||
::http-session-updater-batch-max-size
|
||||
::initial-project-skey
|
||||
::ldap-attrs-email
|
||||
::ldap-attrs-fullname
|
||||
::ldap-attrs-photo
|
||||
::ldap-attrs-username
|
||||
::ldap-base-dn
|
||||
::ldap-bind-dn
|
||||
::ldap-bind-password
|
||||
::ldap-host
|
||||
::ldap-port
|
||||
::ldap-ssl
|
||||
::ldap-starttls
|
||||
::ldap-user-query
|
||||
::local-assets-uri
|
||||
::loggers-loki-uri
|
||||
::loggers-zmq-uri
|
||||
::profile-bounce-max-age
|
||||
::profile-bounce-threshold
|
||||
::profile-complaint-max-age
|
||||
::profile-complaint-threshold
|
||||
::public-uri
|
||||
::redis-uri
|
||||
::registration-domain-whitelist
|
||||
::registration-enabled
|
||||
::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
|
||||
::smtp-host
|
||||
::smtp-port
|
||||
::smtp-username
|
||||
::smtp-password
|
||||
::smtp-tls
|
||||
::smtp-port
|
||||
::smtp-ssl
|
||||
::host
|
||||
::file-trimming-threshold
|
||||
::debug-humanize-transit
|
||||
::allow-demo-users
|
||||
::registration-enabled
|
||||
::registration-domain-whitelist
|
||||
::image-process-max-threads
|
||||
::ldap-auth-host
|
||||
::ldap-auth-port
|
||||
::ldap-bind-dn
|
||||
::ldap-bind-password
|
||||
::ldap-auth-ssl
|
||||
::ldap-auth-starttls
|
||||
::ldap-auth-base-dn
|
||||
::ldap-auth-user-query
|
||||
::ldap-auth-username-attribute
|
||||
::ldap-auth-email-attribute
|
||||
::ldap-auth-fullname-attribute
|
||||
::ldap-auth-avatar-attribute]))
|
||||
::smtp-tls
|
||||
::smtp-username
|
||||
::srepl-host
|
||||
::srepl-port
|
||||
::assets-storage-backend
|
||||
::storage-assets-fs-directory
|
||||
::storage-assets-s3-bucket
|
||||
::storage-assets-s3-region
|
||||
::storage-assets-s3-endpoint
|
||||
::fdata-storage-backend
|
||||
::storage-fdata-s3-bucket
|
||||
::storage-fdata-s3-region
|
||||
::storage-fdata-s3-prefix
|
||||
::storage-fdata-s3-endpoint
|
||||
::telemetry-enabled
|
||||
::telemetry-uri
|
||||
::telemetry-referer
|
||||
::telemetry-with-taiga
|
||||
::tenant]))
|
||||
|
||||
(defn env->config
|
||||
[env]
|
||||
(reduce-kv
|
||||
(fn [acc k v]
|
||||
(cond-> acc
|
||||
(str/starts-with? (name k) "penpot-")
|
||||
(assoc (keyword (subs (name k) 7)) v)
|
||||
(def default-flags
|
||||
[:enable-backend-asserts
|
||||
:enable-backend-api-doc
|
||||
:enable-secure-session-cookies])
|
||||
|
||||
(str/starts-with? (name k) "app-")
|
||||
(assoc (keyword (subs (name k) 4)) v)))
|
||||
{}
|
||||
env))
|
||||
(defn- parse-flags
|
||||
[config]
|
||||
(flags/parse flags/default
|
||||
default-flags
|
||||
(:flags config)))
|
||||
|
||||
(defn read-config
|
||||
[env]
|
||||
(->> (env->config env)
|
||||
(merge defaults)
|
||||
(us/conform ::config)))
|
||||
(defn read-env
|
||||
[prefix]
|
||||
(let [prefix (str prefix "-")
|
||||
len (count prefix)]
|
||||
(reduce-kv
|
||||
(fn [acc k v]
|
||||
(cond-> acc
|
||||
(str/starts-with? (name k) prefix)
|
||||
(assoc (keyword (subs (name k) len)) v)))
|
||||
{}
|
||||
env)))
|
||||
|
||||
(defn read-test-config
|
||||
[env]
|
||||
(assoc (read-config env)
|
||||
:redis-uri "redis://redis/1"
|
||||
:database-uri "postgresql://postgres/penpot_test"
|
||||
:media-directory "/tmp/app/media"
|
||||
:assets-directory "/tmp/app/static"
|
||||
:migrations-verbose false))
|
||||
|
||||
(defstate config
|
||||
:start (read-config env))
|
||||
|
||||
(def default-deletion-delay
|
||||
(dt/duration {:hours 48}))
|
||||
(defn- read-config
|
||||
[]
|
||||
(try
|
||||
(->> (read-env "penpot")
|
||||
(merge defaults)
|
||||
(us/conform ::config))
|
||||
(catch Throwable e
|
||||
(when (ex/ex-info? e)
|
||||
(println ";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;")
|
||||
(println "Error on validating configuration:")
|
||||
(println (:explain (ex-data e))
|
||||
(println ";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;")))
|
||||
(throw e))))
|
||||
|
||||
(def version
|
||||
(delay (v/parse "%version%")))
|
||||
(v/parse (or (some-> (io/resource "version.txt")
|
||||
(slurp)
|
||||
(str/trim))
|
||||
"%version%")))
|
||||
|
||||
(defn smtp
|
||||
[cfg]
|
||||
{:host (:smtp-host cfg "localhost")
|
||||
:port (:smtp-port cfg 25)
|
||||
:default-reply-to (:smtp-default-reply-to cfg)
|
||||
:default-from (:smtp-default-from cfg)
|
||||
:tls (:smtp-tls cfg)
|
||||
:enabled (:smtp-enabled cfg)
|
||||
:username (:smtp-username cfg)
|
||||
:password (:smtp-password cfg)})
|
||||
(def ^:dynamic config (read-config))
|
||||
(def ^:dynamic flags (parse-flags config))
|
||||
|
||||
(def deletion-delay
|
||||
(dt/duration {:days 7}))
|
||||
|
||||
(defn get
|
||||
"A configuration getter. Helps code be more testable."
|
||||
([key]
|
||||
(c/get config key))
|
||||
([key default]
|
||||
(c/get config key default)))
|
||||
|
||||
;; Set value for all new threads bindings.
|
||||
(alter-var-root #'*assert* (constantly (contains? flags :backend-asserts)))
|
||||
|
||||
@@ -2,65 +2,149 @@
|
||||
;; 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/.
|
||||
;;
|
||||
;; This Source Code Form is "Incompatible With Secondary Licenses", as
|
||||
;; defined by the Mozilla Public License, v. 2.0.
|
||||
;;
|
||||
;; Copyright (c) 2020 UXBOX Labs SL
|
||||
;; Copyright (c) UXBOX Labs SL
|
||||
|
||||
(ns app.db
|
||||
(:require
|
||||
[app.common.data :as d]
|
||||
[app.common.exceptions :as ex]
|
||||
[app.common.geom.point :as gpt]
|
||||
[app.config :as cfg]
|
||||
[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.migrations :as mg]
|
||||
[app.util.time :as dt]
|
||||
[app.util.transit :as t]
|
||||
[clojure.data.json :as json]
|
||||
[clojure.java.io :as io]
|
||||
[clojure.spec.alpha :as s]
|
||||
[clojure.string :as str]
|
||||
[mount.core :as mount :refer [defstate]]
|
||||
[integrant.core :as ig]
|
||||
[next.jdbc :as jdbc]
|
||||
[next.jdbc.date-time :as jdbc-dt]
|
||||
[next.jdbc.optional :as jdbc-opt]
|
||||
[next.jdbc.sql :as jdbc-sql]
|
||||
[next.jdbc.sql.builder :as jdbc-bld])
|
||||
[next.jdbc.date-time :as jdbc-dt])
|
||||
(:import
|
||||
com.zaxxer.hikari.HikariConfig
|
||||
com.zaxxer.hikari.HikariDataSource
|
||||
com.zaxxer.hikari.metrics.prometheus.PrometheusMetricsTrackerFactory
|
||||
java.io.InputStream
|
||||
java.io.OutputStream
|
||||
java.lang.AutoCloseable
|
||||
java.sql.Connection
|
||||
java.sql.Savepoint
|
||||
org.postgresql.PGConnection
|
||||
org.postgresql.geometric.PGpoint
|
||||
org.postgresql.jdbc.PgArray
|
||||
org.postgresql.largeobject.LargeObject
|
||||
org.postgresql.largeobject.LargeObjectManager
|
||||
org.postgresql.util.PGInterval
|
||||
org.postgresql.util.PGobject))
|
||||
|
||||
(declare open)
|
||||
(declare create-pool)
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;; Initialization
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
|
||||
(declare apply-migrations!)
|
||||
|
||||
(s/def ::connection-timeout ::us/integer)
|
||||
(s/def ::max-size ::us/integer)
|
||||
(s/def ::min-size ::us/integer)
|
||||
(s/def ::migrations map?)
|
||||
(s/def ::name keyword?)
|
||||
(s/def ::password ::us/string)
|
||||
(s/def ::read-only ::us/boolean)
|
||||
(s/def ::uri ::us/not-empty-string)
|
||||
(s/def ::username ::us/string)
|
||||
(s/def ::validation-timeout ::us/integer)
|
||||
|
||||
(defmethod ig/pre-init-spec ::pool [_]
|
||||
(s/keys :req-un [::uri ::name
|
||||
::min-size
|
||||
::max-size
|
||||
::connection-timeout
|
||||
::validation-timeout]
|
||||
:opt-un [::migrations
|
||||
::username
|
||||
::password
|
||||
::mtx/metrics
|
||||
::read-only]))
|
||||
|
||||
(defmethod ig/prep-key ::pool
|
||||
[_ cfg]
|
||||
(merge {:name :main
|
||||
:min-size 0
|
||||
:max-size 30
|
||||
:connection-timeout 10000
|
||||
:validation-timeout 10000
|
||||
:idle-timeout 120000 ; 2min
|
||||
:max-lifetime 1800000 ; 30m
|
||||
:read-only false}
|
||||
(d/without-nils cfg)))
|
||||
|
||||
(defmethod ig/init-key ::pool
|
||||
[_ {:keys [migrations name read-only] :as cfg}]
|
||||
(l/info :hint "initialize connection pool"
|
||||
:name (d/name name)
|
||||
:uri (:uri cfg)
|
||||
:read-only read-only
|
||||
:with-credentials (and (contains? cfg :username)
|
||||
(contains? cfg :password))
|
||||
:min-size (:min-size cfg)
|
||||
:max-size (:max-size cfg))
|
||||
|
||||
(let [pool (create-pool cfg)]
|
||||
(when-not read-only
|
||||
(some->> (seq migrations) (apply-migrations! pool)))
|
||||
pool))
|
||||
|
||||
(defmethod ig/halt-key! ::pool
|
||||
[_ pool]
|
||||
(.close ^HikariDataSource pool))
|
||||
|
||||
(defn- apply-migrations!
|
||||
[pool migrations]
|
||||
(with-open [conn ^AutoCloseable (open pool)]
|
||||
(mg/setup! conn)
|
||||
(doseq [[name steps] migrations]
|
||||
(mg/migrate! conn {:name (d/name name) :steps steps}))))
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;; API & Impl
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
|
||||
(def initsql
|
||||
(str "SET statement_timeout = 10000;\n"
|
||||
"SET idle_in_transaction_session_timeout = 30000;"))
|
||||
(str "SET statement_timeout = 300000;\n"
|
||||
"SET idle_in_transaction_session_timeout = 300000;"))
|
||||
|
||||
(defn- create-datasource-config
|
||||
[cfg]
|
||||
(let [dburi (:database-uri cfg)
|
||||
username (:database-username cfg)
|
||||
password (:database-password cfg)
|
||||
config (HikariConfig.)
|
||||
mfactory (PrometheusMetricsTrackerFactory. mtx/registry)]
|
||||
[{:keys [metrics uri] :as cfg}]
|
||||
(let [config (HikariConfig.)]
|
||||
(doto config
|
||||
(.setJdbcUrl (str "jdbc:" dburi))
|
||||
(.setPoolName "main")
|
||||
(.setJdbcUrl (str "jdbc:" uri))
|
||||
(.setPoolName (d/name (:name cfg)))
|
||||
(.setAutoCommit true)
|
||||
(.setReadOnly false)
|
||||
(.setConnectionTimeout 8000) ;; 8seg
|
||||
(.setValidationTimeout 4000) ;; 4seg
|
||||
(.setIdleTimeout 300000) ;; 5min
|
||||
(.setMaxLifetime 900000) ;; 15min
|
||||
(.setMinimumIdle 0)
|
||||
(.setMaximumPoolSize 15)
|
||||
(.setReadOnly (:read-only cfg))
|
||||
(.setConnectionTimeout (:connection-timeout cfg))
|
||||
(.setValidationTimeout (:validation-timeout cfg))
|
||||
(.setIdleTimeout (:idle-timeout cfg))
|
||||
(.setMaxLifetime (:max-lifetime cfg))
|
||||
(.setMinimumIdle (:min-size cfg))
|
||||
(.setMaximumPoolSize (:max-size cfg))
|
||||
(.setConnectionInitSql initsql)
|
||||
(.setMetricsTrackerFactory mfactory))
|
||||
(when username (.setUsername config username))
|
||||
(when password (.setPassword config password))
|
||||
(.setInitializationFailTimeout -1))
|
||||
|
||||
;; When metrics namespace is provided
|
||||
(when metrics
|
||||
(->> (:registry metrics)
|
||||
(PrometheusMetricsTrackerFactory.)
|
||||
(.setMetricsTrackerFactory config)))
|
||||
|
||||
(some->> ^String (:username cfg) (.setUsername config))
|
||||
(some->> ^String (:password cfg) (.setPassword config))
|
||||
|
||||
config))
|
||||
|
||||
(defn pool?
|
||||
@@ -69,76 +153,124 @@
|
||||
|
||||
(s/def ::pool pool?)
|
||||
|
||||
(defn pool-closed?
|
||||
(defn closed?
|
||||
[pool]
|
||||
(.isClosed ^com.zaxxer.hikari.HikariDataSource pool))
|
||||
(.isClosed ^HikariDataSource pool))
|
||||
|
||||
(defn- create-pool
|
||||
(defn read-only?
|
||||
[pool]
|
||||
(.isReadOnly ^HikariDataSource pool))
|
||||
|
||||
(defn create-pool
|
||||
[cfg]
|
||||
(let [dsc (create-datasource-config cfg)]
|
||||
(jdbc-dt/read-as-instant)
|
||||
(HikariDataSource. dsc)))
|
||||
|
||||
(declare pool)
|
||||
(defn unwrap
|
||||
[conn klass]
|
||||
(.unwrap ^Connection conn klass))
|
||||
|
||||
(defstate pool
|
||||
:start (create-pool cfg/config)
|
||||
:stop (.close pool))
|
||||
(defn lobj-manager
|
||||
[conn]
|
||||
(let [conn (unwrap conn org.postgresql.PGConnection)]
|
||||
(.getLargeObjectAPI ^PGConnection conn)))
|
||||
|
||||
(defn lobj-create
|
||||
[manager]
|
||||
(.createLO ^LargeObjectManager manager LargeObjectManager/READWRITE))
|
||||
|
||||
(defn lobj-open
|
||||
([manager oid]
|
||||
(lobj-open manager oid {}))
|
||||
([manager oid {:keys [mode] :or {mode :rw}}]
|
||||
(let [mode (case mode
|
||||
(:r :read) LargeObjectManager/READ
|
||||
(:w :write) LargeObjectManager/WRITE
|
||||
(:rw :read+write) LargeObjectManager/READWRITE)]
|
||||
(.open ^LargeObjectManager manager (long oid) mode))))
|
||||
|
||||
(defn lobj-unlink
|
||||
[manager oid]
|
||||
(.unlink ^LargeObjectManager manager (long oid)))
|
||||
|
||||
(extend-type LargeObject
|
||||
io/IOFactory
|
||||
(make-reader [lobj opts]
|
||||
(let [^InputStream is (.getInputStream ^LargeObject lobj)]
|
||||
(io/make-reader is opts)))
|
||||
(make-writer [lobj opts]
|
||||
(let [^OutputStream os (.getOutputStream ^LargeObject lobj)]
|
||||
(io/make-writer os opts)))
|
||||
(make-input-stream [lobj opts]
|
||||
(let [^InputStream is (.getInputStream ^LargeObject lobj)]
|
||||
(io/make-input-stream is opts)))
|
||||
(make-output-stream [lobj opts]
|
||||
(let [^OutputStream os (.getOutputStream ^LargeObject lobj)]
|
||||
(io/make-output-stream os opts))))
|
||||
|
||||
(defmacro with-atomic
|
||||
[& args]
|
||||
`(jdbc/with-transaction ~@args))
|
||||
|
||||
(defn- kebab-case [s] (str/replace s #"_" "-"))
|
||||
(defn- snake-case [s] (str/replace s #"-" "_"))
|
||||
(defn- as-kebab-maps
|
||||
[rs opts]
|
||||
(jdbc-opt/as-unqualified-modified-maps rs (assoc opts :label-fn kebab-case)))
|
||||
|
||||
(defn open
|
||||
[]
|
||||
(defn ^Connection open
|
||||
[pool]
|
||||
(jdbc/get-connection pool))
|
||||
|
||||
(defn exec!
|
||||
([ds sv]
|
||||
(exec! ds sv {}))
|
||||
([ds sv opts]
|
||||
(jdbc/execute! ds sv (assoc opts :builder-fn as-kebab-maps))))
|
||||
(jdbc/execute! ds sv (assoc opts :builder-fn sql/as-kebab-maps))))
|
||||
|
||||
(defn exec-one!
|
||||
([ds sv] (exec-one! ds sv {}))
|
||||
([ds sv opts]
|
||||
(jdbc/execute-one! ds sv (assoc opts :builder-fn as-kebab-maps))))
|
||||
|
||||
(def ^:private default-options
|
||||
{:table-fn snake-case
|
||||
:column-fn snake-case
|
||||
:builder-fn as-kebab-maps})
|
||||
(jdbc/execute-one! ds sv (assoc opts :builder-fn sql/as-kebab-maps))))
|
||||
|
||||
(defn insert!
|
||||
[ds table params]
|
||||
(jdbc-sql/insert! ds table params default-options))
|
||||
([ds table params] (insert! ds table params nil))
|
||||
([ds table params opts]
|
||||
(exec-one! ds
|
||||
(sql/insert table params opts)
|
||||
(assoc opts :return-keys true))))
|
||||
|
||||
(defn insert-multi!
|
||||
([ds table cols rows] (insert-multi! ds table cols rows nil))
|
||||
([ds table cols rows opts]
|
||||
(exec! ds
|
||||
(sql/insert-multi table cols rows opts)
|
||||
(assoc opts :return-keys true))))
|
||||
|
||||
(defn update!
|
||||
[ds table params where]
|
||||
(let [opts (assoc default-options :return-keys true)]
|
||||
(jdbc-sql/update! ds table params where opts)))
|
||||
([ds table params where] (update! ds table params where nil))
|
||||
([ds table params where opts]
|
||||
(exec-one! ds
|
||||
(sql/update table params where opts)
|
||||
(assoc opts :return-keys true))))
|
||||
|
||||
(defn delete!
|
||||
[ds table params]
|
||||
(let [opts (assoc default-options :return-keys true)]
|
||||
(jdbc-sql/delete! ds table params opts)))
|
||||
([ds table params] (delete! ds table params nil))
|
||||
([ds table params opts]
|
||||
(exec-one! ds
|
||||
(sql/delete table params opts)
|
||||
(assoc opts :return-keys true))))
|
||||
|
||||
(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 opts]
|
||||
(let [opts (cond-> (merge default-options opts)
|
||||
(:for-update opts)
|
||||
(assoc :suffix "for update"))
|
||||
res (exec-one! ds (jdbc-bld/for-query table params opts) opts)]
|
||||
(when (or (:deleted-at res) (not res))
|
||||
(ex/raise :type :not-found))
|
||||
([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 check-not-found (or (not res) (is-deleted? res)))
|
||||
(ex/raise :type :not-found
|
||||
:table table
|
||||
:hint "database object not found"))
|
||||
res)))
|
||||
|
||||
(defn get-by-id
|
||||
@@ -151,14 +283,14 @@
|
||||
([ds table params]
|
||||
(query ds table params nil))
|
||||
([ds table params opts]
|
||||
(let [opts (cond-> (merge default-options opts)
|
||||
(:for-update opts)
|
||||
(assoc :suffix "for update"))]
|
||||
(exec! ds (jdbc-bld/for-query table params opts) opts))))
|
||||
(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]
|
||||
@@ -169,17 +301,39 @@
|
||||
(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)))
|
||||
|
||||
(defn create-array
|
||||
[conn type objects]
|
||||
(let [^PGConnection conn (unwrap conn org.postgresql.PGConnection)]
|
||||
(if (coll? objects)
|
||||
(.createArrayOf conn ^String type (into-array Object objects))
|
||||
(.createArrayOf conn ^String type objects))))
|
||||
|
||||
(defn decode-pgpoint
|
||||
[^PGpoint v]
|
||||
(gpt/point (.-x v) (.-y v)))
|
||||
@@ -212,7 +366,7 @@
|
||||
(pginterval data)
|
||||
|
||||
(dt/duration? data)
|
||||
(->> (/ (.toMillis data) 1000.0)
|
||||
(->> (/ (.toMillis ^java.time.Duration data) 1000.0)
|
||||
(format "%s seconds")
|
||||
(pginterval))
|
||||
|
||||
@@ -225,7 +379,7 @@
|
||||
val (.getValue o)]
|
||||
(if (or (= typ "json")
|
||||
(= typ "jsonb"))
|
||||
(json/read-str val :key-fn keyword)
|
||||
(json/read val)
|
||||
val)))
|
||||
|
||||
(defn decode-transit-pgobject
|
||||
@@ -237,12 +391,24 @@
|
||||
(t/decode-str val)
|
||||
val)))
|
||||
|
||||
(defn inet
|
||||
[ip-addr]
|
||||
(doto (org.postgresql.util.PGobject.)
|
||||
(.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."
|
||||
@@ -251,18 +417,23 @@
|
||||
(.setType "jsonb")
|
||||
(.setValue (json/write-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"))))
|
||||
|
||||
;; Instrumentation
|
||||
(defn xact-lock!
|
||||
[conn n]
|
||||
(let [n (xact-check-param n)]
|
||||
(exec-one! conn ["select pg_advisory_xact_lock(?::bigint) as lock" n])
|
||||
true))
|
||||
|
||||
(mtx/instrument-with-counter!
|
||||
{:var [#'jdbc/execute-one!
|
||||
#'jdbc/execute!]
|
||||
:id "database__query_counter"
|
||||
:help "An absolute counter of database queries."})
|
||||
(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)))
|
||||
|
||||
62
backend/src/app/db/sql.clj
Normal file
@@ -0,0 +1,62 @@
|
||||
;; 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.db.sql
|
||||
(:refer-clojure :exclude [update])
|
||||
(:require
|
||||
[clojure.string :as str]
|
||||
[next.jdbc.optional :as jdbc-opt]
|
||||
[next.jdbc.sql.builder :as sql]))
|
||||
|
||||
(defn kebab-case [s] (str/replace s #"_" "-"))
|
||||
(defn snake-case [s] (str/replace s #"-" "_"))
|
||||
|
||||
(def default-opts
|
||||
{:table-fn snake-case
|
||||
:column-fn snake-case})
|
||||
|
||||
(defn as-kebab-maps
|
||||
[rs opts]
|
||||
(jdbc-opt/as-unqualified-modified-maps rs (assoc opts :label-fn kebab-case)))
|
||||
|
||||
(defn insert
|
||||
([table key-map]
|
||||
(insert table key-map nil))
|
||||
([table key-map opts]
|
||||
(let [opts (merge default-opts opts)
|
||||
opts (cond-> opts
|
||||
(:on-conflict-do-nothing opts)
|
||||
(assoc :suffix "ON CONFLICT DO NOTHING"))]
|
||||
(sql/for-insert table key-map opts))))
|
||||
|
||||
(defn insert-multi
|
||||
[table cols rows opts]
|
||||
(let [opts (merge default-opts opts)]
|
||||
(sql/for-insert-multi table cols rows opts)))
|
||||
|
||||
(defn select
|
||||
([table where-params]
|
||||
(select table where-params nil))
|
||||
([table where-params opts]
|
||||
(let [opts (merge default-opts opts)
|
||||
opts (cond-> opts
|
||||
(:for-update opts) (assoc :suffix "FOR UPDATE")
|
||||
(:for-key-share opts) (assoc :suffix "FOR KEY SHARE"))]
|
||||
(sql/for-query table where-params opts))))
|
||||
|
||||
(defn update
|
||||
([table key-map where-params]
|
||||
(update table key-map where-params nil))
|
||||
([table key-map where-params opts]
|
||||
(let [opts (merge default-opts opts)]
|
||||
(sql/for-update table key-map where-params opts))))
|
||||
|
||||
(defn delete
|
||||
([table where-params]
|
||||
(delete table where-params nil))
|
||||
([table where-params opts]
|
||||
(let [opts (merge default-opts opts)]
|
||||
(sql/for-delete table where-params opts))))
|
||||
@@ -2,28 +2,22 @@
|
||||
;; 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/.
|
||||
;;
|
||||
;; This Source Code Form is "Incompatible With Secondary Licenses", as
|
||||
;; defined by the Mozilla Public License, v. 2.0.
|
||||
;;
|
||||
;; Copyright (c) 2020 UXBOX Labs SL
|
||||
;; Copyright (c) UXBOX Labs SL
|
||||
|
||||
(ns app.emails
|
||||
"Main api for send emails."
|
||||
(:require
|
||||
[app.common.logging :as l]
|
||||
[app.common.spec :as us]
|
||||
[app.config :as cfg]
|
||||
[app.tasks :as tasks]
|
||||
[app.config :as cf]
|
||||
[app.db :as db]
|
||||
[app.db.sql :as sql]
|
||||
[app.util.emails :as emails]
|
||||
[clojure.spec.alpha :as s]))
|
||||
[app.worker :as wrk]
|
||||
[clojure.spec.alpha :as s]
|
||||
[integrant.core :as ig]))
|
||||
|
||||
;; --- Defaults
|
||||
|
||||
(defn default-context
|
||||
[]
|
||||
{:assets-uri (:assets-uri cfg/config)
|
||||
:public-uri (:public-uri cfg/config)})
|
||||
|
||||
;; --- Public API
|
||||
;; --- PUBLIC API
|
||||
|
||||
(defn render
|
||||
[email-factory context]
|
||||
@@ -31,17 +25,78 @@
|
||||
|
||||
(defn send!
|
||||
"Schedule the email for sending."
|
||||
[conn email-factory context]
|
||||
(us/verify fn? email-factory)
|
||||
(us/verify map? context)
|
||||
(let [email (email-factory context)]
|
||||
(tasks/submit! conn {:name "sendmail"
|
||||
:delay 0
|
||||
:max-retries 1
|
||||
:priority 200
|
||||
:props email})))
|
||||
[{:keys [::conn ::factory] :as context}]
|
||||
(us/verify fn? factory)
|
||||
(us/verify some? conn)
|
||||
(let [email (factory context)]
|
||||
(wrk/submit! (assoc email
|
||||
::wrk/task :sendmail
|
||||
::wrk/delay 0
|
||||
::wrk/max-retries 1
|
||||
::wrk/priority 200
|
||||
::wrk/conn conn))))
|
||||
|
||||
;; --- Emails
|
||||
|
||||
;; --- BOUNCE/COMPLAINS HANDLING
|
||||
|
||||
(def sql:profile-complaint-report
|
||||
"select (select count(*)
|
||||
from profile_complaint_report
|
||||
where type = 'complaint'
|
||||
and profile_id = ?
|
||||
and created_at > now() - ?::interval) as complaints,
|
||||
(select count(*)
|
||||
from profile_complaint_report
|
||||
where type = 'bounce'
|
||||
and profile_id = ?
|
||||
and created_at > now() - ?::interval) as bounces;")
|
||||
|
||||
(defn allow-send-emails?
|
||||
[conn profile]
|
||||
(when-not (:is-muted profile false)
|
||||
(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
|
||||
(:id profile)
|
||||
(db/interval complaint-max-age)
|
||||
(:id profile)
|
||||
(db/interval bounce-max-age)])]
|
||||
|
||||
(and (< (or complaints 0) complaint-threshold)
|
||||
(< (or bounces 0) bounce-threshold)))))
|
||||
|
||||
(defn has-complaint-reports?
|
||||
([conn email] (has-complaint-reports? conn email nil))
|
||||
([conn email {:keys [threshold] :or {threshold 1}}]
|
||||
(let [reports (db/exec! conn (sql/select :global-complaint-report
|
||||
{:email email :type "complaint"}
|
||||
{:limit 10}))]
|
||||
(>= (count reports) threshold))))
|
||||
|
||||
(defn has-bounce-reports?
|
||||
([conn email] (has-bounce-reports? conn email nil))
|
||||
([conn email {:keys [threshold] :or {threshold 1}}]
|
||||
(let [reports (db/exec! conn (sql/select :global-complaint-report
|
||||
{:email email :type "bounce"}
|
||||
{:limit 10}))]
|
||||
(>= (count reports) threshold))))
|
||||
|
||||
|
||||
;; --- EMAIL FACTORIES
|
||||
|
||||
(s/def ::subject ::us/string)
|
||||
(s/def ::content ::us/string)
|
||||
|
||||
(s/def ::feedback
|
||||
(s/keys :req-un [::subject ::content]))
|
||||
|
||||
(def feedback
|
||||
"A profile feedback email."
|
||||
(emails/template-factory ::feedback))
|
||||
|
||||
(s/def ::name ::us/string)
|
||||
(s/def ::register
|
||||
@@ -49,7 +104,7 @@
|
||||
|
||||
(def register
|
||||
"A new profile registration welcome email."
|
||||
(emails/template-factory ::register default-context))
|
||||
(emails/template-factory ::register))
|
||||
|
||||
(s/def ::token ::us/string)
|
||||
(s/def ::password-recovery
|
||||
@@ -57,7 +112,7 @@
|
||||
|
||||
(def password-recovery
|
||||
"A password recovery notification email."
|
||||
(emails/template-factory ::password-recovery default-context))
|
||||
(emails/template-factory ::password-recovery))
|
||||
|
||||
(s/def ::pending-email ::us/email)
|
||||
(s/def ::change-email
|
||||
@@ -65,17 +120,64 @@
|
||||
|
||||
(def change-email
|
||||
"Password change confirmation email"
|
||||
(emails/template-factory ::change-email default-context))
|
||||
(emails/template-factory ::change-email))
|
||||
|
||||
(s/def :internal.emails.invite-to-team/invited-by ::us/string)
|
||||
(s/def :internal.emails.invite-to-team/team ::us/string)
|
||||
(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."
|
||||
(emails/template-factory ::invite-to-team default-context))
|
||||
(emails/template-factory ::invite-to-team))
|
||||
|
||||
|
||||
;; --- SENDMAIL TASK
|
||||
|
||||
(declare send-console!)
|
||||
|
||||
(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 :opt-un [::username
|
||||
::password
|
||||
::tls
|
||||
::ssl
|
||||
::host
|
||||
::port
|
||||
::default-from
|
||||
::default-reply-to]))
|
||||
|
||||
(defmethod ig/init-key ::sendmail-handler
|
||||
[_ cfg]
|
||||
(fn [{:keys [props] :as task}]
|
||||
(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]
|
||||
(let [baos (java.io.ByteArrayOutputStream.)
|
||||
mesg (emails/smtp-message cfg email)]
|
||||
(.writeTo mesg baos)
|
||||
(let [out (with-out-str
|
||||
(println "email console dump:")
|
||||
(println "******** start email" (:id email) "**********")
|
||||
(println (.toString baos))
|
||||
(println "******** end email "(:id email) "**********"))]
|
||||
(l/info :email out))))
|
||||
|
||||
|
||||
@@ -1,83 +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/.
|
||||
;;
|
||||
;; This Source Code Form is "Incompatible With Secondary Licenses", as
|
||||
;; defined by the Mozilla Public License, v. 2.0.
|
||||
;;
|
||||
;; Copyright (c) 2020 Andrey Antukh <niwi@niwi.nz>
|
||||
|
||||
(ns app.error-reporter
|
||||
"A mattermost integration for error reporting."
|
||||
(:require
|
||||
[app.common.exceptions :as ex]
|
||||
[app.common.spec :as us]
|
||||
[app.config :as cfg]
|
||||
[app.db :as db]
|
||||
[app.tasks :as tasks]
|
||||
[app.util.async :as aa]
|
||||
[app.worker :as wrk]
|
||||
[app.util.http :as http]
|
||||
[clojure.core.async :as a]
|
||||
[clojure.data.json :as json]
|
||||
[clojure.spec.alpha :as s]
|
||||
[clojure.tools.logging :as log]
|
||||
[cuerdas.core :as str]
|
||||
[mount.core :as mount :refer [defstate]]
|
||||
[promesa.exec :as px]))
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;; Public API
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
|
||||
(defonce enqueue identity)
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;; Implementation
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
|
||||
(defn- send-to-mattermost!
|
||||
[log-event]
|
||||
(try
|
||||
(let [text (str/fmt "Unhandled exception: `host='%s'`, `version=%s`.\n@channel ⇊\n```%s\n```"
|
||||
(:host cfg/config)
|
||||
(:full @cfg/version)
|
||||
(str log-event))
|
||||
rsp (http/send! {:uri (:error-reporter-webhook cfg/config)
|
||||
:method :post
|
||||
:headers {"content-type" "application/json"}
|
||||
:body (json/write-str {:text text})})]
|
||||
(when (not= (:status rsp) 200)
|
||||
(log/warnf "Error reporting webhook replying with unexpected status: %s\n%s"
|
||||
(:status rsp)
|
||||
(pr-str rsp))))
|
||||
(catch Exception e
|
||||
(log/warnf e "Unexpected exception on error reporter."))))
|
||||
|
||||
(defn- send!
|
||||
[val]
|
||||
(aa/thread-call wrk/executor (partial send-to-mattermost! val)))
|
||||
|
||||
(defn- start
|
||||
[]
|
||||
(let [qch (a/chan (a/sliding-buffer 128))]
|
||||
(log/info "Starting error reporter loop.")
|
||||
|
||||
;; Only enable when a valid URL is provided.
|
||||
(when (:error-reporter-webhook cfg/config)
|
||||
(alter-var-root #'enqueue (constantly #(a/>!! qch %)))
|
||||
(a/go-loop []
|
||||
(let [val (a/<! qch)]
|
||||
(if (nil? val)
|
||||
(do
|
||||
(log/info "Closing error reporting loop.")
|
||||
(alter-var-root #'enqueue (constantly identity)))
|
||||
(do
|
||||
(a/<! (send! val))
|
||||
(recur))))))
|
||||
|
||||
qch))
|
||||
|
||||
(defstate reporter
|
||||
:start (start)
|
||||
:stop (a/close! reporter))
|
||||
@@ -2,79 +2,183 @@
|
||||
;; 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/.
|
||||
;;
|
||||
;; This Source Code Form is "Incompatible With Secondary Licenses", as
|
||||
;; defined by the Mozilla Public License, v. 2.0.
|
||||
;;
|
||||
;; Copyright (c) 2020 UXBOX Labs SL
|
||||
;; Copyright (c) UXBOX Labs SL
|
||||
|
||||
(ns app.http
|
||||
(:require
|
||||
[app.config :as cfg]
|
||||
[app.http.auth :as auth]
|
||||
[app.http.auth.gitlab :as gitlab]
|
||||
[app.http.auth.google :as google]
|
||||
[app.http.auth.ldap :as ldap]
|
||||
[app.common.data :as d]
|
||||
[app.common.exceptions :as ex]
|
||||
[app.common.logging :as l]
|
||||
[app.common.spec :as us]
|
||||
[app.config :as cf]
|
||||
[app.http.doc :as doc]
|
||||
[app.http.errors :as errors]
|
||||
[app.http.handlers :as handlers]
|
||||
[app.http.middleware :as middleware]
|
||||
[app.http.session :as session]
|
||||
[app.http.ws :as ws]
|
||||
[app.metrics :as mtx]
|
||||
[clojure.tools.logging :as log]
|
||||
[mount.core :as mount :refer [defstate]]
|
||||
[reitit.ring :as rring]
|
||||
[ring.adapter.jetty9 :as jetty]))
|
||||
[clojure.spec.alpha :as s]
|
||||
[integrant.core :as ig]
|
||||
[reitit.ring :as rr]
|
||||
[yetti.adapter :as yt])
|
||||
(:import
|
||||
org.eclipse.jetty.server.Server
|
||||
org.eclipse.jetty.server.handler.StatisticsHandler))
|
||||
|
||||
(defn- create-router
|
||||
[]
|
||||
(rring/router
|
||||
[["/metrics" {:get mtx/dump}]
|
||||
["/api" {:middleware [[middleware/format-response-body]
|
||||
[middleware/parse-request-body]
|
||||
(declare wrap-router)
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;; HTTP SERVER
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
|
||||
(s/def ::handler fn?)
|
||||
(s/def ::router some?)
|
||||
(s/def ::port ::us/integer)
|
||||
(s/def ::host ::us/string)
|
||||
(s/def ::name ::us/string)
|
||||
(s/def ::max-threads ::cf/http-server-max-threads)
|
||||
(s/def ::min-threads ::cf/http-server-min-threads)
|
||||
|
||||
(defmethod ig/prep-key ::server
|
||||
[_ cfg]
|
||||
(merge {:name "http"
|
||||
:min-threads 4
|
||||
:max-threads 60
|
||||
:port 6060
|
||||
:host "0.0.0.0"}
|
||||
(d/without-nils cfg)))
|
||||
|
||||
(defmethod ig/pre-init-spec ::server [_]
|
||||
(s/keys :req-un [::port ::host ::name ::min-threads ::max-threads]
|
||||
:opt-un [::mtx/metrics ::router ::handler]))
|
||||
|
||||
(defn- instrument-metrics
|
||||
[^Server server metrics]
|
||||
(let [stats (doto (StatisticsHandler.)
|
||||
(.setHandler (.getHandler server)))]
|
||||
(.setHandler server stats)
|
||||
(mtx/instrument-jetty! (:registry metrics) stats)
|
||||
server))
|
||||
|
||||
(defmethod ig/init-key ::server
|
||||
[_ {:keys [handler router port name metrics host] :as opts}]
|
||||
(l/info :hint "starting http server"
|
||||
:port port :host host :name name
|
||||
:min-threads (:min-threads opts)
|
||||
:max-threads (:max-threads opts))
|
||||
(let [options {:http/port port
|
||||
:http/host host
|
||||
:thread-pool/max-threads (:max-threads opts)
|
||||
:thread-pool/min-threads (:min-threads opts)
|
||||
:ring/async true}
|
||||
handler (cond
|
||||
(fn? handler) handler
|
||||
(some? router) (wrap-router router)
|
||||
:else (ex/raise :type :internal
|
||||
:code :invalid-argument
|
||||
:hint "Missing `handler` or `router` option."))
|
||||
server (-> (yt/server handler (d/without-nils options))
|
||||
(cond-> metrics (instrument-metrics metrics)))]
|
||||
(assoc opts :server (yt/start! server))))
|
||||
|
||||
(defmethod ig/halt-key! ::server
|
||||
[_ {:keys [server name port] :as opts}]
|
||||
(l/info :msg "stoping http server" :name name :port port)
|
||||
(yt/stop! server))
|
||||
|
||||
(defn- wrap-router
|
||||
[router]
|
||||
(let [default (rr/routes
|
||||
(rr/create-resource-handler {:path "/"})
|
||||
(rr/create-default-handler))
|
||||
options {:middleware [middleware/wrap-server-timing]
|
||||
:inject-match? false
|
||||
:inject-router? false}
|
||||
handler (rr/ring-handler router default options)]
|
||||
(fn [request respond _]
|
||||
(handler request respond (fn [cause]
|
||||
(l/error :hint "unexpected error processing request"
|
||||
::l/context (errors/get-error-context request cause)
|
||||
:query-string (:query-string request)
|
||||
:cause cause)
|
||||
(respond {:status 500 :body "internal server error"}))))))
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;; HTTP ROUTER
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
|
||||
(s/def ::rpc map?)
|
||||
(s/def ::session map?)
|
||||
(s/def ::oauth map?)
|
||||
(s/def ::storage map?)
|
||||
(s/def ::assets map?)
|
||||
(s/def ::feedback fn?)
|
||||
(s/def ::ws fn?)
|
||||
(s/def ::audit-http-handler fn?)
|
||||
(s/def ::debug map?)
|
||||
|
||||
(defmethod ig/pre-init-spec ::router [_]
|
||||
(s/keys :req-un [::rpc ::session ::mtx/metrics ::ws
|
||||
::oauth ::storage ::assets ::feedback
|
||||
::debug ::audit-http-handler]))
|
||||
|
||||
(defmethod ig/init-key ::router
|
||||
[_ {:keys [ws session rpc oauth metrics assets feedback debug] :as cfg}]
|
||||
(rr/router
|
||||
[["/metrics" {:get (:handler metrics)}]
|
||||
["/assets" {:middleware [[middleware/format-response-body]
|
||||
[middleware/errors errors/handle]
|
||||
[middleware/cookies]
|
||||
(:middleware session)]}
|
||||
["/by-id/:id" {:get (:objects-handler assets)}]
|
||||
["/by-file-media-id/:id" {:get (:file-objects-handler assets)}]
|
||||
["/by-file-media-id/:id/thumbnail" {:get (:file-thumbnails-handler assets)}]]
|
||||
|
||||
["/dbg" {:middleware [[middleware/multipart-params]
|
||||
[middleware/params]
|
||||
[middleware/keyword-params]
|
||||
[middleware/format-response-body]
|
||||
[middleware/errors errors/handle]
|
||||
[middleware/cookies]
|
||||
[(:middleware session)]]}
|
||||
["" {:get (:index debug)}]
|
||||
["/error-by-id/:id" {:get (:retrieve-error debug)}]
|
||||
["/error/:id" {:get (:retrieve-error debug)}]
|
||||
["/error" {:get (:retrieve-error-list debug)}]
|
||||
["/file/data" {:get (:retrieve-file-data debug)
|
||||
:post (:upload-file-data debug)}]
|
||||
["/file/changes" {:get (:retrieve-file-changes debug)}]]
|
||||
|
||||
["/webhooks"
|
||||
["/sns" {:post (:sns-webhook cfg)}]]
|
||||
|
||||
["/ws/notifications"
|
||||
{:middleware [[middleware/params]
|
||||
[middleware/keyword-params]
|
||||
[middleware/format-response-body]
|
||||
[middleware/errors errors/handle]
|
||||
[middleware/cookies]
|
||||
[(:middleware session)]]
|
||||
:get ws}]
|
||||
|
||||
["/api" {:middleware [[middleware/cors]
|
||||
[middleware/params]
|
||||
[middleware/multipart-params]
|
||||
[middleware/keyword-params]
|
||||
[middleware/format-response-body]
|
||||
[middleware/parse-request-body]
|
||||
[middleware/errors errors/handle]
|
||||
[middleware/cookies]]}
|
||||
|
||||
["/oauth"
|
||||
["/google" {:post google/auth}]
|
||||
["/google/callback" {:get google/callback}]
|
||||
["/gitlab" {:post gitlab/auth}]
|
||||
["/gitlab/callback" {:get gitlab/callback}]]
|
||||
["/health" {:get (:health-check debug)}]
|
||||
["/_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)}]
|
||||
|
||||
["/echo" {:get handlers/echo-handler
|
||||
:post handlers/echo-handler}]
|
||||
["/audit/events" {:middleware [(:middleware session)]
|
||||
:post (:audit-http-handler cfg)}]
|
||||
|
||||
["/login" {:handler auth/login-handler
|
||||
:method :post}]
|
||||
["/logout" {:handler auth/logout-handler
|
||||
:method :post}]
|
||||
["/login-ldap" {:handler ldap/auth
|
||||
:method :post}]
|
||||
|
||||
["/w" {:middleware [session/middleware]}
|
||||
["/query/:type" {:get handlers/query-handler}]
|
||||
["/mutation/:type" {:post handlers/mutation-handler}]]]]))
|
||||
|
||||
(defn start-server
|
||||
[]
|
||||
(let [wsockets {"/ws/notifications" ws/handler}
|
||||
options {:port (:http-server-port cfg/config)
|
||||
:h2c? true
|
||||
:join? false
|
||||
:allow-null-path-info true
|
||||
:websockets wsockets}
|
||||
handler (rring/ring-handler
|
||||
(create-router)
|
||||
(constantly {:status 404, :body ""})
|
||||
{:middleware [[middleware/development-resources]
|
||||
[middleware/development-cors]
|
||||
[middleware/metrics]]})]
|
||||
(log/infof "Http server listening on http://localhost:%s/"
|
||||
(:http-server-port cfg/config))
|
||||
(jetty/run-jetty handler options)))
|
||||
|
||||
(defstate server
|
||||
:start (start-server)
|
||||
:stop (.stop server))
|
||||
["/rpc" {:middleware [(:middleware session)]}
|
||||
["/query/:type" {:get (:query-handler rpc)
|
||||
:post (:query-handler rpc)}]
|
||||
["/mutation/:type" {:post (:mutation-handler rpc)}]]]]))
|
||||
|
||||
128
backend/src/app/http/assets.clj
Normal file
@@ -0,0 +1,128 @@
|
||||
;; 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.assets
|
||||
"Assets related handlers."
|
||||
(:require
|
||||
[app.common.exceptions :as ex]
|
||||
[app.common.spec :as us]
|
||||
[app.common.uri :as u]
|
||||
[app.db :as db]
|
||||
[app.metrics :as mtx]
|
||||
[app.storage :as sto]
|
||||
[app.util.async :as async]
|
||||
[app.util.time :as dt]
|
||||
[app.worker :as wrk]
|
||||
[clojure.spec.alpha :as s]
|
||||
[integrant.core :as ig]
|
||||
[promesa.core :as p]))
|
||||
|
||||
(def ^:private cache-max-age
|
||||
(dt/duration {:hours 24}))
|
||||
|
||||
(def ^:private signature-max-age
|
||||
(dt/duration {:hours 24 :minutes 15}))
|
||||
|
||||
(defn coerce-id
|
||||
[id]
|
||||
(let [res (us/uuid-conformer id)]
|
||||
(when-not (uuid? res)
|
||||
(ex/raise :type :not-found
|
||||
:hint "object not found"))
|
||||
res))
|
||||
|
||||
(defn- get-file-media-object
|
||||
[{:keys [pool] :as storage} id]
|
||||
(let [id (coerce-id id)
|
||||
mobj (db/exec-one! pool ["select * from file_media_object where id=?" id])]
|
||||
(when-not mobj
|
||||
(ex/raise :type :not-found
|
||||
:hint "object does not found"))
|
||||
mobj))
|
||||
|
||||
(defn- serve-object
|
||||
[{:keys [storage] :as cfg} obj]
|
||||
(let [mdata (meta obj)
|
||||
backend (sto/resolve-backend storage (:backend obj))]
|
||||
(case (:type backend)
|
||||
:db
|
||||
{:status 200
|
||||
:headers {"content-type" (:content-type mdata)
|
||||
"cache-control" (str "max-age=" (inst-ms cache-max-age))}
|
||||
:body (sto/get-object-bytes storage obj)}
|
||||
|
||||
:s3
|
||||
(let [{:keys [host port] :as url} (sto/get-object-url storage obj {:max-age signature-max-age})]
|
||||
{:status 307
|
||||
:headers {"location" (str url)
|
||||
"x-host" (cond-> host port (str ":" port))
|
||||
"cache-control" (str "max-age=" (inst-ms cache-max-age))}
|
||||
:body ""})
|
||||
|
||||
:fs
|
||||
(let [purl (u/uri (:assets-path cfg))
|
||||
purl (u/join purl (sto/object->relative-path obj))]
|
||||
{:status 204
|
||||
:headers {"x-accel-redirect" (:path purl)
|
||||
"content-type" (:content-type mdata)
|
||||
"cache-control" (str "max-age=" (inst-ms cache-max-age))}
|
||||
:body ""}))))
|
||||
|
||||
(defn- generic-handler
|
||||
[{:keys [storage executor] :as cfg} request kf]
|
||||
(async/with-dispatch executor
|
||||
(let [id (get-in request [:path-params :id])
|
||||
mobj (get-file-media-object storage id)
|
||||
obj (sto/get-object storage (kf mobj))]
|
||||
(if obj
|
||||
(serve-object cfg obj)
|
||||
{:status 404 :body ""}))))
|
||||
|
||||
(defn objects-handler
|
||||
[{:keys [storage executor] :as cfg} request respond raise]
|
||||
(-> (async/with-dispatch executor
|
||||
(let [id (get-in request [:path-params :id])
|
||||
id (coerce-id id)
|
||||
obj (sto/get-object storage id)]
|
||||
(if obj
|
||||
(serve-object cfg obj)
|
||||
{:status 404 :body ""})))
|
||||
(p/then respond)
|
||||
(p/catch raise)))
|
||||
|
||||
(defn file-objects-handler
|
||||
[cfg request respond raise]
|
||||
(-> (generic-handler cfg request :media-id)
|
||||
(p/then respond)
|
||||
(p/catch raise)))
|
||||
|
||||
(defn file-thumbnails-handler
|
||||
[cfg request respond raise]
|
||||
(-> (generic-handler cfg request #(or (:thumbnail-id %) (:media-id %)))
|
||||
(p/then respond)
|
||||
(p/catch raise)))
|
||||
|
||||
;; --- Initialization
|
||||
|
||||
(s/def ::storage some?)
|
||||
(s/def ::assets-path ::us/string)
|
||||
(s/def ::cache-max-age ::dt/duration)
|
||||
(s/def ::signature-max-age ::dt/duration)
|
||||
|
||||
(defmethod ig/pre-init-spec ::handlers [_]
|
||||
(s/keys :req-un [::storage
|
||||
::wrk/executor
|
||||
::mtx/metrics
|
||||
::assets-path
|
||||
::cache-max-age
|
||||
::signature-max-age]))
|
||||
|
||||
(defmethod ig/init-key ::handlers
|
||||
[_ cfg]
|
||||
{:objects-handler (partial objects-handler cfg)
|
||||
:file-objects-handler (partial file-objects-handler cfg)
|
||||
:file-thumbnails-handler (partial file-thumbnails-handler cfg)})
|
||||
|
||||
@@ -1,31 +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/.
|
||||
;;
|
||||
;; This Source Code Form is "Incompatible With Secondary Licenses", as
|
||||
;; defined by the Mozilla Public License, v. 2.0.
|
||||
;;
|
||||
;; Copyright (c) 2020 UXBOX Labs SL
|
||||
|
||||
(ns app.http.auth
|
||||
(:require
|
||||
[app.http.session :as session]
|
||||
[app.services.mutations :as sm]))
|
||||
|
||||
(defn login-handler
|
||||
[req]
|
||||
(let [data (:body-params req)
|
||||
uagent (get-in req [:headers "user-agent"])
|
||||
profile (sm/handle (assoc data ::sm/type :login))
|
||||
id (session/create (:id profile) uagent)]
|
||||
{:status 200
|
||||
:cookies (session/cookies id)
|
||||
:body profile}))
|
||||
|
||||
(defn logout-handler
|
||||
[req]
|
||||
(some-> (session/extract-auth-token req)
|
||||
(session/delete))
|
||||
{:status 200
|
||||
:cookies (session/cookies "" {:max-age -1})
|
||||
:body ""})
|
||||
@@ -1,147 +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/.
|
||||
;;
|
||||
;; This Source Code Form is "Incompatible With Secondary Licenses", as
|
||||
;; defined by the Mozilla Public License, v. 2.0.
|
||||
;;
|
||||
;; Copyright (c) 2020 UXBOX Labs SL
|
||||
|
||||
(ns app.http.auth.gitlab
|
||||
(:require
|
||||
[app.common.exceptions :as ex]
|
||||
[app.config :as cfg]
|
||||
[app.http.session :as session]
|
||||
[app.services.mutations :as sm]
|
||||
[app.services.tokens :as tokens]
|
||||
[app.util.http :as http]
|
||||
[app.util.time :as dt]
|
||||
[clojure.data.json :as json]
|
||||
[clojure.tools.logging :as log]
|
||||
[lambdaisland.uri :as uri]))
|
||||
|
||||
(def default-base-gitlab-uri "https://gitlab.com")
|
||||
|
||||
(def scope "read_user")
|
||||
|
||||
(defn- build-redirect-url
|
||||
[]
|
||||
(let [public (uri/uri (:public-uri cfg/config))]
|
||||
(str (assoc public :path "/api/oauth/gitlab/callback"))))
|
||||
|
||||
|
||||
(defn- build-oauth-uri
|
||||
[]
|
||||
(let [base-uri (uri/uri (:gitlab-base-uri cfg/config default-base-gitlab-uri))]
|
||||
(assoc base-uri :path "/oauth/authorize")))
|
||||
|
||||
|
||||
(defn- build-token-url
|
||||
[]
|
||||
(let [base-uri (uri/uri (:gitlab-base-uri cfg/config default-base-gitlab-uri))]
|
||||
(str (assoc base-uri :path "/oauth/token"))))
|
||||
|
||||
|
||||
(defn- build-user-info-url
|
||||
[]
|
||||
(let [base-uri (uri/uri (:gitlab-base-uri cfg/config default-base-gitlab-uri))]
|
||||
(str (assoc base-uri :path "/api/v4/user"))))
|
||||
|
||||
|
||||
(defn- get-access-token
|
||||
[code]
|
||||
(let [params {:client_id (:gitlab-client-id cfg/config)
|
||||
:client_secret (:gitlab-client-secret cfg/config)
|
||||
:code code
|
||||
:grant_type "authorization_code"
|
||||
:redirect_uri (build-redirect-url)}
|
||||
req {:method :post
|
||||
:headers {"content-type" "application/x-www-form-urlencoded"}
|
||||
:uri (build-token-url)
|
||||
:body (uri/map->query-string params)}
|
||||
res (http/send! req)]
|
||||
|
||||
(when (not= 200 (:status res))
|
||||
(ex/raise :type :internal
|
||||
:code :invalid-response-from-gitlab
|
||||
:context {:status (:status res)
|
||||
:body (:body res)}))
|
||||
|
||||
(try
|
||||
(let [data (json/read-str (:body res))]
|
||||
(get data "access_token"))
|
||||
(catch Throwable e
|
||||
(log/error "unexpected error on parsing response body from gitlab access token request" e)
|
||||
nil))))
|
||||
|
||||
|
||||
(defn- get-user-info
|
||||
[token]
|
||||
(let [req {:uri (build-user-info-url)
|
||||
:headers {"Authorization" (str "Bearer " token)}
|
||||
:method :get}
|
||||
res (http/send! req)]
|
||||
|
||||
(when (not= 200 (:status res))
|
||||
(ex/raise :type :internal
|
||||
:code :invalid-response-from-gitlab
|
||||
:context {:status (:status res)
|
||||
:body (:body res)}))
|
||||
|
||||
(try
|
||||
(let [data (json/read-str (:body res))]
|
||||
;; (clojure.pprint/pprint data)
|
||||
{:email (get data "email")
|
||||
:fullname (get data "name")})
|
||||
(catch Throwable e
|
||||
(log/error "unexpected error on parsing response body from gitlab access token request" e)
|
||||
nil))))
|
||||
|
||||
(defn auth
|
||||
[_req]
|
||||
(let [token (tokens/generate
|
||||
{:iss :gitlab-oauth
|
||||
:exp (dt/in-future "15m")})
|
||||
|
||||
params {:client_id (:gitlab-client-id cfg/config)
|
||||
:redirect_uri (build-redirect-url)
|
||||
:response_type "code"
|
||||
:state token
|
||||
:scope scope}
|
||||
query (uri/map->query-string params)
|
||||
uri (-> (build-oauth-uri)
|
||||
(assoc :query query))]
|
||||
{:status 200
|
||||
:body {:redirect-uri (str uri)}}))
|
||||
|
||||
(defn callback
|
||||
[req]
|
||||
(let [token (get-in req [:params :state])
|
||||
_ (tokens/verify token {:iss :gitlab-oauth})
|
||||
info (some-> (get-in req [:params :code])
|
||||
(get-access-token)
|
||||
(get-user-info))]
|
||||
|
||||
(when-not info
|
||||
(ex/raise :type :authentication
|
||||
:code :unable-to-authenticate-with-gitlab))
|
||||
|
||||
(let [profile (sm/handle {::sm/type :login-or-register
|
||||
:email (:email info)
|
||||
:fullname (:fullname info)})
|
||||
uagent (get-in req [:headers "user-agent"])
|
||||
|
||||
token (tokens/generate
|
||||
{:iss :auth
|
||||
:exp (dt/in-future "15m")
|
||||
:profile-id (:id profile)})
|
||||
|
||||
uri (-> (uri/uri (:public-uri cfg/config))
|
||||
(assoc :path "/#/auth/verify-token")
|
||||
(assoc :query (uri/map->query-string {:token token})))
|
||||
sid (session/create (:id profile) uagent)]
|
||||
|
||||
{:status 302
|
||||
:headers {"location" (str uri)}
|
||||
:cookies (session/cookies sid)
|
||||
:body ""})))
|
||||
@@ -1,131 +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/.
|
||||
;;
|
||||
;; This Source Code Form is "Incompatible With Secondary Licenses", as
|
||||
;; defined by the Mozilla Public License, v. 2.0.
|
||||
;;
|
||||
;; Copyright (c) 2020 UXBOX Labs SL
|
||||
|
||||
(ns app.http.auth.google
|
||||
(:require
|
||||
[app.common.exceptions :as ex]
|
||||
[app.config :as cfg]
|
||||
[app.http.session :as session]
|
||||
[app.services.mutations :as sm]
|
||||
[app.services.tokens :as tokens]
|
||||
[app.util.http :as http]
|
||||
[app.util.time :as dt]
|
||||
[clojure.data.json :as json]
|
||||
[clojure.tools.logging :as log]
|
||||
[lambdaisland.uri :as uri]))
|
||||
|
||||
(def base-goauth-uri "https://accounts.google.com/o/oauth2/v2/auth")
|
||||
|
||||
(def scope
|
||||
(str "email profile "
|
||||
"https://www.googleapis.com/auth/userinfo.email "
|
||||
"https://www.googleapis.com/auth/userinfo.profile "
|
||||
"openid"))
|
||||
|
||||
(defn- build-redirect-url
|
||||
[]
|
||||
(let [public (uri/uri (:public-uri cfg/config))]
|
||||
(str (assoc public :path "/api/oauth/google/callback"))))
|
||||
|
||||
(defn- get-access-token
|
||||
[code]
|
||||
(let [params {:code code
|
||||
:client_id (:google-client-id cfg/config)
|
||||
:client_secret (:google-client-secret cfg/config)
|
||||
:redirect_uri (build-redirect-url)
|
||||
:grant_type "authorization_code"}
|
||||
req {:method :post
|
||||
:headers {"content-type" "application/x-www-form-urlencoded"}
|
||||
:uri "https://oauth2.googleapis.com/token"
|
||||
:body (uri/map->query-string params)}
|
||||
res (http/send! req)]
|
||||
|
||||
(when (not= 200 (:status res))
|
||||
(ex/raise :type :internal
|
||||
:code :invalid-response-from-google
|
||||
:context {:status (:status res)
|
||||
:body (:body res)}))
|
||||
|
||||
(try
|
||||
(let [data (json/read-str (:body res))]
|
||||
(get data "access_token"))
|
||||
(catch Throwable e
|
||||
(log/error "unexpected error on parsing response body from google access token request" e)
|
||||
nil))))
|
||||
|
||||
|
||||
(defn- get-user-info
|
||||
[token]
|
||||
(let [req {:uri "https://openidconnect.googleapis.com/v1/userinfo"
|
||||
:headers {"Authorization" (str "Bearer " token)}
|
||||
:method :get}
|
||||
res (http/send! req)]
|
||||
|
||||
(when (not= 200 (:status res))
|
||||
(ex/raise :type :internal
|
||||
:code :invalid-response-from-google
|
||||
:context {:status (:status res)
|
||||
:body (:body res)}))
|
||||
|
||||
(try
|
||||
(let [data (json/read-str (:body res))]
|
||||
;; (clojure.pprint/pprint data)
|
||||
{:email (get data "email")
|
||||
:fullname (get data "name")})
|
||||
(catch Throwable e
|
||||
(log/error "unexpected error on parsing response body from google access token request" e)
|
||||
nil))))
|
||||
|
||||
(defn auth
|
||||
[_req]
|
||||
(let [token (tokens/generate {:iss :google-oauth :exp (dt/in-future "15m")})
|
||||
params {:scope scope
|
||||
:access_type "offline"
|
||||
:include_granted_scopes true
|
||||
:state token
|
||||
:response_type "code"
|
||||
:redirect_uri (build-redirect-url)
|
||||
:client_id (:google-client-id cfg/config)}
|
||||
query (uri/map->query-string params)
|
||||
uri (-> (uri/uri base-goauth-uri)
|
||||
(assoc :query query))]
|
||||
{:status 200
|
||||
:body {:redirect-uri (str uri)}}))
|
||||
|
||||
|
||||
(defn callback
|
||||
[req]
|
||||
(let [token (get-in req [:params :state])
|
||||
_ (tokens/verify token {:iss :google-oauth})
|
||||
info (some-> (get-in req [:params :code])
|
||||
(get-access-token)
|
||||
(get-user-info))]
|
||||
|
||||
(when-not info
|
||||
(ex/raise :type :authentication
|
||||
:code :unable-to-authenticate-with-google))
|
||||
|
||||
(let [profile (sm/handle {::sm/type :login-or-register
|
||||
:email (:email info)
|
||||
:fullname (:fullname info)})
|
||||
uagent (get-in req [:headers "user-agent"])
|
||||
|
||||
token (tokens/generate
|
||||
{:iss :auth
|
||||
:exp (dt/in-future "15m")
|
||||
:profile-id (:id profile)})
|
||||
uri (-> (uri/uri (:public-uri cfg/config))
|
||||
(assoc :path "/#/auth/verify-token")
|
||||
(assoc :query (uri/map->query-string {:token token})))
|
||||
sid (session/create (:id profile) uagent)]
|
||||
|
||||
{:status 302
|
||||
:headers {"location" (str uri)}
|
||||
:cookies (session/cookies sid)
|
||||
:body ""})))
|
||||
@@ -1,80 +0,0 @@
|
||||
;; This Source Code Form is subject to the terms of the Mozilla Public
|
||||
;; License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
;;
|
||||
;; This Source Code Form is "Incompatible With Secondary Licenses", as
|
||||
;; defined by the Mozilla Public License, v. 2.0.
|
||||
;;
|
||||
;; Copyright (c) 2020 UXBOX Labs SL
|
||||
|
||||
(ns app.http.auth.ldap
|
||||
(:require
|
||||
[app.common.exceptions :as ex]
|
||||
[app.config :as cfg]
|
||||
[app.http.session :as session]
|
||||
[app.services.mutations :as sm]
|
||||
[clj-ldap.client :as client]
|
||||
[clojure.set :as set]
|
||||
[clojure.string]
|
||||
[clojure.tools.logging :as log]
|
||||
[mount.core :refer [defstate]]))
|
||||
|
||||
(defn replace-several [s & {:as replacements}]
|
||||
(reduce-kv clojure.string/replace s replacements))
|
||||
|
||||
(declare *ldap-pool)
|
||||
|
||||
(defstate *ldap-pool
|
||||
:start (delay
|
||||
(try
|
||||
(client/connect (merge {:host {:address (:ldap-auth-host cfg/config)
|
||||
:port (:ldap-auth-port cfg/config)}}
|
||||
(-> cfg/config
|
||||
(select-keys [:ldap-auth-ssl
|
||||
:ldap-auth-starttls
|
||||
:ldap-bind-dn
|
||||
:ldap-bind-password])
|
||||
(set/rename-keys {:ldap-auth-ssl :ssl?
|
||||
:ldap-auth-starttls :startTLS?
|
||||
:ldap-bind-dn :bind-dn
|
||||
:ldap-bind-password :password}))))
|
||||
(catch Exception e
|
||||
(log/errorf e "Cannot connect to LDAP %s:%s"
|
||||
(:ldap-auth-host cfg/config) (:ldap-auth-port cfg/config)))))
|
||||
:stop (when (realized? *ldap-pool)
|
||||
(some-> *ldap-pool deref (.close))))
|
||||
|
||||
(defn- auth-with-ldap [username password]
|
||||
(when-some [conn (some-> *ldap-pool deref)]
|
||||
(let [user-search-query (replace-several (:ldap-auth-user-query cfg/config)
|
||||
"$username" username)
|
||||
user-attributes (-> cfg/config
|
||||
(select-keys [:ldap-auth-username-attribute
|
||||
:ldap-auth-email-attribute
|
||||
:ldap-auth-fullname-attribute
|
||||
:ldap-auth-avatar-attribute])
|
||||
vals)]
|
||||
(when-some [user-entry (-> conn
|
||||
(client/search (:ldap-auth-base-dn cfg/config)
|
||||
{:filter user-search-query
|
||||
:sizelimit 1
|
||||
:attributes user-attributes})
|
||||
(first))]
|
||||
(when-not (client/bind? conn (:dn user-entry) password)
|
||||
(ex/raise :type :authentication
|
||||
:code :wrong-credentials))
|
||||
(set/rename-keys user-entry {(keyword (:ldap-auth-avatar-attribute cfg/config)) :photo
|
||||
(keyword (:ldap-auth-fullname-attribute cfg/config)) :fullname
|
||||
(keyword (:ldap-auth-email-attribute cfg/config)) :email})))))
|
||||
|
||||
(defn auth [req]
|
||||
(let [data (:body-params req)
|
||||
uagent (get-in req [:headers "user-agent"])]
|
||||
(when-some [info (auth-with-ldap (:email data) (:password data))]
|
||||
(let [profile (sm/handle {::sm/type :login-or-register
|
||||
:email (:email info)
|
||||
:fullname (:fullname info)})
|
||||
sid (session/create (:id profile) uagent)]
|
||||
{:status 200
|
||||
:cookies (session/cookies sid)
|
||||
:body profile}))))
|
||||
201
backend/src/app/http/awsns.clj
Normal file
@@ -0,0 +1,201 @@
|
||||
;; 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.awsns
|
||||
"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]
|
||||
[clojure.spec.alpha :as s]
|
||||
[cuerdas.core :as str]
|
||||
[integrant.core :as ig]
|
||||
[jsonista.core :as j]))
|
||||
|
||||
(declare parse-json)
|
||||
(declare parse-notification)
|
||||
(declare process-report)
|
||||
|
||||
(defmethod ig/pre-init-spec ::handler [_]
|
||||
(s/keys :req-un [::db/pool]))
|
||||
|
||||
(defmethod ig/init-key ::handler
|
||||
[_ cfg]
|
||||
(fn [request respond _]
|
||||
(try
|
||||
(let [body (parse-json (slurp (:body request)))
|
||||
mtype (get body "Type")]
|
||||
(cond
|
||||
(= mtype "SubscriptionConfirmation")
|
||||
(let [surl (get body "SubscribeURL")
|
||||
stopic (get body "TopicArn")]
|
||||
(l/info :action "subscription received" :topic stopic :url surl)
|
||||
(http/send! {:uri surl :method :post :timeout 10000}))
|
||||
|
||||
(= mtype "Notification")
|
||||
(when-let [message (parse-json (get body "Message"))]
|
||||
(let [notification (parse-notification cfg message)]
|
||||
(process-report cfg notification)))
|
||||
|
||||
:else
|
||||
(l/warn :hint "unexpected data received"
|
||||
:report (pr-str body))))
|
||||
(catch Throwable cause
|
||||
(l/error :hint "unexpected exception on awsns handler"
|
||||
:cause cause)))
|
||||
|
||||
(respond {:status 200 :body ""})))
|
||||
|
||||
(defn- parse-bounce
|
||||
[data]
|
||||
{:type "bounce"
|
||||
:kind (str/lower (get data "bounceType"))
|
||||
:category (str/lower (get data "bounceSubType"))
|
||||
:feedback-id (get data "feedbackId")
|
||||
:timestamp (get data "timestamp")
|
||||
:recipients (->> (get data "bouncedRecipients")
|
||||
(mapv (fn [item]
|
||||
{:email (str/lower (get item "emailAddress"))
|
||||
:status (get item "status")
|
||||
:action (get item "action")
|
||||
:dcode (get item "diagnosticCode")})))})
|
||||
|
||||
(defn- parse-complaint
|
||||
[data]
|
||||
{:type "complaint"
|
||||
:user-agent (get data "userAgent")
|
||||
:kind (get data "complaintFeedbackType")
|
||||
:category (get data "complaintSubType")
|
||||
:timestamp (get data "arrivalDate")
|
||||
:feedback-id (get data "feedbackId")
|
||||
:recipients (->> (get data "complainedRecipients")
|
||||
(mapv #(get % "emailAddress"))
|
||||
(mapv str/lower))})
|
||||
|
||||
(defn- extract-headers
|
||||
[mail]
|
||||
(reduce (fn [acc item]
|
||||
(let [key (get item "name")
|
||||
val (get item "value")]
|
||||
(assoc acc (str/lower key) val)))
|
||||
{}
|
||||
(get mail "headers")))
|
||||
|
||||
(defn- extract-identity
|
||||
[{:keys [tokens] :as cfg} headers]
|
||||
(let [tdata (get headers "x-penpot-data")]
|
||||
(when-not (str/empty? tdata)
|
||||
(let [result (tokens :verify {:token tdata :iss :profile-identity})]
|
||||
(:profile-id result)))))
|
||||
|
||||
(defn- parse-notification
|
||||
[cfg message]
|
||||
(let [type (get message "notificationType")
|
||||
data (case type
|
||||
"Bounce" (parse-bounce (get message "bounce"))
|
||||
"Complaint" (parse-complaint (get message "complaint"))
|
||||
{:type (keyword (str/lower type))
|
||||
:message message})]
|
||||
(when data
|
||||
(let [mail (get message "mail")]
|
||||
(when-not mail
|
||||
(ex/raise :type :internal
|
||||
:code :incomplete-notification
|
||||
:hint "no email data received, please enable full headers report"))
|
||||
(let [headers (extract-headers mail)
|
||||
mail {:destination (get mail "destination")
|
||||
:source (get mail "source")
|
||||
:timestamp (get mail "timestamp")
|
||||
:subject (get-in mail ["commonHeaders" "subject"])
|
||||
:headers headers}]
|
||||
(assoc data
|
||||
:mail mail
|
||||
:profile-id (extract-identity cfg headers)))))))
|
||||
|
||||
(defn- parse-json
|
||||
[v]
|
||||
(ex/ignoring
|
||||
(j/read-value v)))
|
||||
|
||||
(defn- register-bounce-for-profile
|
||||
[{:keys [pool]} {:keys [type kind profile-id] :as report}]
|
||||
(when (= kind "permanent")
|
||||
(db/with-atomic [conn pool]
|
||||
(db/insert! conn :profile-complaint-report
|
||||
{:profile-id profile-id
|
||||
:type (name type)
|
||||
:content (db/tjson report)})
|
||||
|
||||
;; TODO: maybe also try to find profiles by mail and if exists
|
||||
;; register profile reports for them?
|
||||
(doseq [recipient (:recipients report)]
|
||||
(db/insert! conn :global-complaint-report
|
||||
{:email (:email recipient)
|
||||
:type (name type)
|
||||
:content (db/tjson report)}))
|
||||
|
||||
(let [profile (db/exec-one! conn (sql/select :profile {:id profile-id}))]
|
||||
(when (some #(= (:email profile) (:email %)) (:recipients report))
|
||||
;; If the report matches the profile email, this means that
|
||||
;; the report is for itself, can be caused when a user
|
||||
;; registers with an invalid email or the user email is
|
||||
;; permanently rejecting receiving the email. In this case we
|
||||
;; have no option to mark the user as muted (and in this case
|
||||
;; the profile will be also inactive.
|
||||
(db/update! conn :profile
|
||||
{:is-muted true}
|
||||
{:id profile-id}))))))
|
||||
|
||||
(defn- register-complaint-for-profile
|
||||
[{:keys [pool]} {:keys [type profile-id] :as report}]
|
||||
(db/with-atomic [conn pool]
|
||||
(db/insert! conn :profile-complaint-report
|
||||
{:profile-id profile-id
|
||||
:type (name type)
|
||||
:content (db/tjson report)})
|
||||
|
||||
;; TODO: maybe also try to find profiles by email and if exists
|
||||
;; register profile reports for them?
|
||||
(doseq [email (:recipients report)]
|
||||
(db/insert! conn :global-complaint-report
|
||||
{:email email
|
||||
:type (name type)
|
||||
:content (db/tjson report)}))
|
||||
|
||||
(let [profile (db/exec-one! conn (sql/select :profile {:id profile-id}))]
|
||||
(when (some #(= % (:email profile)) (:recipients report))
|
||||
;; If the report matches the profile email, this means that
|
||||
;; the report is for itself, rare case but can happen; In this
|
||||
;; case just mark profile as muted (very rare case).
|
||||
(db/update! conn :profile
|
||||
{:is-muted true}
|
||||
{:id profile-id})))))
|
||||
|
||||
(defn- process-report
|
||||
[cfg {:keys [type profile-id] :as report}]
|
||||
(l/trace :action "processing report" :report (pr-str report))
|
||||
(cond
|
||||
;; In this case we receive a bounce/complaint notification without
|
||||
;; confirmed identity, we just emit a warning but do nothing about
|
||||
;; it because this is not a normal case. All notifications should
|
||||
;; come with profile identity.
|
||||
(nil? profile-id)
|
||||
(l/warn :msg "a notification without identity received from AWS"
|
||||
:report (pr-str report))
|
||||
|
||||
(= "bounce" type)
|
||||
(register-bounce-for-profile cfg report)
|
||||
|
||||
(= "complaint" type)
|
||||
(register-complaint-for-profile cfg report)
|
||||
|
||||
:else
|
||||
(l/warn :msg "unrecognized report received from AWS"
|
||||
:report (pr-str report))))
|
||||
|
||||
|
||||
227
backend/src/app/http/debug.clj
Normal file
@@ -0,0 +1,227 @@
|
||||
;; 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.debug
|
||||
(:require
|
||||
[app.common.data :as d]
|
||||
[app.common.exceptions :as ex]
|
||||
[app.common.spec :as us]
|
||||
[app.common.uuid :as uuid]
|
||||
[app.config :as cf]
|
||||
[app.db :as db]
|
||||
[app.rpc.mutations.files :as m.files]
|
||||
[app.rpc.queries.profile :as profile]
|
||||
[app.util.async :as async]
|
||||
[app.util.blob :as blob]
|
||||
[app.util.template :as tmpl]
|
||||
[app.util.time :as dt]
|
||||
[app.worker :as wrk]
|
||||
[clojure.java.io :as io]
|
||||
[clojure.spec.alpha :as s]
|
||||
[cuerdas.core :as str]
|
||||
[datoteka.core :as fs]
|
||||
[fipp.edn :as fpp]
|
||||
[integrant.core :as ig]
|
||||
[promesa.core :as p]))
|
||||
|
||||
;; (selmer.parser/cache-off!)
|
||||
|
||||
(defn authorized?
|
||||
[pool {:keys [profile-id]}]
|
||||
(or (= "devenv" (cf/get :host))
|
||||
(let [profile (ex/ignoring (profile/retrieve-profile-data pool profile-id))
|
||||
admins (or (cf/get :admins) #{})]
|
||||
(contains? admins (:email profile)))))
|
||||
|
||||
(defn index
|
||||
[{:keys [pool]} request]
|
||||
(when-not (authorized? pool request)
|
||||
(ex/raise :type :authentication
|
||||
:code :only-admins-allowed))
|
||||
|
||||
{:status 200
|
||||
:headers {"content-type" "text/html"}
|
||||
:body (-> (io/resource "templates/debug.tmpl")
|
||||
(tmpl/render {}))})
|
||||
|
||||
|
||||
(def sql:retrieve-range-of-changes
|
||||
"select revn, changes from file_change where file_id=? and revn >= ? and revn <= ? order by revn")
|
||||
|
||||
(def sql:retrieve-single-change
|
||||
"select revn, changes, data from file_change where file_id=? and revn = ?")
|
||||
|
||||
(defn prepare-response
|
||||
[{:keys [params] :as request} body]
|
||||
(when-not body
|
||||
(ex/raise :type :not-found
|
||||
:code :enpty-data
|
||||
:hint "empty response"))
|
||||
|
||||
(cond-> {:status 200
|
||||
:headers {"content-type" "application/transit+json"}
|
||||
:body body}
|
||||
(contains? params :download)
|
||||
(update :headers assoc "content-disposition" "attachment")))
|
||||
|
||||
(defn retrieve-file-data
|
||||
[{:keys [pool]} {:keys [params] :as request}]
|
||||
(when-not (authorized? pool request)
|
||||
(ex/raise :type :authentication
|
||||
:code :only-admins-allowed))
|
||||
|
||||
(let [file-id (some-> (get-in request [:params :file-id]) uuid/uuid)
|
||||
revn (some-> (get-in request [:params :revn]) d/parse-integer)]
|
||||
(when-not file-id
|
||||
(ex/raise :type :validation
|
||||
:code :missing-arguments))
|
||||
|
||||
(let [data (if (integer? revn)
|
||||
(some-> (db/exec-one! pool [sql:retrieve-single-change file-id revn]) :data)
|
||||
(some-> (db/get-by-id pool :file file-id) :data))]
|
||||
(if (contains? params :download)
|
||||
(-> (prepare-response request data)
|
||||
(update :headers assoc "content-type" "application/octet-stream"))
|
||||
(prepare-response request (some-> data blob/decode))))))
|
||||
|
||||
(defn upload-file-data
|
||||
[{:keys [pool]} {:keys [profile-id params] :as request}]
|
||||
(let [project-id (some-> (profile/retrieve-additional-data pool profile-id) :default-project-id)
|
||||
data (some-> params :file :tempfile fs/slurp-bytes blob/decode)]
|
||||
|
||||
(if (and data project-id)
|
||||
(let [fname (str "imported-file-" (dt/now))]
|
||||
(m.files/create-file pool {:id (uuid/next)
|
||||
:name fname
|
||||
:project-id project-id
|
||||
:profile-id profile-id
|
||||
:data data})
|
||||
{:status 200
|
||||
:body "OK"})
|
||||
{:status 500
|
||||
:body "error"})))
|
||||
|
||||
(defn retrieve-file-changes
|
||||
[{:keys [pool]} request]
|
||||
(when-not (authorized? pool request)
|
||||
(ex/raise :type :authentication
|
||||
:code :only-admins-allowed))
|
||||
|
||||
(let [file-id (some-> (get-in request [:params :id]) uuid/uuid)
|
||||
revn (or (get-in request [:params :revn]) "latest")]
|
||||
|
||||
(when (or (not file-id) (not revn))
|
||||
(ex/raise :type :validation
|
||||
:code :invalid-arguments
|
||||
:hint "missing arguments"))
|
||||
|
||||
(cond
|
||||
(d/num-string? revn)
|
||||
(let [item (db/exec-one! pool [sql:retrieve-single-change file-id (d/parse-integer revn)])]
|
||||
(prepare-response request (some-> item :changes blob/decode vec)))
|
||||
|
||||
(str/includes? revn ":")
|
||||
(let [[start end] (->> (str/split revn #":")
|
||||
(map str/trim)
|
||||
(map d/parse-integer))
|
||||
items (db/exec! pool [sql:retrieve-range-of-changes file-id start end])]
|
||||
(prepare-response request
|
||||
(some->> items
|
||||
(map :changes)
|
||||
(map blob/decode)
|
||||
(mapcat identity)
|
||||
(vec))))
|
||||
:else
|
||||
(ex/raise :type :validation :code :invalid-arguments))))
|
||||
|
||||
|
||||
(defn retrieve-error
|
||||
[{:keys [pool]} request]
|
||||
(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
|
||||
(some-> (db/get-by-id pool :server-error-report id) :content db/decode-transit-pgobject)))
|
||||
|
||||
(render-template [report]
|
||||
(let [context (dissoc report
|
||||
:trace :cause :params :data :spec-problems
|
||||
:spec-explain :spec-value :error :explain :hint)
|
||||
params {:context (with-out-str (fpp/pprint context {:width 300}))
|
||||
:hint (:hint report)
|
||||
:spec-explain (:spec-explain report)
|
||||
:spec-problems (:spec-problems report)
|
||||
:spec-value (:spec-value report)
|
||||
:data (:data report)
|
||||
:trace (or (:trace report)
|
||||
(some-> report :error :trace))
|
||||
:params (:params report)}]
|
||||
(-> (io/resource "templates/error-report.tmpl")
|
||||
(tmpl/render params))))
|
||||
]
|
||||
|
||||
(when-not (authorized? pool request)
|
||||
(ex/raise :type :authentication
|
||||
:code :only-admins-allowed))
|
||||
|
||||
(let [result (some-> (parse-id request)
|
||||
(retrieve-report)
|
||||
(render-template))]
|
||||
(if result
|
||||
{:status 200
|
||||
:headers {"content-type" "text/html; charset=utf-8"
|
||||
"x-robots-tag" "noindex"}
|
||||
:body result}
|
||||
{:status 404
|
||||
:body "not found"}))))
|
||||
|
||||
(def sql:error-reports
|
||||
"select id, created_at from server_error_report order by created_at desc limit 100")
|
||||
|
||||
(defn retrieve-error-list
|
||||
[{:keys [pool]} request]
|
||||
(when-not (authorized? pool request)
|
||||
(ex/raise :type :authentication
|
||||
:code :only-admins-allowed))
|
||||
(let [items (db/exec! pool [sql:error-reports])
|
||||
items (map #(update % :created-at dt/format-instant :rfc1123) items)]
|
||||
{:status 200
|
||||
:headers {"content-type" "text/html; charset=utf-8"
|
||||
"x-robots-tag" "noindex"}
|
||||
:body (-> (io/resource "templates/error-list.tmpl")
|
||||
(tmpl/render {:items items}))}))
|
||||
|
||||
(defn health-check
|
||||
"Mainly a task that performs a health check."
|
||||
[{:keys [pool]} _]
|
||||
(db/with-atomic [conn pool]
|
||||
(db/exec-one! conn ["select count(*) as count from server_prop;"])
|
||||
{:status 200 :body "Ok"}))
|
||||
|
||||
(defn- wrap-async
|
||||
[{:keys [executor] :as cfg} f]
|
||||
(fn [request respond raise]
|
||||
(-> (async/with-dispatch executor
|
||||
(f cfg request))
|
||||
(p/then respond)
|
||||
(p/catch raise))))
|
||||
|
||||
(defmethod ig/pre-init-spec ::handlers [_]
|
||||
(s/keys :req-un [::db/pool ::wrk/executor]))
|
||||
|
||||
(defmethod ig/init-key ::handlers
|
||||
[_ cfg]
|
||||
{:index (wrap-async cfg index)
|
||||
:health-check (wrap-async cfg health-check)
|
||||
:retrieve-file-data (wrap-async cfg retrieve-file-data)
|
||||
:retrieve-file-changes (wrap-async cfg retrieve-file-changes)
|
||||
:retrieve-error (wrap-async cfg retrieve-error)
|
||||
:retrieve-error-list (wrap-async cfg retrieve-error-list)
|
||||
:upload-file-data (wrap-async cfg upload-file-data)})
|
||||
54
backend/src/app/http/doc.clj
Normal file
@@ -0,0 +1,54 @@
|
||||
;; 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 [_ respond _]
|
||||
(respond {:status 200
|
||||
:body (-> (io/resource "api-doc.tmpl")
|
||||
(tmpl/render context))}))
|
||||
(fn [_ respond _]
|
||||
(respond {:status 404 :body ""})))))
|
||||
@@ -2,18 +2,44 @@
|
||||
;; 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/.
|
||||
;;
|
||||
;; This Source Code Form is "Incompatible With Secondary Licenses", as
|
||||
;; defined by the Mozilla Public License, v. 2.0.
|
||||
;;
|
||||
;; Copyright (c) 2020 UXBOX Labs SL
|
||||
;; Copyright (c) UXBOX Labs SL
|
||||
|
||||
(ns app.http.errors
|
||||
"A errors handling for the http server."
|
||||
(:require
|
||||
[app.common.exceptions :as ex]
|
||||
[clojure.tools.logging :as log]
|
||||
[cuerdas.core :as str]
|
||||
[expound.alpha :as expound]))
|
||||
[app.common.logging :as l]
|
||||
[app.common.spec :as us]
|
||||
[clojure.spec.alpha :as s]
|
||||
[cuerdas.core :as str]))
|
||||
|
||||
(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 get-error-context
|
||||
[request error]
|
||||
(let [data (ex-data error)]
|
||||
(merge
|
||||
{:path (:uri request)
|
||||
:method (:request-method request)
|
||||
:hint (ex-message error)
|
||||
:params (:params request)
|
||||
|
||||
:spec-problems (some->> data ::s/problems (take 10) seq vec)
|
||||
:spec-value (some->> data ::s/value)
|
||||
:data (some-> data (dissoc ::s/problems ::s/value ::s/spec))
|
||||
:ip-addr (parse-client-ip request)
|
||||
:profile-id (:profile-id request)}
|
||||
|
||||
(let [headers (:headers request)]
|
||||
{:user-agent (get headers "user-agent")
|
||||
:frontend-version (get headers "x-frontend-version" "unknown")})
|
||||
|
||||
(when (and data (::s/problems data))
|
||||
{:spec-explain (us/pretty-explain data)}))))
|
||||
|
||||
(defmulti handle-exception
|
||||
(fn [err & _rest]
|
||||
@@ -21,86 +47,86 @@
|
||||
(or (:type edata)
|
||||
(class err)))))
|
||||
|
||||
(defmethod handle-exception :authorization
|
||||
(defmethod handle-exception :authentication
|
||||
[err _]
|
||||
{:status 403
|
||||
:body (ex-data err)})
|
||||
{:status 401 :body (ex-data err)})
|
||||
|
||||
(defmethod handle-exception :restriction
|
||||
[err _]
|
||||
{:status 400 :body (ex-data err)})
|
||||
|
||||
(defmethod handle-exception :validation
|
||||
[err req]
|
||||
(let [header (get-in req [:headers "accept"])
|
||||
edata (ex-data err)]
|
||||
(cond
|
||||
(and (str/starts-with? header "text/html")
|
||||
(= :spec-validation (:code edata)))
|
||||
{:status 400
|
||||
:headers {"content-type" "text/html"}
|
||||
:body (str "<pre style='font-size:16px'>"
|
||||
(with-out-str
|
||||
(:data edata))
|
||||
"</pre>\n")}
|
||||
:else
|
||||
{:status 400
|
||||
:body edata})))
|
||||
[err _]
|
||||
(let [data (ex-data err)
|
||||
explain (us/pretty-explain data)]
|
||||
{:status 400
|
||||
:body (-> data
|
||||
(dissoc ::s/problems)
|
||||
(dissoc ::s/value)
|
||||
(cond-> explain (assoc :explain explain)))}))
|
||||
|
||||
(defmethod handle-exception :ratelimit
|
||||
[_ _]
|
||||
{:status 429
|
||||
:headers {"retry-after" 1000}
|
||||
:body ""})
|
||||
(defmethod handle-exception :assertion
|
||||
[error request]
|
||||
(let [edata (ex-data error)]
|
||||
(l/error ::l/raw (ex-message error)
|
||||
::l/context (get-error-context request error)
|
||||
:cause error)
|
||||
|
||||
{:status 500
|
||||
:body {:type :server-error
|
||||
:code :assertion
|
||||
:data (dissoc edata ::s/problems ::s/value ::s/spec)}}))
|
||||
|
||||
(defmethod handle-exception :not-found
|
||||
[err _]
|
||||
(let [response (ex-data err)]
|
||||
{:status 404
|
||||
:body response}))
|
||||
|
||||
(defmethod handle-exception :service-error
|
||||
[err req]
|
||||
(handle-exception (.getCause ^Throwable err) req))
|
||||
|
||||
(defmethod handle-exception :parse
|
||||
[err _]
|
||||
{:status 400
|
||||
:body {:type :parse
|
||||
:message (ex-message err)}})
|
||||
|
||||
(defn get-context-string
|
||||
[err request]
|
||||
(str
|
||||
"=| uri: " (pr-str (:uri request)) "\n"
|
||||
"=| method: " (pr-str (:request-method request)) "\n"
|
||||
"=| path-params: " (pr-str (:path-params request)) "\n"
|
||||
"=| query-params: " (pr-str (:query-params request)) "\n"
|
||||
|
||||
(when-let [bparams (:body-params request)]
|
||||
(str "=| body-params: " (pr-str bparams) "\n"))
|
||||
|
||||
(when (ex/ex-info? err)
|
||||
(str "=| ex-data: " (pr-str (ex-data err)) "\n"))
|
||||
|
||||
"\n"))
|
||||
|
||||
|
||||
(defmethod handle-exception :assertion
|
||||
[err request]
|
||||
(let [{:keys [data] :as edata} (ex-data err)]
|
||||
(log/errorf err
|
||||
(str "Assertion error\n"
|
||||
(get-context-string err request)
|
||||
(with-out-str (expound/printer data))))
|
||||
{:status 500
|
||||
:body {:type :internal-error
|
||||
:message "Assertion error"
|
||||
:data (ex-data err)}}))
|
||||
{:status 404 :body (ex-data err)})
|
||||
|
||||
(defmethod handle-exception :default
|
||||
[err request]
|
||||
(log/errorf err (str "Internal Error\n" (get-context-string err request)))
|
||||
{:status 500
|
||||
:body {:type :internal-error
|
||||
:message (ex-message err)
|
||||
:data (ex-data err)}})
|
||||
[error request]
|
||||
(let [edata (ex-data error)]
|
||||
;; NOTE: this is a special case for the idle-in-transaction error;
|
||||
;; when it happens, the connection is automatically closed and
|
||||
;; next-jdbc combines the two errors in a single ex-info. We only
|
||||
;; need the :handling error, because the :rollback error will be
|
||||
;; always "connection closed".
|
||||
(if (and (ex/exception? (:rollback edata))
|
||||
(ex/exception? (:handling edata)))
|
||||
(handle-exception (:handling edata) request)
|
||||
(do
|
||||
(l/error ::l/raw (ex-message error)
|
||||
::l/context (get-error-context request error)
|
||||
:cause error)
|
||||
{:status 500
|
||||
:body {:type :server-error
|
||||
:code :unexpected
|
||||
:hint (ex-message error)
|
||||
:data edata}}))))
|
||||
|
||||
(defmethod handle-exception org.postgresql.util.PSQLException
|
||||
[error request]
|
||||
(let [state (.getSQLState ^java.sql.SQLException error)]
|
||||
(l/error ::l/raw (ex-message error)
|
||||
::l/context (get-error-context request error)
|
||||
:cause error)
|
||||
(cond
|
||||
(= state "57014")
|
||||
{:status 504
|
||||
:body {:type :server-timeout
|
||||
:code :statement-timeout
|
||||
:hint (ex-message error)}}
|
||||
|
||||
(= state "25P03")
|
||||
{:status 504
|
||||
:body {:type :server-timeout
|
||||
:code :idle-in-transaction-timeout
|
||||
:hint (ex-message error)}}
|
||||
|
||||
:else
|
||||
{:status 500
|
||||
:body {:type :server-error
|
||||
:code :psql-exception
|
||||
:hint (ex-message error)
|
||||
:state state}})))
|
||||
|
||||
(defn handle
|
||||
[error req]
|
||||
|
||||
78
backend/src/app/http/feedback.clj
Normal file
@@ -0,0 +1,78 @@
|
||||
;; 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.feedback
|
||||
"A general purpose feedback module."
|
||||
(:require
|
||||
[app.common.data :as d]
|
||||
[app.common.exceptions :as ex]
|
||||
[app.common.spec :as us]
|
||||
[app.config :as cf]
|
||||
[app.db :as db]
|
||||
[app.emails :as eml]
|
||||
[app.rpc.queries.profile :as profile]
|
||||
[app.worker :as wrk]
|
||||
[clojure.spec.alpha :as s]
|
||||
[integrant.core :as ig]
|
||||
[promesa.core :as p]
|
||||
[promesa.exec :as px]))
|
||||
|
||||
(declare ^:private send-feedback)
|
||||
(declare ^:private handler)
|
||||
|
||||
(defmethod ig/pre-init-spec ::handler [_]
|
||||
(s/keys :req-un [::db/pool ::wrk/executor]))
|
||||
|
||||
(defmethod ig/init-key ::handler
|
||||
[_ {:keys [executor] :as cfg}]
|
||||
(let [enabled? (contains? cf/flags :user-feedback)]
|
||||
(if enabled?
|
||||
(fn [request respond raise]
|
||||
(-> (px/submit! executor #(handler cfg request))
|
||||
(p/then' respond)
|
||||
(p/catch raise)))
|
||||
(fn [_ _ raise]
|
||||
(raise (ex/error :type :validation
|
||||
:code :feedback-disabled
|
||||
:hint "feedback module is disabled"))))))
|
||||
|
||||
(defn- handler
|
||||
[{:keys [pool] :as cfg} {:keys [profile-id] :as request}]
|
||||
(let [ftoken (cf/get :feedback-token ::no-token)
|
||||
token (get-in request [:headers "x-feedback-token"])
|
||||
params (d/merge (:params request)
|
||||
(:body-params request))]
|
||||
(cond
|
||||
(uuid? profile-id)
|
||||
(let [profile (profile/retrieve-profile-data pool profile-id)
|
||||
params (assoc params :from (:email profile))]
|
||||
(send-feedback pool profile params))
|
||||
|
||||
(= token ftoken)
|
||||
(send-feedback cfg nil params))
|
||||
|
||||
{:status 204 :body ""}))
|
||||
|
||||
(s/def ::content ::us/string)
|
||||
(s/def ::from ::us/email)
|
||||
(s/def ::subject ::us/string)
|
||||
(s/def ::feedback
|
||||
(s/keys :req-un [::from ::subject ::content]))
|
||||
|
||||
(defn- send-feedback
|
||||
[pool profile params]
|
||||
(let [params (us/conform ::feedback params)
|
||||
destination (cf/get :feedback-destination)]
|
||||
(eml/send! {::eml/conn pool
|
||||
::eml/factory eml/feedback
|
||||
:from destination
|
||||
:to destination
|
||||
:profile profile
|
||||
:reply-to (:from params)
|
||||
:email (:from params)
|
||||
:subject (:subject params)
|
||||
:content (:content params)})
|
||||
nil))
|
||||
@@ -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/.
|
||||
;;
|
||||
;; This Source Code Form is "Incompatible With Secondary Licenses", as
|
||||
;; defined by the Mozilla Public License, v. 2.0.
|
||||
;;
|
||||
;; Copyright (c) 2020 UXBOX Labs SL
|
||||
|
||||
(ns app.http.handlers
|
||||
(:require
|
||||
[app.common.data :as d]
|
||||
[app.common.exceptions :as ex]
|
||||
[app.emails :as emails]
|
||||
[app.http.session :as session]
|
||||
[app.services.init]
|
||||
[app.services.mutations :as sm]
|
||||
[app.services.queries :as sq]))
|
||||
|
||||
(def unauthorized-services
|
||||
#{:create-demo-profile
|
||||
:logout
|
||||
:profile
|
||||
:verify-token
|
||||
:recover-profile
|
||||
:register-profile
|
||||
:request-profile-recovery
|
||||
:viewer-bundle
|
||||
:login})
|
||||
|
||||
(defn query-handler
|
||||
[{:keys [profile-id] :as request}]
|
||||
(let [type (keyword (get-in request [:path-params :type]))
|
||||
data (assoc (:params request) ::sq/type type)
|
||||
data (if profile-id
|
||||
(assoc data :profile-id profile-id)
|
||||
(dissoc data :profile-id))]
|
||||
|
||||
(if (or (uuid? profile-id)
|
||||
(contains? unauthorized-services type))
|
||||
{:status 200
|
||||
:body (sq/handle (with-meta data {:req request}))}
|
||||
{:status 403
|
||||
:body {:type :authentication
|
||||
:code :unauthorized}})))
|
||||
|
||||
(defn mutation-handler
|
||||
[{:keys [profile-id] :as request}]
|
||||
(let [type (keyword (get-in request [:path-params :type]))
|
||||
data (d/merge (:params request)
|
||||
(:body-params request)
|
||||
(:uploads request)
|
||||
{::sm/type type})
|
||||
data (if profile-id
|
||||
(assoc data :profile-id profile-id)
|
||||
(dissoc data :profile-id))]
|
||||
|
||||
(if (or (uuid? profile-id)
|
||||
(contains? unauthorized-services type))
|
||||
(let [result (sm/handle (with-meta data {:req request}))
|
||||
mdata (meta result)
|
||||
resp {:status (if (nil? (seq result)) 204 200)
|
||||
:body result}]
|
||||
(cond->> resp
|
||||
(:transform-response mdata) ((:transform-response mdata) request)))
|
||||
{:status 403
|
||||
:body {:type :authentication
|
||||
:code :unauthorized}})))
|
||||
|
||||
(defn echo-handler
|
||||
[req]
|
||||
{:status 200
|
||||
:body {:params (:params req)
|
||||
:cookies (:cookies req)
|
||||
:headers (:headers req)}})
|
||||
|
||||
@@ -2,93 +2,152 @@
|
||||
;; 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/.
|
||||
;;
|
||||
;; This Source Code Form is "Incompatible With Secondary Licenses", as
|
||||
;; defined by the Mozilla Public License, v. 2.0.
|
||||
;;
|
||||
;; Copyright (c) 2020 UXBOX Labs SL
|
||||
;; Copyright (c) UXBOX Labs SL
|
||||
|
||||
(ns app.http.middleware
|
||||
(:require
|
||||
[app.common.exceptions :as ex]
|
||||
[app.config :as cfg]
|
||||
[app.metrics :as mtx]
|
||||
[app.util.transit :as t]
|
||||
[app.common.logging :as l]
|
||||
[app.common.transit :as t]
|
||||
[app.config :as cf]
|
||||
[app.util.json :as json]
|
||||
[ring.core.protocols :as rp]
|
||||
[ring.middleware.cookies :refer [wrap-cookies]]
|
||||
[ring.middleware.keyword-params :refer [wrap-keyword-params]]
|
||||
[ring.middleware.multipart-params :refer [wrap-multipart-params]]
|
||||
[ring.middleware.params :refer [wrap-params]]
|
||||
[ring.middleware.resource :refer [wrap-resource]]))
|
||||
[yetti.adapter :as yt]))
|
||||
|
||||
(defn- wrap-parse-request-body
|
||||
(defn wrap-server-timing
|
||||
[handler]
|
||||
(letfn [(parse-body [body]
|
||||
(try
|
||||
(let [reader (t/reader body)]
|
||||
(t/read! reader))
|
||||
(catch Exception e
|
||||
(ex/raise :type :parse
|
||||
:message "Unable to parse transit from request body."
|
||||
:cause e))))]
|
||||
(fn [{:keys [headers body request-method] :as request}]
|
||||
(handler
|
||||
(cond-> request
|
||||
(and (= "application/transit+json" (get headers "content-type"))
|
||||
(not= request-method :get))
|
||||
(assoc :body-params (parse-body body)))))))
|
||||
(letfn [(get-age [start]
|
||||
(float (/ (- (System/nanoTime) start) 1000000000)))
|
||||
|
||||
(update-headers [headers start]
|
||||
(assoc headers "Server-Timing" (str "total;dur=" (get-age start))))]
|
||||
|
||||
(fn [request respond raise]
|
||||
(let [start (System/nanoTime)]
|
||||
(handler request #(respond (update % :headers update-headers start)) raise)))))
|
||||
|
||||
(defn wrap-parse-request-body
|
||||
[handler]
|
||||
(letfn [(parse-transit [body]
|
||||
(let [reader (t/reader body)]
|
||||
(t/read! reader)))
|
||||
|
||||
(parse-json [body]
|
||||
(json/read body))
|
||||
|
||||
(handle-request [{:keys [headers body] :as request}]
|
||||
(let [ctype (get headers "content-type")]
|
||||
(case ctype
|
||||
"application/transit+json"
|
||||
(let [params (parse-transit body)]
|
||||
(-> request
|
||||
(assoc :body-params params)
|
||||
(update :params merge params)))
|
||||
|
||||
"application/json"
|
||||
(let [params (parse-json body)]
|
||||
(-> request
|
||||
(assoc :body-params params)
|
||||
(update :params merge params)))
|
||||
|
||||
request)))
|
||||
|
||||
(handle-exception [cause]
|
||||
(let [data {:type :validation
|
||||
:code :unable-to-parse-request-body
|
||||
:hint "malformed params"}]
|
||||
(l/error :hint (ex-message cause) :cause cause)
|
||||
{:status 400
|
||||
:headers {"content-type" "application/transit+json"}
|
||||
:body (t/encode-str data {:type :json-verbose})}))]
|
||||
|
||||
(fn [request respond raise]
|
||||
(try
|
||||
(let [request (handle-request request)]
|
||||
(handler request respond raise))
|
||||
(catch Exception cause
|
||||
(respond (handle-exception cause)))))))
|
||||
|
||||
(def parse-request-body
|
||||
{:name ::parse-request-body
|
||||
:compile (constantly wrap-parse-request-body)})
|
||||
|
||||
(defn- impl-format-response-body
|
||||
[response]
|
||||
(let [body (:body response)
|
||||
type (if (:debug-humanize-transit cfg/config)
|
||||
:json-verbose
|
||||
:json)]
|
||||
(cond
|
||||
(coll? body)
|
||||
(-> response
|
||||
(assoc :body (t/encode body {:type type}))
|
||||
(update :headers assoc
|
||||
"content-type"
|
||||
"application/transit+json"))
|
||||
(defn buffered-output-stream
|
||||
"Returns a buffered output stream that ignores flush calls. This is
|
||||
needed because transit-java calls flush very aggresivelly on each
|
||||
object write."
|
||||
[^java.io.OutputStream os ^long chunk-size]
|
||||
(proxy [java.io.BufferedOutputStream] [os (int chunk-size)]
|
||||
;; Explicitly do not forward flush
|
||||
(flush [])
|
||||
(close []
|
||||
(proxy-super flush)
|
||||
(proxy-super close))))
|
||||
|
||||
(nil? body)
|
||||
(assoc response :status 204 :body "")
|
||||
(def ^:const buffer-size (:http/output-buffer-size yt/base-defaults))
|
||||
|
||||
:else
|
||||
response)))
|
||||
|
||||
(defn- wrap-format-response-body
|
||||
(defn wrap-format-response-body
|
||||
[handler]
|
||||
(fn [request]
|
||||
(let [response (handler request)]
|
||||
(cond-> response
|
||||
(map? response) (impl-format-response-body)))))
|
||||
(letfn [(transit-streamable-body [data opts]
|
||||
(reify rp/StreamableResponseBody
|
||||
(write-body-to-stream [_ _ output-stream]
|
||||
;; Use the same buffer as jetty output buffer size
|
||||
(try
|
||||
(with-open [bos (buffered-output-stream output-stream buffer-size)]
|
||||
(let [tw (t/writer bos opts)]
|
||||
(t/write! tw data)))
|
||||
(catch org.eclipse.jetty.io.EofException _cause
|
||||
;; Do nothing, EOF means client closes connection abruptly
|
||||
nil)
|
||||
(catch Throwable cause
|
||||
(l/warn :hint "unexpected error on encoding response"
|
||||
:cause cause))))))
|
||||
|
||||
(impl-format-response-body [response {:keys [query-params] :as request}]
|
||||
(let [body (:body response)
|
||||
opts {:type (if (contains? query-params "transit_verbose") :json-verbose :json)}]
|
||||
(cond
|
||||
(:ws response)
|
||||
response
|
||||
|
||||
(coll? body)
|
||||
(-> response
|
||||
(update :headers assoc "content-type" "application/transit+json")
|
||||
(assoc :body (transit-streamable-body body opts)))
|
||||
|
||||
(nil? body)
|
||||
(assoc response :status 204 :body "")
|
||||
|
||||
:else
|
||||
response)))
|
||||
|
||||
(handle-response [response request]
|
||||
(cond-> response
|
||||
(map? response) (impl-format-response-body request)))]
|
||||
|
||||
(fn [request respond raise]
|
||||
(handler request
|
||||
(fn [response]
|
||||
(respond (handle-response response request)))
|
||||
raise))))
|
||||
|
||||
(def format-response-body
|
||||
{:name ::format-response-body
|
||||
:compile (constantly wrap-format-response-body)})
|
||||
|
||||
(defn- wrap-errors
|
||||
(defn wrap-errors
|
||||
[handler on-error]
|
||||
(fn [request]
|
||||
(try
|
||||
(handler request)
|
||||
(catch Throwable e
|
||||
(on-error e request)))))
|
||||
(fn [request respond _]
|
||||
(handler request respond (fn [cause]
|
||||
(-> cause (on-error request) respond)))))
|
||||
|
||||
(def errors
|
||||
{:name ::errors
|
||||
:compile (constantly wrap-errors)})
|
||||
|
||||
(def metrics
|
||||
{:name ::metrics
|
||||
:wrap (fn [handler]
|
||||
(mtx/wrap-counter handler {:id "http__requests_counter"
|
||||
:help "Absolute http requests counter."}))})
|
||||
|
||||
(def cookies
|
||||
{:name ::cookies
|
||||
:compile (constantly wrap-cookies)})
|
||||
@@ -105,33 +164,35 @@
|
||||
{:name ::keyword-params
|
||||
:compile (constantly wrap-keyword-params)})
|
||||
|
||||
(defn- wrap-development-cors
|
||||
(def server-timing
|
||||
{:name ::server-timing
|
||||
:compile (constantly wrap-server-timing)})
|
||||
|
||||
(defn wrap-cors
|
||||
[handler]
|
||||
(letfn [(add-cors-headers [response]
|
||||
(update response :headers
|
||||
(fn [headers]
|
||||
(-> headers
|
||||
(assoc "access-control-allow-origin" "http://localhost:3449")
|
||||
(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" "content-type")))))]
|
||||
(fn [request]
|
||||
(if (= (:request-method request) :options)
|
||||
(-> {:status 200 :body ""}
|
||||
(add-cors-headers))
|
||||
(let [response (handler request)]
|
||||
(add-cors-headers response))))))
|
||||
|
||||
(def development-cors
|
||||
{:name ::development-cors
|
||||
:compile (fn [& _args]
|
||||
(when *assert*
|
||||
wrap-development-cors))})
|
||||
|
||||
(def development-resources
|
||||
{:name ::development-resources
|
||||
:compile (fn [& _args]
|
||||
(when *assert*
|
||||
#(wrap-resource % "public")))})
|
||||
(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 respond raise]
|
||||
(if (= (:request-method request) :options)
|
||||
(-> {:status 200 :body ""}
|
||||
(add-cors-headers request)
|
||||
(respond))
|
||||
(handler request
|
||||
(fn [response]
|
||||
(respond (add-cors-headers response request)))
|
||||
raise))))))
|
||||
|
||||
(def cors
|
||||
{:name ::cors
|
||||
:compile (constantly wrap-cors)})
|
||||
|
||||
411
backend/src/app/http/oauth.clj
Normal file
@@ -0,0 +1,411 @@
|
||||
;; 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.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.time :as dt]
|
||||
[clojure.data.json :as json]
|
||||
[clojure.set :as set]
|
||||
[clojure.spec.alpha :as s]
|
||||
[cuerdas.core :as str]
|
||||
[integrant.core :as ig]
|
||||
[promesa.exec :as px]))
|
||||
|
||||
;; TODO: make it fully async (?)
|
||||
|
||||
(defn- build-redirect-uri
|
||||
[{:keys [provider] :as cfg}]
|
||||
(let [public (u/uri (:public-uri cfg))]
|
||||
(str (assoc public :path (str "/api/auth/oauth/" (:name provider) "/callback")))))
|
||||
|
||||
(defn- build-auth-uri
|
||||
[{:keys [provider] :as cfg} state]
|
||||
(let [params {:client_id (:client-id provider)
|
||||
:redirect_uri (build-redirect-uri cfg)
|
||||
:response_type "code"
|
||||
:state state
|
||||
:scope (str/join " " (:scopes provider []))}
|
||||
query (u/map->query-string params)]
|
||||
(-> (u/uri (:auth-uri provider))
|
||||
(assoc :query query)
|
||||
(str))))
|
||||
|
||||
(defn retrieve-access-token
|
||||
[{:keys [provider] :as cfg} code]
|
||||
(try
|
||||
(let [params {:client_id (:client-id provider)
|
||||
:client_secret (:client-secret provider)
|
||||
:code code
|
||||
:grant_type "authorization_code"
|
||||
:redirect_uri (build-redirect-uri cfg)}
|
||||
req {:method :post
|
||||
:headers {"content-type" "application/x-www-form-urlencoded"}
|
||||
:uri (:token-uri provider)
|
||||
:body (u/map->query-string params)}
|
||||
res (http/send! req)]
|
||||
(when (= 200 (:status res))
|
||||
(let [data (json/read-str (:body res))]
|
||||
{:token (get data "access_token")
|
||||
:type (get data "token_type")})))
|
||||
(catch Exception 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
|
||||
(let [req {:uri (:user-uri provider)
|
||||
:headers {"Authorization" (str (:type tdata) " " (:token tdata))}
|
||||
:timeout 6000
|
||||
:method :get}
|
||||
res (http/send! req)]
|
||||
|
||||
(when (= 200 (:status res))
|
||||
(let [info (json/read-str (:body res) :key-fn keyword)]
|
||||
{:backend (:name provider)
|
||||
:email (:email info)
|
||||
:fullname (:name info)
|
||||
:props (->> (dissoc info :name :email)
|
||||
(qualify-props provider))})))
|
||||
(catch Exception e
|
||||
(l/warn :hint "unexpected exception on retrieve-user-info" :cause e)
|
||||
nil)))
|
||||
|
||||
(s/def ::backend ::us/not-empty-string)
|
||||
(s/def ::email ::us/not-empty-string)
|
||||
(s/def ::fullname ::us/not-empty-string)
|
||||
(s/def ::props (s/map-of ::us/keyword any?))
|
||||
|
||||
(s/def ::info
|
||||
(s/keys :req-un [::backend
|
||||
::email
|
||||
::fullname
|
||||
::props]))
|
||||
|
||||
(defn retrieve-info
|
||||
[{:keys [tokens provider] :as cfg} request]
|
||||
(let [state (get-in request [:params :state])
|
||||
state (tokens :verify {:token state :iss :oauth})
|
||||
info (some->> (get-in request [:params :code])
|
||||
(retrieve-access-token cfg)
|
||||
(retrieve-user-info cfg))]
|
||||
|
||||
(when-not (s/valid? ::info info)
|
||||
(l/warn :hint "received incomplete profile info object (please set correct scopes)"
|
||||
:info (pr-str info))
|
||||
(ex/raise :type :internal
|
||||
:code :unable-to-auth
|
||||
:hint "no user info"))
|
||||
|
||||
;; If the provider is OIDC, we can proceed to check
|
||||
;; roles if they are defined.
|
||||
(when (and (= "oidc" (:name provider))
|
||||
(seq (:roles provider)))
|
||||
(let [provider-roles (into #{} (:roles provider))
|
||||
profile-roles (let [attr (cf/get :oidc-roles-attr :roles)
|
||||
roles (get info attr)]
|
||||
(cond
|
||||
(string? roles) (into #{} (str/words roles))
|
||||
(vector? roles) (into #{} roles)
|
||||
:else #{}))]
|
||||
|
||||
;; check if profile has a configured set of roles
|
||||
(when-not (set/subset? provider-roles profile-roles)
|
||||
(ex/raise :type :internal
|
||||
:code :unable-to-auth
|
||||
:hint "not enough permissions"))))
|
||||
|
||||
(cond-> info
|
||||
(some? (:invitation-token state))
|
||||
(assoc :invitation-token (:invitation-token state))
|
||||
|
||||
;; If state token comes with props, merge them. The state token
|
||||
;; props can contain pm_ and utm_ prefixed query params.
|
||||
(map? (:props state))
|
||||
(update :props merge (:props state)))))
|
||||
|
||||
;; --- HTTP HANDLERS
|
||||
|
||||
(defn extract-utm-props
|
||||
"Extracts additional data from user params."
|
||||
[params]
|
||||
(reduce-kv (fn [params k v]
|
||||
(let [sk (name k)]
|
||||
(cond-> params
|
||||
(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 executor] :as cfg} {:keys [params] :as request} respond _]
|
||||
(px/run!
|
||||
executor
|
||||
(fn []
|
||||
(let [invitation (:invitation-token params)
|
||||
props (extract-utm-props params)
|
||||
state (tokens :generate
|
||||
{:iss :oauth
|
||||
:invitation-token invitation
|
||||
:props props
|
||||
:exp (dt/in-future "15m")})
|
||||
uri (build-auth-uri cfg state)]
|
||||
|
||||
(respond
|
||||
{:status 200
|
||||
:body {:redirect-uri uri}})))))
|
||||
|
||||
(defn- callback-handler
|
||||
[{:keys [executor] :as cfg} request respond _]
|
||||
(px/run!
|
||||
executor
|
||||
(fn []
|
||||
(try
|
||||
(let [info (retrieve-info cfg request)
|
||||
profile (retrieve-profile cfg info)]
|
||||
(respond (generate-redirect cfg request info profile)))
|
||||
(catch Exception cause
|
||||
(l/warn :hint "error on oauth process" :cause cause)
|
||||
(respond (generate-error-redirect cfg cause)))))))
|
||||
|
||||
;; --- INIT
|
||||
|
||||
(declare initialize)
|
||||
|
||||
(s/def ::public-uri ::us/not-empty-string)
|
||||
(s/def ::session map?)
|
||||
(s/def ::tokens fn?)
|
||||
(s/def ::rpc map?)
|
||||
|
||||
(defmethod ig/pre-init-spec ::handler [_]
|
||||
(s/keys :req-un [::public-uri ::session ::tokens ::rpc ::db/pool]))
|
||||
|
||||
(defn wrap-handler
|
||||
[cfg handler]
|
||||
(fn [request respond raise]
|
||||
(let [provider (get-in request [:path-params :provider])
|
||||
provider (get-in @cfg [:providers provider])]
|
||||
(if provider
|
||||
(handler (assoc @cfg :provider provider)
|
||||
request
|
||||
respond
|
||||
raise)
|
||||
(raise
|
||||
(ex/error
|
||||
:type :not-found
|
||||
:provider provider
|
||||
:hint "provider not configured"))))))
|
||||
|
||||
(defmethod ig/init-key ::handler
|
||||
[_ cfg]
|
||||
(let [cfg (initialize cfg)]
|
||||
{:handler (wrap-handler cfg auth-handler)
|
||||
:callback-handler (wrap-handler cfg callback-handler)}))
|
||||
|
||||
(defn- discover-oidc-config
|
||||
[{:keys [base-uri] :as opts}]
|
||||
|
||||
(let [discovery-uri (u/join base-uri ".well-known/openid-configuration")
|
||||
response (ex/try (http/send! {:method :get :uri (str discovery-uri)}))]
|
||||
(cond
|
||||
(ex/exception? response)
|
||||
(do
|
||||
(l/warn :hint "unable to discover oidc configuration"
|
||||
:discover-uri (str discovery-uri)
|
||||
:cause response)
|
||||
nil)
|
||||
|
||||
(= 200 (:status response))
|
||||
(let [data (json/read-str (:body response))]
|
||||
{:token-uri (get data "token_endpoint")
|
||||
:auth-uri (get data "authorization_endpoint")
|
||||
:user-uri (get data "userinfo_endpoint")})
|
||||
|
||||
:else
|
||||
(do
|
||||
(l/warn :hint "unable to discover OIDC configuration"
|
||||
:uri (str discovery-uri)
|
||||
:response-status-code (:status response))
|
||||
nil))))
|
||||
|
||||
(defn- obfuscate-string
|
||||
[s]
|
||||
(if (< (count s) 10)
|
||||
(apply str (take (count s) (repeat "*")))
|
||||
(str (subs s 0 5)
|
||||
(apply str (take (- (count s) 5) (repeat "*"))))))
|
||||
|
||||
(defn- initialize-oidc-provider
|
||||
[cfg]
|
||||
(let [opts {:base-uri (cf/get :oidc-base-uri)
|
||||
:client-id (cf/get :oidc-client-id)
|
||||
:client-secret (cf/get :oidc-client-secret)
|
||||
:token-uri (cf/get :oidc-token-uri)
|
||||
:auth-uri (cf/get :oidc-auth-uri)
|
||||
:user-uri (cf/get :oidc-user-uri)
|
||||
:scopes (cf/get :oidc-scopes #{"openid" "profile" "email"})
|
||||
:roles-attr (cf/get :oidc-roles-attr)
|
||||
:roles (cf/get :oidc-roles)
|
||||
:name "oidc"}]
|
||||
(if (and (string? (:base-uri opts))
|
||||
(string? (:client-id opts))
|
||||
(string? (:client-secret opts)))
|
||||
(do
|
||||
(l/debug :hint "initialize oidc provider" :name "generic-oidc"
|
||||
:opts (update opts :client-secret obfuscate-string))
|
||||
(if (and (string? (:token-uri opts))
|
||||
(string? (:user-uri opts))
|
||||
(string? (:auth-uri opts)))
|
||||
(do
|
||||
(l/debug :hint "initialized with user provided configuration")
|
||||
(assoc-in cfg [:providers "oidc"] opts))
|
||||
(do
|
||||
(l/debug :hint "trying to discover oidc provider configuration using BASE_URI")
|
||||
(if-let [opts' (discover-oidc-config opts)]
|
||||
(do
|
||||
(l/debug :hint "discovered opts" :additional-opts opts')
|
||||
(assoc-in cfg [:providers "oidc"] (merge opts opts')))
|
||||
|
||||
cfg))))
|
||||
cfg)))
|
||||
|
||||
(defn- initialize-google-provider
|
||||
[cfg]
|
||||
(let [opts {:client-id (cf/get :google-client-id)
|
||||
:client-secret (cf/get :google-client-secret)
|
||||
:scopes #{"openid" "email" "profile"}
|
||||
:auth-uri "https://accounts.google.com/o/oauth2/v2/auth"
|
||||
:token-uri "https://oauth2.googleapis.com/token"
|
||||
:user-uri "https://openidconnect.googleapis.com/v1/userinfo"
|
||||
:name "google"}]
|
||||
(if (and (string? (:client-id opts))
|
||||
(string? (:client-secret opts)))
|
||||
(do
|
||||
(l/info :action "initialize" :provider "google"
|
||||
:opts (pr-str (update opts :client-secret obfuscate-string)))
|
||||
(assoc-in cfg [:providers "google"] opts))
|
||||
cfg)))
|
||||
|
||||
(defn- initialize-github-provider
|
||||
[cfg]
|
||||
(let [opts {:client-id (cf/get :github-client-id)
|
||||
:client-secret (cf/get :github-client-secret)
|
||||
:scopes #{"read:user" "user:email"}
|
||||
:auth-uri "https://github.com/login/oauth/authorize"
|
||||
:token-uri "https://github.com/login/oauth/access_token"
|
||||
:user-uri "https://api.github.com/user"
|
||||
:name "github"}]
|
||||
(if (and (string? (:client-id opts))
|
||||
(string? (:client-secret opts)))
|
||||
(do
|
||||
(l/info :action "initialize" :provider "github"
|
||||
:opts (pr-str (update opts :client-secret obfuscate-string)))
|
||||
(assoc-in cfg [:providers "github"] opts))
|
||||
cfg)))
|
||||
|
||||
|
||||
(defn- initialize-gitlab-provider
|
||||
[cfg]
|
||||
(let [base (cf/get :gitlab-base-uri "https://gitlab.com")
|
||||
opts {:base-uri base
|
||||
:client-id (cf/get :gitlab-client-id)
|
||||
:client-secret (cf/get :gitlab-client-secret)
|
||||
:scopes #{"read_user"}
|
||||
:auth-uri (str base "/oauth/authorize")
|
||||
:token-uri (str base "/oauth/token")
|
||||
:user-uri (str base "/api/v4/user")
|
||||
:name "gitlab"}]
|
||||
(if (and (string? (:client-id opts))
|
||||
(string? (:client-secret opts)))
|
||||
(do
|
||||
(l/info :action "initialize" :provider "gitlab"
|
||||
:opts (pr-str (update opts :client-secret obfuscate-string)))
|
||||
(assoc-in cfg [:providers "gitlab"] opts))
|
||||
cfg)))
|
||||
|
||||
(defn- initialize
|
||||
[cfg]
|
||||
(let [cfg (agent cfg :error-mode :continue)]
|
||||
(send-off cfg initialize-google-provider)
|
||||
(send-off cfg initialize-gitlab-provider)
|
||||
(send-off cfg initialize-github-provider)
|
||||
(send-off cfg initialize-oidc-provider)
|
||||
cfg))
|
||||
@@ -2,65 +2,269 @@
|
||||
;; 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/.
|
||||
;;
|
||||
;; This Source Code Form is "Incompatible With Secondary Licenses", as
|
||||
;; defined by the Mozilla Public License, v. 2.0.
|
||||
;;
|
||||
;; Copyright (c) 2020 UXBOX Labs SL
|
||||
|
||||
;; TODO: move to services.
|
||||
;; Copyright (c) UXBOX Labs SL
|
||||
|
||||
(ns app.http.session
|
||||
(:require
|
||||
[app.common.data :as d]
|
||||
[app.common.exceptions :as ex]
|
||||
[app.common.logging :as l]
|
||||
[app.config :as cfg]
|
||||
[app.db :as db]
|
||||
[buddy.core.codecs :as bc]
|
||||
[buddy.core.nonce :as bn]))
|
||||
[app.db.sql :as sql]
|
||||
[app.metrics :as mtx]
|
||||
[app.util.async :as aa]
|
||||
[app.util.time :as dt]
|
||||
[app.worker :as wrk]
|
||||
[clojure.core.async :as a]
|
||||
[clojure.spec.alpha :as s]
|
||||
[integrant.core :as ig]
|
||||
[ring.middleware.session.store :as rss]))
|
||||
|
||||
(defn next-token
|
||||
[n]
|
||||
(-> (bn/random-nonce n)
|
||||
(bc/bytes->b64u)
|
||||
(bc/bytes->str)))
|
||||
;; A default cookie name for storing the session. We don't allow to configure it.
|
||||
(def token-cookie-name "auth-token")
|
||||
|
||||
(defn extract-auth-token
|
||||
[request]
|
||||
(get-in request [:cookies "auth-token" :value]))
|
||||
;; A cookie that we can use to check from other sites of the same domain if a user
|
||||
;; is registered. Is not intended for on premise installations, although nothing
|
||||
;; prevents using it if some one wants to.
|
||||
(def authenticated-cookie-name "authenticated")
|
||||
|
||||
(defn retrieve
|
||||
[conn token]
|
||||
(deftype DatabaseStore [pool tokens]
|
||||
rss/SessionStore
|
||||
(read-session [_ token]
|
||||
(db/exec-one! pool (sql/select :http-session {:id token})))
|
||||
|
||||
(write-session [_ _ data]
|
||||
(let [profile-id (:profile-id data)
|
||||
user-agent (:user-agent data)
|
||||
token (tokens :generate {:iss "authentication"
|
||||
:iat (dt/now)
|
||||
:uid profile-id})
|
||||
|
||||
now (dt/now)
|
||||
params {:user-agent user-agent
|
||||
:profile-id profile-id
|
||||
:created-at now
|
||||
:updated-at now
|
||||
:id token}]
|
||||
(db/insert! pool :http-session params)
|
||||
token))
|
||||
|
||||
(delete-session [_ token]
|
||||
(db/delete! pool :http-session {:id token})
|
||||
nil))
|
||||
|
||||
(deftype MemoryStore [cache tokens]
|
||||
rss/SessionStore
|
||||
(read-session [_ token]
|
||||
(get @cache token))
|
||||
|
||||
(write-session [_ _ data]
|
||||
(let [profile-id (:profile-id data)
|
||||
user-agent (:user-agent data)
|
||||
token (tokens :generate {:iss "authentication"
|
||||
:iat (dt/now)
|
||||
:uid profile-id})
|
||||
params {:user-agent user-agent
|
||||
:profile-id profile-id
|
||||
:id token}]
|
||||
|
||||
(swap! cache assoc token params)
|
||||
token))
|
||||
|
||||
(delete-session [_ token]
|
||||
(swap! cache dissoc token)
|
||||
nil))
|
||||
|
||||
;; --- IMPL
|
||||
|
||||
(defn- create-session
|
||||
[store request profile-id]
|
||||
(let [params {:user-agent (get-in request [:headers "user-agent"])
|
||||
:profile-id profile-id}]
|
||||
(rss/write-session store nil params)))
|
||||
|
||||
(defn- delete-session
|
||||
[store {:keys [cookies] :as request}]
|
||||
(when-let [token (get-in cookies [token-cookie-name :value])]
|
||||
(rss/delete-session store token)))
|
||||
|
||||
(defn- retrieve-session
|
||||
[store token]
|
||||
(when token
|
||||
(-> (db/exec-one! conn ["select profile_id from http_session where id = ?" token])
|
||||
(:profile-id))))
|
||||
(rss/read-session store token)))
|
||||
|
||||
(defn retrieve-from-request
|
||||
[conn request]
|
||||
(->> (extract-auth-token request)
|
||||
(retrieve conn)))
|
||||
(defn- retrieve-from-request
|
||||
[store {:keys [cookies] :as request}]
|
||||
(->> (get-in cookies [token-cookie-name :value])
|
||||
(retrieve-session store)))
|
||||
|
||||
(defn create
|
||||
[profile-id user-agent]
|
||||
(let [id (next-token 64)]
|
||||
(db/insert! db/pool :http-session {:id id
|
||||
:profile-id profile-id
|
||||
:user-agent user-agent})
|
||||
id))
|
||||
(defn- add-cookies
|
||||
[response token]
|
||||
(let [cors? (contains? cfg/flags :cors)
|
||||
secure? (contains? cfg/flags :secure-session-cookies)
|
||||
authenticated-cookie-domain (cfg/get :authenticated-cookie-domain)]
|
||||
(update response :cookies
|
||||
(fn [cookies]
|
||||
(cond-> cookies
|
||||
:always
|
||||
(assoc token-cookie-name {:path "/"
|
||||
:http-only true
|
||||
:value token
|
||||
:same-site (if cors? :none :lax)
|
||||
:secure secure?})
|
||||
|
||||
(defn delete
|
||||
[token]
|
||||
(db/delete! db/pool :http-session {:id token})
|
||||
nil)
|
||||
(some? authenticated-cookie-domain)
|
||||
(assoc authenticated-cookie-name {:domain authenticated-cookie-domain
|
||||
:path "/"
|
||||
:value true
|
||||
:same-site :strict
|
||||
:secure secure?}))))))
|
||||
|
||||
(defn cookies
|
||||
([id] (cookies id {}))
|
||||
([id opts]
|
||||
{"auth-token" (merge opts {:value id :path "/" :http-only true})}))
|
||||
(defn- clear-cookies
|
||||
[response]
|
||||
(let [authenticated-cookie-domain (cfg/get :authenticated-cookie-domain)]
|
||||
(assoc response :cookies {token-cookie-name {:path "/"
|
||||
:value ""
|
||||
:max-age -1}
|
||||
authenticated-cookie-name {:domain authenticated-cookie-domain
|
||||
:path "/"
|
||||
:value ""
|
||||
:max-age -1}})))
|
||||
|
||||
(defn wrap-session
|
||||
[handler]
|
||||
(fn [request]
|
||||
(if-let [profile-id (retrieve-from-request db/pool request)]
|
||||
(handler (assoc request :profile-id profile-id))
|
||||
(handler request))))
|
||||
(defn- middleware
|
||||
[events-ch store handler]
|
||||
(fn [request respond raise]
|
||||
(if-let [{:keys [id profile-id] :as session} (retrieve-from-request store request)]
|
||||
(do
|
||||
(a/>!! events-ch id)
|
||||
(l/set-context! {:profile-id profile-id})
|
||||
(handler (assoc request :profile-id profile-id :session-id id) respond raise))
|
||||
(handler request respond raise))))
|
||||
|
||||
(def middleware
|
||||
{:nane ::middleware
|
||||
:compile (constantly wrap-session)})
|
||||
;; --- STATE INIT: SESSION
|
||||
|
||||
(s/def ::tokens fn?)
|
||||
(defmethod ig/pre-init-spec ::session [_]
|
||||
(s/keys :req-un [::db/pool ::tokens]))
|
||||
|
||||
(defmethod ig/prep-key ::session
|
||||
[_ cfg]
|
||||
(d/merge {:buffer-size 128}
|
||||
(d/without-nils cfg)))
|
||||
|
||||
(defmethod ig/init-key ::session
|
||||
[_ {:keys [pool tokens] :as cfg}]
|
||||
(let [events-ch (a/chan (a/dropping-buffer (:buffer-size cfg)))
|
||||
store (if (db/read-only? pool)
|
||||
(->MemoryStore (atom {}) tokens)
|
||||
(->DatabaseStore pool tokens))]
|
||||
|
||||
(when (db/read-only? pool)
|
||||
(l/warn :hint "sessions module initialized with in-memory store"))
|
||||
|
||||
(-> cfg
|
||||
(assoc ::events-ch events-ch)
|
||||
(assoc :middleware (partial middleware events-ch store))
|
||||
(assoc :create (fn [profile-id]
|
||||
(fn [request response]
|
||||
(let [token (create-session store request profile-id)]
|
||||
(add-cookies response token)))))
|
||||
(assoc :delete (fn [request response]
|
||||
(delete-session store request)
|
||||
(-> response
|
||||
(assoc :status 204)
|
||||
(assoc :body "")
|
||||
(clear-cookies)))))))
|
||||
|
||||
(defmethod ig/halt-key! ::session
|
||||
[_ data]
|
||||
(a/close! (::events-ch data)))
|
||||
|
||||
|
||||
;; --- STATE INIT: SESSION UPDATER
|
||||
|
||||
(declare update-sessions)
|
||||
|
||||
(s/def ::session map?)
|
||||
(s/def ::max-batch-age ::cfg/http-session-updater-batch-max-age)
|
||||
(s/def ::max-batch-size ::cfg/http-session-updater-batch-max-size)
|
||||
|
||||
(defmethod ig/pre-init-spec ::updater [_]
|
||||
(s/keys :req-un [::db/pool ::wrk/executor ::mtx/metrics ::session]
|
||||
:opt-un [::max-batch-age
|
||||
::max-batch-size]))
|
||||
|
||||
(defmethod ig/prep-key ::updater
|
||||
[_ cfg]
|
||||
(merge {:max-batch-age (dt/duration {:minutes 5})
|
||||
:max-batch-size 200}
|
||||
(d/without-nils cfg)))
|
||||
|
||||
(defmethod ig/init-key ::updater
|
||||
[_ {:keys [session metrics] :as cfg}]
|
||||
(l/info :action "initialize session updater"
|
||||
:max-batch-age (str (:max-batch-age cfg))
|
||||
:max-batch-size (str (:max-batch-size cfg)))
|
||||
(let [input (aa/batch (::events-ch session)
|
||||
{:max-batch-size (:max-batch-size cfg)
|
||||
:max-batch-age (inst-ms (:max-batch-age cfg))})]
|
||||
(a/go-loop []
|
||||
(when-let [[reason batch] (a/<! input)]
|
||||
(let [result (a/<! (update-sessions cfg batch))]
|
||||
(mtx/run! metrics {:id :session-update-total :inc 1})
|
||||
(cond
|
||||
(ex/exception? result)
|
||||
(l/error :task "updater"
|
||||
:hint "unexpected error on update sessions"
|
||||
:cause result)
|
||||
|
||||
(= :size reason)
|
||||
(l/debug :task "updater"
|
||||
:hint "update sessions"
|
||||
:reason (name reason)
|
||||
:count result))
|
||||
|
||||
(recur))))))
|
||||
|
||||
(defn- update-sessions
|
||||
[{:keys [pool executor]} ids]
|
||||
(aa/with-thread executor
|
||||
(db/exec-one! pool ["update http_session set updated_at=now() where id = ANY(?)"
|
||||
(into-array String ids)])
|
||||
(count ids)))
|
||||
|
||||
;; --- STATE INIT: SESSION GC
|
||||
|
||||
(declare sql:delete-expired)
|
||||
|
||||
(s/def ::max-age ::dt/duration)
|
||||
|
||||
(defmethod ig/pre-init-spec ::gc-task [_]
|
||||
(s/keys :req-un [::db/pool]
|
||||
:opt-un [::max-age]))
|
||||
|
||||
(defmethod ig/prep-key ::gc-task
|
||||
[_ cfg]
|
||||
(merge {:max-age (dt/duration {:days 15})}
|
||||
(d/without-nils cfg)))
|
||||
|
||||
(defmethod ig/init-key ::gc-task
|
||||
[_ {:keys [pool max-age] :as cfg}]
|
||||
(l/debug :hint "initializing session gc task" :max-age max-age)
|
||||
(fn [_]
|
||||
(db/with-atomic [conn pool]
|
||||
(let [interval (db/interval max-age)
|
||||
result (db/exec-one! conn [sql:delete-expired interval interval])
|
||||
result (:next.jdbc/update-count result)]
|
||||
(l/debug :task "gc"
|
||||
:hint "clean http sessions"
|
||||
:deleted result)
|
||||
result))))
|
||||
|
||||
(def ^:private
|
||||
sql:delete-expired
|
||||
"delete from http_session
|
||||
where updated_at < now() - ?::interval
|
||||
or (updated_at is null and
|
||||
created_at < now() - ?::interval)")
|
||||
|
||||
144
backend/src/app/http/websocket.clj
Normal file
@@ -0,0 +1,144 @@
|
||||
;; 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.websocket
|
||||
"A penpot notification service for file cooperative edition."
|
||||
(:require
|
||||
[app.common.exceptions :as ex]
|
||||
[app.common.logging :as l]
|
||||
[app.common.spec :as us]
|
||||
[app.db :as db]
|
||||
[app.metrics :as mtx]
|
||||
[app.util.websocket :as ws]
|
||||
[clojure.core.async :as a]
|
||||
[clojure.spec.alpha :as s]
|
||||
[integrant.core :as ig]
|
||||
[yetti.websocket :as yws]))
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;; WEBSOCKET HANDLER
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
|
||||
(declare send-presence!)
|
||||
|
||||
(defmulti handle-message
|
||||
(fn [_wsp message] (:type message)))
|
||||
|
||||
(defmethod handle-message :connect
|
||||
[wsp _]
|
||||
(let [{:keys [msgbus file-id team-id session-id ::ws/output-ch]} @wsp
|
||||
sub-ch (a/chan (a/dropping-buffer 32))]
|
||||
|
||||
(swap! wsp assoc :sub-ch sub-ch)
|
||||
|
||||
;; Start a subscription forwarding goroutine
|
||||
(a/go-loop []
|
||||
(when-let [val (a/<! sub-ch)]
|
||||
(when-not (= (:session-id val) session-id)
|
||||
;; If we receive a connect message of other user, we need
|
||||
;; to send an update presence to all participants.
|
||||
(when (= :connect (:type val))
|
||||
(a/<! (send-presence! @wsp :presence)))
|
||||
|
||||
;; Then, just forward the message
|
||||
(a/>! output-ch val))
|
||||
(recur)))
|
||||
|
||||
(a/go
|
||||
(a/<! (msgbus :sub {:topics [file-id team-id] :chan sub-ch}))
|
||||
(a/<! (send-presence! @wsp :connect)))))
|
||||
|
||||
(defmethod handle-message :disconnect
|
||||
[wsp _]
|
||||
(a/close! (:sub-ch @wsp))
|
||||
(send-presence! @wsp :disconnect))
|
||||
|
||||
(defmethod handle-message :keepalive
|
||||
[_ _]
|
||||
(a/go :nothing))
|
||||
|
||||
(defmethod handle-message :pointer-update
|
||||
[wsp message]
|
||||
(let [{:keys [profile-id file-id session-id msgbus]} @wsp]
|
||||
(msgbus :pub {:topic file-id
|
||||
:message (assoc message
|
||||
:profile-id profile-id
|
||||
:session-id session-id)})))
|
||||
|
||||
(defmethod handle-message :default
|
||||
[_ message]
|
||||
(a/go
|
||||
(l/log :level :warn
|
||||
:msg "received unexpected message"
|
||||
:message message)))
|
||||
|
||||
;; --- IMPL
|
||||
|
||||
(defn- send-presence!
|
||||
([ws] (send-presence! ws :presence))
|
||||
([{:keys [msgbus session-id profile-id file-id]} type]
|
||||
(msgbus :pub {:topic file-id
|
||||
:message {:type type
|
||||
:session-id session-id
|
||||
:profile-id profile-id}})))
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;; HTTP HANDLER
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
|
||||
(declare retrieve-file)
|
||||
|
||||
(s/def ::msgbus fn?)
|
||||
(s/def ::file-id ::us/uuid)
|
||||
(s/def ::session-id ::us/uuid)
|
||||
|
||||
(s/def ::handler-params
|
||||
(s/keys :req-un [::file-id ::session-id]))
|
||||
|
||||
(defmethod ig/pre-init-spec ::handler [_]
|
||||
(s/keys :req-un [::msgbus ::db/pool ::mtx/metrics]))
|
||||
|
||||
(defmethod ig/init-key ::handler
|
||||
[_ {:keys [pool] :as cfg}]
|
||||
(fn [{:keys [profile-id params] :as req} respond raise]
|
||||
(let [params (us/conform ::handler-params params)
|
||||
file (retrieve-file pool (:file-id params))
|
||||
cfg (-> (merge cfg params)
|
||||
(assoc :profile-id profile-id)
|
||||
(assoc :team-id (:team-id file)))]
|
||||
|
||||
(cond
|
||||
(not profile-id)
|
||||
(raise (ex/error :type :authentication
|
||||
:hint "Authentication required."))
|
||||
|
||||
(not file)
|
||||
(raise (ex/error :type :not-found
|
||||
:code :object-not-found))
|
||||
|
||||
|
||||
(not (yws/upgrade-request? req))
|
||||
(raise (ex/error :type :validation
|
||||
:code :websocket-request-expected
|
||||
:hint "this endpoint only accepts websocket connections"))
|
||||
|
||||
:else
|
||||
(->> (ws/handler handle-message cfg)
|
||||
(yws/upgrade req)
|
||||
(respond))))))
|
||||
|
||||
(def ^:private
|
||||
sql:retrieve-file
|
||||
"select f.id as id,
|
||||
p.team_id as team_id
|
||||
from file as f
|
||||
join project as p on (p.id = f.project_id)
|
||||
where f.id = ?")
|
||||
|
||||
(defn- retrieve-file
|
||||
[conn id]
|
||||
(db/exec-one! conn [sql:retrieve-file id]))
|
||||
|
||||
@@ -1,61 +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/.
|
||||
;;
|
||||
;; This Source Code Form is "Incompatible With Secondary Licenses", as
|
||||
;; defined by the Mozilla Public License, v. 2.0.
|
||||
;;
|
||||
;; Copyright (c) 2020 UXBOX Labs SL
|
||||
|
||||
(ns app.http.ws
|
||||
"Web Socket handlers"
|
||||
(:require
|
||||
[app.common.spec :as us]
|
||||
[app.db :as db]
|
||||
[app.http.session :refer [wrap-session]]
|
||||
[app.services.notifications :as nf]
|
||||
[clojure.spec.alpha :as s]
|
||||
[ring.middleware.cookies :refer [wrap-cookies]]
|
||||
[ring.middleware.keyword-params :refer [wrap-keyword-params]]
|
||||
[ring.middleware.params :refer [wrap-params]]))
|
||||
|
||||
(s/def ::file-id ::us/uuid)
|
||||
(s/def ::session-id ::us/uuid)
|
||||
|
||||
(s/def ::websocket-params
|
||||
(s/keys :req-un [::file-id ::session-id]))
|
||||
|
||||
(def sql:retrieve-file
|
||||
"select f.id as id,
|
||||
p.team_id as team_id
|
||||
from file as f
|
||||
join project as p on (p.id = f.project_id)
|
||||
where f.id = ?")
|
||||
|
||||
(defn retrieve-file
|
||||
[conn id]
|
||||
(db/exec-one! conn [sql:retrieve-file id]))
|
||||
|
||||
(defn websocket
|
||||
[{:keys [profile-id] :as req}]
|
||||
(let [params (us/conform ::websocket-params (:params req))
|
||||
file (retrieve-file db/pool (:file-id params))
|
||||
params (assoc params
|
||||
:profile-id profile-id
|
||||
:team-id (:team-id file))]
|
||||
(cond
|
||||
(not profile-id)
|
||||
{:error {:code 403 :message "Authentication required"}}
|
||||
|
||||
(not file)
|
||||
{:error {:code 404 :message "File does not exists"}}
|
||||
|
||||
:else
|
||||
(nf/websocket params))))
|
||||
|
||||
(def handler
|
||||
(-> websocket
|
||||
(wrap-session)
|
||||
(wrap-keyword-params)
|
||||
(wrap-cookies)
|
||||
(wrap-params)))
|
||||
346
backend/src/app/loggers/audit.clj
Normal file
@@ -0,0 +1,346 @@
|
||||
;; 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.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.time :as dt]
|
||||
[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]
|
||||
[promesa.core :as p]
|
||||
[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}]
|
||||
(let [invalid-keys #{:session-id
|
||||
:password
|
||||
:old-password
|
||||
:token}
|
||||
xform (comp
|
||||
(remove (fn [kv]
|
||||
(qualified-keyword? (first kv))))
|
||||
(remove (fn [kv]
|
||||
(contains? invalid-keys (first kv))))
|
||||
(remove (fn [[k v]]
|
||||
(and (= k :profile-id)
|
||||
(= v profile-id))))
|
||||
(filter (fn [[_ v]]
|
||||
(or (string? v)
|
||||
(keyword? v)
|
||||
(uuid? v)
|
||||
(boolean? v)
|
||||
(number? v)))))]
|
||||
|
||||
(update event :props #(into {} xform %))))
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;; 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 ::frontend-event
|
||||
(s/keys :req-un [::type ::name ::props ::timestamp ::profile-id]
|
||||
:opt-un [::context]))
|
||||
|
||||
(s/def ::frontend-events (s/every ::frontend-event))
|
||||
|
||||
(defmethod ig/init-key ::http-handler
|
||||
[_ {:keys [executor pool] :as cfg}]
|
||||
(if (or (db/read-only? pool) (not (contains? cf/flags :audit-log)))
|
||||
(do
|
||||
(l/warn :hint "audit log http handler disabled or db is read-only")
|
||||
(fn [_ respond _]
|
||||
(respond {:status 204 :body ""})))
|
||||
|
||||
|
||||
(letfn [(handler [{:keys [params profile-id] :as request}]
|
||||
(let [events (->> (:events params)
|
||||
(remove #(not= profile-id (:profile-id %)))
|
||||
(us/conform ::frontend-events))
|
||||
|
||||
ip-addr (parse-client-ip request)
|
||||
cfg (-> cfg
|
||||
(assoc :source "frontend")
|
||||
(assoc :events events)
|
||||
(assoc :ip-addr ip-addr))]
|
||||
(persist-http-events cfg)))
|
||||
|
||||
(handle-error [cause]
|
||||
(let [xdata (ex-data cause)]
|
||||
(if (= :spec-validation (:code xdata))
|
||||
(l/error ::l/raw (str "spec validation on persist-events:\n" (us/pretty-explain xdata)))
|
||||
(l/error :hint "error on persist-events" :cause cause))))]
|
||||
|
||||
(fn [request respond _]
|
||||
;; Fire and forget, log error in case of errro
|
||||
(-> (px/submit! executor #(handler request))
|
||||
(p/catch handle-error))
|
||||
|
||||
(respond {:status 204 :body ""})))))
|
||||
|
||||
(defn- persist-http-events
|
||||
[{:keys [pool events ip-addr source] :as cfg}]
|
||||
(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)))]))]
|
||||
(when (seq events)
|
||||
(->> (into [] prepare-xf events)
|
||||
(db/insert-multi! pool :audit-log columns)))))
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;; Collector
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
|
||||
;; Defines a service that collects the audit/activity log using
|
||||
;; internal database. Later this audit log can be transferred to
|
||||
;; an external storage and data cleared.
|
||||
|
||||
(declare persist-events)
|
||||
|
||||
(defmethod ig/pre-init-spec ::collector [_]
|
||||
(s/keys :req-un [::db/pool ::wrk/executor]))
|
||||
|
||||
(s/def ::ip-addr string?)
|
||||
(s/def ::backend-event
|
||||
(s/keys :req-un [::type ::name ::profile-id]
|
||||
:opt-un [::ip-addr ::props]))
|
||||
|
||||
(def ^:private backend-event-xform
|
||||
(comp
|
||||
(filter #(us/valid? ::backend-event %))
|
||||
(map clean-props)))
|
||||
|
||||
(defmethod ig/init-key ::collector
|
||||
[_ {:keys [pool] :as cfg}]
|
||||
(cond
|
||||
(not (contains? cf/flags :audit-log))
|
||||
(do
|
||||
(l/info :hint "audit log collection disabled")
|
||||
(constantly nil))
|
||||
|
||||
(db/read-only? pool)
|
||||
(do
|
||||
(l/warn :hint "audit log collection disabled, db is read-only")
|
||||
(constantly nil))
|
||||
|
||||
:else
|
||||
(let [input (a/chan 512 backend-event-xform)
|
||||
buffer (aa/batch input {:max-batch-size 100
|
||||
:max-batch-age (* 10 1000) ; 10s
|
||||
:init []})]
|
||||
(l/info :hint "audit log collector initialized")
|
||||
(a/go-loop []
|
||||
(when-let [[_type events] (a/<! buffer)]
|
||||
(let [res (a/<! (persist-events cfg events))]
|
||||
(when (ex/exception? res)
|
||||
(l/error :hint "error on persisting events" :cause res))
|
||||
(recur))))
|
||||
|
||||
(fn [& {:keys [cmd] :as params}]
|
||||
(case cmd
|
||||
:stop
|
||||
(a/close! input)
|
||||
|
||||
:submit
|
||||
(let [params (-> params
|
||||
(dissoc :cmd)
|
||||
(assoc :tracked-at (dt/now)))]
|
||||
(when-not (a/offer! input params)
|
||||
(l/warn :hint "activity channel is full"))))))))
|
||||
|
||||
(defn- persist-events
|
||||
[{:keys [pool executor] :as cfg} events]
|
||||
(letfn [(event->row [event]
|
||||
[(uuid/next)
|
||||
(:name event)
|
||||
(:type event)
|
||||
(:profile-id event)
|
||||
(:tracked-at event)
|
||||
(some-> (:ip-addr event) db/inet)
|
||||
(db/tjson (:props event))
|
||||
"backend"])]
|
||||
(aa/with-thread executor
|
||||
(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 (keep event->row) events)))))))
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;; Archive Task
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
|
||||
;; This is a task responsible to send the accumulated events to an
|
||||
;; external service for archival.
|
||||
|
||||
(declare archive-events)
|
||||
|
||||
(s/def ::uri ::us/string)
|
||||
(s/def ::tokens fn?)
|
||||
|
||||
(defmethod ig/pre-init-spec ::archive-task [_]
|
||||
(s/keys :req-un [::db/pool ::tokens]
|
||||
:opt-un [::uri]))
|
||||
|
||||
(defmethod ig/init-key ::archive-task
|
||||
[_ {: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 1000
|
||||
for update skip locked;")
|
||||
|
||||
(defn archive-events
|
||||
[{:keys [pool uri tokens] :as cfg}]
|
||||
(letfn [(decode-row [{:keys [props ip-addr context] :as row}]
|
||||
(cond-> row
|
||||
(db/pgobject? props)
|
||||
(assoc :props (db/decode-transit-pgobject 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"
|
||||
:iat (dt/now)
|
||||
:uid uuid/zero})
|
||||
body (t/encode {:events events})
|
||||
headers {"content-type" "application/transit+json"
|
||||
"origin" (cf/get :public-uri)
|
||||
"cookie" (u/map->query-string {:auth-token token})}
|
||||
params {:uri uri
|
||||
:timeout 6000
|
||||
:method :post
|
||||
:headers headers
|
||||
:body body}
|
||||
resp (http/send! params)]
|
||||
(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(?)"
|
||||
(->> (map :id rows)
|
||||
(into-array java.util.UUID)
|
||||
(db/create-array conn "uuid"))]))]
|
||||
|
||||
(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)]
|
||||
(when-not (empty? events)
|
||||
(l/debug :action "archive-events" :uri uri :events (count events))
|
||||
(when (send events)
|
||||
(mark-as-archived conn rows)
|
||||
:continue))))))
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;; GC Task
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
|
||||
(def sql:clean-archived
|
||||
"delete from audit_log
|
||||
where archived_at is not null
|
||||
and archived_at < now() - ?::interval")
|
||||
|
||||
(defn- clean-archived
|
||||
[{:keys [pool max-age]}]
|
||||
(let [interval (db/interval max-age)
|
||||
result (db/exec-one! pool [sql:clean-archived interval])
|
||||
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)))
|
||||
92
backend/src/app/loggers/database.clj
Normal file
@@ -0,0 +1,92 @@
|
||||
;; 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.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]))
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;; Error Listener
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
|
||||
(declare handle-event)
|
||||
|
||||
(defonce enabled (atom true))
|
||||
|
||||
(defn- persist-on-database!
|
||||
[{:keys [pool] :as cfg} {:keys [id] :as event}]
|
||||
(when-not (db/read-only? pool)
|
||||
(db/insert! pool :server-error-report {:id id :content (db/tjson event)})))
|
||||
|
||||
(defn- parse-event-data
|
||||
[event]
|
||||
(reduce-kv
|
||||
(fn [acc k v]
|
||||
(cond
|
||||
(= k :id) (assoc acc k (uuid/uuid v))
|
||||
(= k :profile-id) (assoc acc k (uuid/uuid v))
|
||||
(str/blank? v) acc
|
||||
:else (assoc acc k v)))
|
||||
{}
|
||||
event))
|
||||
|
||||
(defn parse-event
|
||||
[event]
|
||||
(-> (parse-event-data event)
|
||||
(assoc :tenant (cf/get :tenant))
|
||||
(assoc :host (cf/get :host))
|
||||
(assoc :public-uri (cf/get :public-uri))
|
||||
(assoc :version (:full cf/version))
|
||||
(update :id #(or % (uuid/next)))))
|
||||
|
||||
(defn handle-event
|
||||
[{:keys [executor] :as cfg} event]
|
||||
(aa/with-thread executor
|
||||
(try
|
||||
(let [event (parse-event event)
|
||||
uri (cf/get :public-uri)]
|
||||
|
||||
(l/debug :hint "registering error on database" :id (:id event)
|
||||
:uri (str uri "/dbg/error/" (:id event)))
|
||||
|
||||
(persist-on-database! cfg event))
|
||||
(catch Exception cause
|
||||
(l/warn :hint "unexpected exception on database error logger" :cause cause)))))
|
||||
|
||||
(defmethod ig/pre-init-spec ::reporter [_]
|
||||
(s/keys :req-un [::wrk/executor ::db/pool ::receiver]))
|
||||
|
||||
(defn error-event?
|
||||
[event]
|
||||
(= "error" (:logger/level event)))
|
||||
|
||||
(defmethod ig/init-key ::reporter
|
||||
[_ {:keys [receiver] :as cfg}]
|
||||
(l/info :msg "initializing database error persistence")
|
||||
(let [output (a/chan (a/sliding-buffer 5) (filter error-event?))]
|
||||
(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))
|
||||
99
backend/src/app/loggers/loki.clj
Normal file
@@ -0,0 +1,99 @@
|
||||
;; 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.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.worker :as wrk]
|
||||
[clojure.core.async :as a]
|
||||
[clojure.spec.alpha :as s]
|
||||
[integrant.core :as ig]))
|
||||
|
||||
(declare handle-event)
|
||||
|
||||
(s/def ::uri ::us/string)
|
||||
(s/def ::receiver fn?)
|
||||
|
||||
(defmethod ig/pre-init-spec ::reporter [_]
|
||||
(s/keys :req-un [::wrk/executor ::receiver]
|
||||
:opt-un [::uri]))
|
||||
|
||||
(defmethod ig/init-key ::reporter
|
||||
[_ {:keys [receiver uri] :as cfg}]
|
||||
(when uri
|
||||
(l/info :msg "initializing loki reporter" :uri uri)
|
||||
(let [input (a/chan (a/dropping-buffer 512))]
|
||||
(receiver :sub input)
|
||||
(a/go-loop []
|
||||
(let [msg (a/<! input)]
|
||||
(if (nil? msg)
|
||||
(l/info :msg "stoping error reporting loop")
|
||||
(do
|
||||
(a/<! (handle-event cfg msg))
|
||||
(recur)))))
|
||||
input)))
|
||||
|
||||
(defmethod ig/halt-key! ::reporter
|
||||
[_ output]
|
||||
(when output
|
||||
(a/close! output)))
|
||||
|
||||
(defn- prepare-payload
|
||||
[event]
|
||||
(let [labels {:host (cfg/get :host)
|
||||
:tenant (cfg/get :tenant)
|
||||
:version (:full cfg/version)
|
||||
:logger (:logger/name event)
|
||||
:level (:logger/level event)}]
|
||||
{:streams
|
||||
[{:stream labels
|
||||
:values [[(str (* (inst-ms (:created-at event)) 1000000))
|
||||
(str (:message event)
|
||||
(when-let [error (:trace event)]
|
||||
(str "\n" error)))]]}]}))
|
||||
|
||||
(defn- send-log
|
||||
[uri payload i]
|
||||
(try
|
||||
(let [response (http/send! {:uri uri
|
||||
:timeout 6000
|
||||
:method :post
|
||||
:headers {"content-type" "application/json"}
|
||||
:body (json/write payload)})]
|
||||
(cond
|
||||
(= (:status response) 204)
|
||||
true
|
||||
|
||||
(= (:status response) 400)
|
||||
(do
|
||||
(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)
|
||||
false)))
|
||||
|
||||
(defn- handle-event
|
||||
[{:keys [executor uri]} event]
|
||||
(aa/with-thread executor
|
||||
(let [payload (prepare-payload event)]
|
||||
(loop [i 1]
|
||||
(when (and (not (send-log uri payload i)) (< i 20))
|
||||
(Thread/sleep (* i 2000))
|
||||
(recur (inc i)))))))
|
||||
|
||||
80
backend/src/app/loggers/mattermost.clj
Normal file
@@ -0,0 +1,80 @@
|
||||
;; 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.mattermost
|
||||
"A mattermost integration for error reporting."
|
||||
(:require
|
||||
[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.worker :as wrk]
|
||||
[clojure.core.async :as a]
|
||||
[clojure.spec.alpha :as s]
|
||||
[integrant.core :as ig]))
|
||||
|
||||
(defonce enabled (atom true))
|
||||
|
||||
(defn- send-mattermost-notification!
|
||||
[cfg {:keys [host id public-uri] :as event}]
|
||||
(try
|
||||
(let [uri (:uri cfg)
|
||||
text (str "Exception on (host: " host ", url: " public-uri "/dbg/error/" id ")\n"
|
||||
(when-let [pid (:profile-id event)]
|
||||
(str "- profile-id: #uuid-" pid "\n")))
|
||||
rsp (http/send! {:uri uri
|
||||
:method :post
|
||||
:headers {"content-type" "application/json"}
|
||||
:body (json/write-str {:text text})})]
|
||||
(when (not= (:status rsp) 200)
|
||||
(l/error :hint "error on sending data to mattermost"
|
||||
:response (pr-str rsp))))
|
||||
|
||||
(catch Exception e
|
||||
(l/error :hint "unexpected exception on error reporter"
|
||||
:cause e))))
|
||||
|
||||
(defn handle-event
|
||||
[{:keys [executor] :as cfg} event]
|
||||
(aa/with-thread executor
|
||||
(try
|
||||
(let [event (ldb/parse-event event)]
|
||||
(when @enabled
|
||||
(send-mattermost-notification! cfg event)))
|
||||
(catch Exception e
|
||||
(l/warn :hint "unexpected exception on error reporter" :cause e)))))
|
||||
|
||||
|
||||
(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 (fn [event]
|
||||
(= (:logger/level event) "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)))
|
||||
170
backend/src/app/loggers/sentry.clj
Normal file
@@ -0,0 +1,170 @@
|
||||
;; 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)
|
||||
))
|
||||
]
|
||||
(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)))
|
||||
106
backend/src/app/loggers/zmq.clj
Normal file
@@ -0,0 +1,106 @@
|
||||
;; 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.zmq
|
||||
"A generic ZMQ listener."
|
||||
(:require
|
||||
[app.common.exceptions :as ex]
|
||||
[app.common.logging :as l]
|
||||
[app.common.spec :as us]
|
||||
[app.util.json :as json]
|
||||
[app.util.time :as dt]
|
||||
[clojure.core.async :as a]
|
||||
[clojure.spec.alpha :as s]
|
||||
[cuerdas.core :as str]
|
||||
[integrant.core :as ig])
|
||||
(:import
|
||||
org.zeromq.SocketType
|
||||
org.zeromq.ZMQ$Socket
|
||||
org.zeromq.ZContext))
|
||||
|
||||
(declare prepare)
|
||||
(declare start-rcv-loop)
|
||||
|
||||
(s/def ::endpoint ::us/string)
|
||||
|
||||
(defmethod ig/pre-init-spec ::receiver [_]
|
||||
(s/keys :opt-un [::endpoint]))
|
||||
|
||||
(defmethod ig/init-key ::receiver
|
||||
[_ {:keys [endpoint] :as cfg}]
|
||||
(l/info :msg "initializing ZMQ receiver" :bind endpoint)
|
||||
(let [buffer (a/chan 1)
|
||||
output (a/chan 1 (comp (filter map?)
|
||||
(keep prepare)))
|
||||
mult (a/mult output)]
|
||||
(when endpoint
|
||||
(a/thread (start-rcv-loop {:out buffer :endpoint endpoint})))
|
||||
(a/pipe buffer output)
|
||||
(with-meta
|
||||
(fn [cmd ch]
|
||||
(case cmd
|
||||
:sub (a/tap mult ch)
|
||||
:unsub (a/untap mult ch))
|
||||
ch)
|
||||
{::output output
|
||||
::buffer buffer
|
||||
::mult mult})))
|
||||
|
||||
(defmethod ig/halt-key! ::receiver
|
||||
[_ f]
|
||||
(a/close! (::buffer (meta f))))
|
||||
|
||||
(def ^:private json-mapper
|
||||
(json/mapper
|
||||
{:encode-key-fn str/camel
|
||||
:decode-key-fn (comp keyword str/kebab)}))
|
||||
|
||||
(defn- start-rcv-loop
|
||||
([] (start-rcv-loop nil))
|
||||
([{:keys [out endpoint] :or {endpoint "tcp://localhost:5556"}}]
|
||||
(let [out (or out (a/chan 1))
|
||||
zctx (ZContext.)
|
||||
socket (.. zctx (createSocket SocketType/SUB))]
|
||||
(.. socket (connect ^String endpoint))
|
||||
(.. socket (subscribe ""))
|
||||
(.. socket (setReceiveTimeOut 5000))
|
||||
(loop []
|
||||
(let [msg (.recv ^ZMQ$Socket socket)
|
||||
msg (ex/ignoring (json/read msg json-mapper))
|
||||
msg (if (nil? msg) :empty msg)]
|
||||
(if (a/>!! out msg)
|
||||
(recur)
|
||||
(do
|
||||
(.close ^java.lang.AutoCloseable socket)
|
||||
(.close ^java.lang.AutoCloseable zctx))))))))
|
||||
|
||||
(s/def ::logger-name string?)
|
||||
(s/def ::level string?)
|
||||
(s/def ::thread string?)
|
||||
(s/def ::time-millis integer?)
|
||||
(s/def ::message string?)
|
||||
(s/def ::context-map map?)
|
||||
(s/def ::throw map?)
|
||||
|
||||
(s/def ::log4j-event
|
||||
(s/keys :req-un [::logger-name ::level ::thread ::time-millis ::message]
|
||||
:opt-un [::context-map ::thrown]))
|
||||
|
||||
(defn- prepare
|
||||
[event]
|
||||
(if (s/valid? ::log4j-event event)
|
||||
(merge {:message (:message event)
|
||||
:created-at (dt/instant (:time-millis event))
|
||||
:logger/name (:logger-name event)
|
||||
:logger/level (str/lower (:level event))}
|
||||
|
||||
(when-let [thrown (:thrown event)]
|
||||
{:trace (:extended-stack-trace thrown)})
|
||||
|
||||
(:context-map event))
|
||||
(do
|
||||
(l/warn :hint "invalid event" :event event)
|
||||
nil)))
|
||||
@@ -2,41 +2,367 @@
|
||||
;; 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/.
|
||||
;;
|
||||
;; This Source Code Form is "Incompatible With Secondary Licenses", as
|
||||
;; defined by the Mozilla Public License, v. 2.0.
|
||||
;;
|
||||
;; Copyright (c) 2020 UXBOX Labs SL
|
||||
;; Copyright (c) UXBOX Labs SL
|
||||
|
||||
(ns app.main
|
||||
(:require
|
||||
[app.config :as cfg]
|
||||
[clojure.tools.logging :as log]
|
||||
[mount.core :as mount]))
|
||||
[app.common.logging :as l]
|
||||
[app.config :as cf]
|
||||
[app.util.time :as dt]
|
||||
[integrant.core :as ig])
|
||||
(:gen-class))
|
||||
|
||||
(defn- enable-asserts
|
||||
[_]
|
||||
(let [m (System/getProperty "app.enable-asserts")]
|
||||
(or (nil? m) (= "true" m))))
|
||||
(def system-config
|
||||
{:app.db/pool
|
||||
{:uri (cf/get :database-uri)
|
||||
:username (cf/get :database-username)
|
||||
:password (cf/get :database-password)
|
||||
:read-only (cf/get :database-readonly false)
|
||||
:metrics (ig/ref :app.metrics/metrics)
|
||||
:migrations (ig/ref :app.migrations/all)
|
||||
:name :main
|
||||
:min-size (cf/get :database-min-pool-size 0)
|
||||
:max-size (cf/get :database-max-pool-size 30)}
|
||||
|
||||
;; Set value for all new threads bindings.
|
||||
(alter-var-root #'*assert* enable-asserts)
|
||||
;; Default thread pool for IO operations
|
||||
[::default :app.worker/executor]
|
||||
{:parallelism (cf/get :default-executor-parallelism 60)
|
||||
:prefix :default}
|
||||
|
||||
;; Set value for current thread binding.
|
||||
(set! *assert* (enable-asserts nil))
|
||||
;; Constrained thread pool. Should only be used from high demand
|
||||
;; RPC methods.
|
||||
[::blocking :app.worker/executor]
|
||||
{:parallelism (cf/get :blocking-executor-parallelism 20)
|
||||
:prefix :blocking}
|
||||
|
||||
;; --- Entry point
|
||||
;; Dedicated thread pool for backround tasks execution.
|
||||
[::worker :app.worker/executor]
|
||||
{:parallelism (cf/get :worker-executor-parallelism 10)
|
||||
:prefix :worker}
|
||||
|
||||
(defn run
|
||||
[_params]
|
||||
(require 'app.srepl.server
|
||||
'app.services
|
||||
'app.migrations
|
||||
'app.worker
|
||||
'app.media
|
||||
'app.http)
|
||||
(mount/start)
|
||||
(log/infof "Welcome to penpot! Version: '%s'." (:full @cfg/version)))
|
||||
:app.worker/executors
|
||||
{:default (ig/ref [::default :app.worker/executor])
|
||||
:worker (ig/ref [::worker :app.worker/executor])
|
||||
:blocking (ig/ref [::blocking :app.worker/executor])}
|
||||
|
||||
:app.worker/executors-monitor
|
||||
{:metrics (ig/ref :app.metrics/metrics)
|
||||
:executors (ig/ref :app.worker/executors)}
|
||||
|
||||
:app.migrations/migrations
|
||||
{}
|
||||
|
||||
:app.metrics/metrics
|
||||
{}
|
||||
|
||||
:app.migrations/all
|
||||
{:main (ig/ref :app.migrations/migrations)}
|
||||
|
||||
:app.msgbus/msgbus
|
||||
{:backend (cf/get :msgbus-backend :redis)
|
||||
:redis-uri (cf/get :redis-uri)}
|
||||
|
||||
:app.tokens/tokens
|
||||
{:keys (ig/ref :app.setup/keys)}
|
||||
|
||||
:app.storage/gc-deleted-task
|
||||
{:pool (ig/ref :app.db/pool)
|
||||
:storage (ig/ref :app.storage/storage)
|
||||
:min-age (dt/duration {:hours 2})}
|
||||
|
||||
:app.storage/gc-touched-task
|
||||
{:pool (ig/ref :app.db/pool)}
|
||||
|
||||
:app.http.session/session
|
||||
{:pool (ig/ref :app.db/pool)
|
||||
:tokens (ig/ref :app.tokens/tokens)}
|
||||
|
||||
:app.http.session/gc-task
|
||||
{:pool (ig/ref :app.db/pool)
|
||||
:max-age (cf/get :http-session-idle-max-age)}
|
||||
|
||||
:app.http.session/updater
|
||||
{:pool (ig/ref :app.db/pool)
|
||||
:metrics (ig/ref :app.metrics/metrics)
|
||||
:executor (ig/ref [::worker :app.worker/executor])
|
||||
:session (ig/ref :app.http.session/session)
|
||||
:max-batch-age (cf/get :http-session-updater-batch-max-age)
|
||||
:max-batch-size (cf/get :http-session-updater-batch-max-size)}
|
||||
|
||||
:app.http.awsns/handler
|
||||
{:tokens (ig/ref :app.tokens/tokens)
|
||||
:pool (ig/ref :app.db/pool)}
|
||||
|
||||
:app.http/server
|
||||
{:port (cf/get :http-server-port)
|
||||
:host (cf/get :http-server-host)
|
||||
:router (ig/ref :app.http/router)
|
||||
:metrics (ig/ref :app.metrics/metrics)
|
||||
|
||||
:max-threads (cf/get :http-server-max-threads)
|
||||
:min-threads (cf/get :http-server-min-threads)}
|
||||
|
||||
:app.http/router
|
||||
{:assets (ig/ref :app.http.assets/handlers)
|
||||
:feedback (ig/ref :app.http.feedback/handler)
|
||||
:session (ig/ref :app.http.session/session)
|
||||
:sns-webhook (ig/ref :app.http.awsns/handler)
|
||||
:oauth (ig/ref :app.http.oauth/handler)
|
||||
:debug (ig/ref :app.http.debug/handlers)
|
||||
:ws (ig/ref :app.http.websocket/handler)
|
||||
:metrics (ig/ref :app.metrics/metrics)
|
||||
:public-uri (cf/get :public-uri)
|
||||
:storage (ig/ref :app.storage/storage)
|
||||
:tokens (ig/ref :app.tokens/tokens)
|
||||
:audit-http-handler (ig/ref :app.loggers.audit/http-handler)
|
||||
:rpc (ig/ref :app.rpc/rpc)}
|
||||
|
||||
:app.http.debug/handlers
|
||||
{:pool (ig/ref :app.db/pool)
|
||||
:executor (ig/ref [::default :app.worker/executor])}
|
||||
|
||||
:app.http.websocket/handler
|
||||
{:pool (ig/ref :app.db/pool)
|
||||
:metrics (ig/ref :app.metrics/metrics)
|
||||
:msgbus (ig/ref :app.msgbus/msgbus)}
|
||||
|
||||
:app.http.assets/handlers
|
||||
{:metrics (ig/ref :app.metrics/metrics)
|
||||
:assets-path (cf/get :assets-path)
|
||||
:storage (ig/ref :app.storage/storage)
|
||||
:executor (ig/ref [::default :app.worker/executor])
|
||||
:cache-max-age (dt/duration {:hours 24})
|
||||
:signature-max-age (dt/duration {:hours 24 :minutes 5})}
|
||||
|
||||
:app.http.feedback/handler
|
||||
{:pool (ig/ref :app.db/pool)
|
||||
:executor (ig/ref [::default :app.worker/executor])}
|
||||
|
||||
: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)
|
||||
:executor (ig/ref [::default :app.worker/executor])
|
||||
:public-uri (cf/get :public-uri)}
|
||||
|
||||
:app.rpc/rpc
|
||||
{:pool (ig/ref :app.db/pool)
|
||||
:session (ig/ref :app.http.session/session)
|
||||
:tokens (ig/ref :app.tokens/tokens)
|
||||
:metrics (ig/ref :app.metrics/metrics)
|
||||
:storage (ig/ref :app.storage/storage)
|
||||
:msgbus (ig/ref :app.msgbus/msgbus)
|
||||
:public-uri (cf/get :public-uri)
|
||||
:audit (ig/ref :app.loggers.audit/collector)
|
||||
:executors (ig/ref :app.worker/executors)}
|
||||
|
||||
:app.worker/worker
|
||||
{:executor (ig/ref [::worker :app.worker/executor])
|
||||
:tasks (ig/ref :app.worker/registry)
|
||||
:metrics (ig/ref :app.metrics/metrics)
|
||||
:pool (ig/ref :app.db/pool)}
|
||||
|
||||
:app.worker/scheduler
|
||||
{:executor (ig/ref [::worker :app.worker/executor])
|
||||
:tasks (ig/ref :app.worker/registry)
|
||||
:pool (ig/ref :app.db/pool)
|
||||
:schedule
|
||||
[{:cron #app/cron "0 0 0 * * ?" ;; daily
|
||||
:task :file-media-gc}
|
||||
|
||||
{:cron #app/cron "0 0 * * * ?" ;; hourly
|
||||
:task :file-xlog-gc}
|
||||
|
||||
{:cron #app/cron "0 0 0 * * ?" ;; daily
|
||||
:task :storage-deleted-gc}
|
||||
|
||||
{:cron #app/cron "0 0 0 * * ?" ;; daily
|
||||
:task :storage-touched-gc}
|
||||
|
||||
{:cron #app/cron "0 0 0 * * ?" ;; daily
|
||||
:task :session-gc}
|
||||
|
||||
{:cron #app/cron "0 0 0 * * ?" ;; daily
|
||||
:task :objects-gc}
|
||||
|
||||
{:cron #app/cron "0 0 0 * * ?" ;; daily
|
||||
:task :tasks-gc}
|
||||
|
||||
(when (cf/get :fdata-storage-backed)
|
||||
{:cron #app/cron "0 0 * * * ?" ;; hourly
|
||||
:task :file-offload})
|
||||
|
||||
(when (contains? cf/flags :audit-log-archive)
|
||||
{:cron #app/cron "0 */5 * * * ?" ;; every 5m
|
||||
:task :audit-log-archive})
|
||||
|
||||
(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 30 */3,23 * * ?"
|
||||
:task :telemetry})]}
|
||||
|
||||
:app.worker/registry
|
||||
{:metrics (ig/ref :app.metrics/metrics)
|
||||
:tasks
|
||||
{:sendmail (ig/ref :app.emails/sendmail-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)
|
||||
:storage-touched-gc (ig/ref :app.storage/gc-touched-task)
|
||||
:tasks-gc (ig/ref :app.tasks.tasks-gc/handler)
|
||||
:telemetry (ig/ref :app.tasks.telemetry/handler)
|
||||
:session-gc (ig/ref :app.http.session/gc-task)
|
||||
: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)
|
||||
:username (cf/get :smtp-username)
|
||||
:password (cf/get :smtp-password)
|
||||
:metrics (ig/ref :app.metrics/metrics)
|
||||
:default-reply-to (cf/get :smtp-default-reply-to)
|
||||
:default-from (cf/get :smtp-default-from)}
|
||||
|
||||
:app.tasks.tasks-gc/handler
|
||||
{:pool (ig/ref :app.db/pool)
|
||||
:max-age cf/deletion-delay}
|
||||
|
||||
:app.tasks.objects-gc/handler
|
||||
{:pool (ig/ref :app.db/pool)
|
||||
:storage (ig/ref :app.storage/storage)
|
||||
:max-age cf/deletion-delay}
|
||||
|
||||
:app.tasks.file-media-gc/handler
|
||||
{:pool (ig/ref :app.db/pool)
|
||||
:max-age cf/deletion-delay}
|
||||
|
||||
:app.tasks.file-xlog-gc/handler
|
||||
{:pool (ig/ref :app.db/pool)
|
||||
: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)
|
||||
:version (:full cf/version)
|
||||
:uri (cf/get :telemetry-uri)
|
||||
:sprops (ig/ref :app.setup/props)}
|
||||
|
||||
:app.srepl/server
|
||||
{:port (cf/get :srepl-port)
|
||||
:host (cf/get :srepl-host)}
|
||||
|
||||
:app.setup/props
|
||||
{: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 [::worker :app.worker/executor])}
|
||||
|
||||
:app.loggers.audit/collector
|
||||
{:pool (ig/ref :app.db/pool)
|
||||
:executor (ig/ref [::worker :app.worker/executor])}
|
||||
|
||||
:app.loggers.audit/archive-task
|
||||
{:uri (cf/get :audit-log-archive-uri)
|
||||
:tokens (ig/ref :app.tokens/tokens)
|
||||
:pool (ig/ref :app.db/pool)}
|
||||
|
||||
: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
|
||||
{:uri (cf/get :loggers-loki-uri)
|
||||
:receiver (ig/ref :app.loggers.zmq/receiver)
|
||||
:executor (ig/ref [::worker :app.worker/executor])}
|
||||
|
||||
:app.loggers.mattermost/reporter
|
||||
{:uri (cf/get :error-report-webhook)
|
||||
:receiver (ig/ref :app.loggers.zmq/receiver)
|
||||
:pool (ig/ref :app.db/pool)
|
||||
:executor (ig/ref [::worker :app.worker/executor])}
|
||||
|
||||
:app.loggers.database/reporter
|
||||
{:receiver (ig/ref :app.loggers.zmq/receiver)
|
||||
:pool (ig/ref :app.db/pool)
|
||||
:executor (ig/ref [::worker :app.worker/executor])}
|
||||
|
||||
:app.storage/storage
|
||||
{:pool (ig/ref :app.db/pool)
|
||||
: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])
|
||||
|
||||
;; 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)
|
||||
:endpoint (cf/get :storage-fdata-s3-endpoint)
|
||||
:prefix (cf/get :storage-fdata-s3-prefix)}
|
||||
|
||||
[::assets :app.storage.s3/backend]
|
||||
{:region (cf/get :storage-assets-s3-region)
|
||||
:endpoint (cf/get :storage-assets-s3-endpoint)
|
||||
:bucket (cf/get :storage-assets-s3-bucket)}
|
||||
|
||||
[::assets :app.storage.fs/backend]
|
||||
{:directory (cf/get :storage-assets-fs-directory)}
|
||||
|
||||
[::tmp :app.storage.fs/backend]
|
||||
{:directory "/tmp/penpot"}
|
||||
|
||||
[::assets :app.storage.db/backend]
|
||||
{:pool (ig/ref :app.db/pool)}})
|
||||
|
||||
(def system nil)
|
||||
|
||||
(defn start
|
||||
[]
|
||||
(ig/load-namespaces system-config)
|
||||
(alter-var-root #'system (fn [sys]
|
||||
(when sys (ig/halt! sys))
|
||||
(-> system-config
|
||||
(ig/prep)
|
||||
(ig/init))))
|
||||
(l/info :msg "welcome to penpot"
|
||||
:version (:full cf/version)))
|
||||
|
||||
(defn stop
|
||||
[]
|
||||
(alter-var-root #'system (fn [sys]
|
||||
(when sys (ig/halt! sys))
|
||||
nil)))
|
||||
|
||||
(defn -main
|
||||
[& _args]
|
||||
(run {}))
|
||||
(start))
|
||||
|
||||
@@ -2,42 +2,38 @@
|
||||
;; 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/.
|
||||
;;
|
||||
;; This Source Code Form is "Incompatible With Secondary Licenses", as
|
||||
;; defined by the Mozilla Public License, v. 2.0.
|
||||
;;
|
||||
;; Copyright (c) 2020 UXBOX Labs SL
|
||||
;; Copyright (c) UXBOX Labs SL
|
||||
|
||||
(ns app.media
|
||||
"Media postprocessing."
|
||||
"Media & Font postprocessing."
|
||||
(:require
|
||||
[app.common.data :as d]
|
||||
[app.common.exceptions :as ex]
|
||||
[app.common.media :as cm]
|
||||
[app.common.spec :as us]
|
||||
[app.config :as cfg]
|
||||
[app.util.http :as http]
|
||||
[clojure.core.async :as a]
|
||||
[app.config :as cf]
|
||||
[app.util.svg :as svg]
|
||||
[buddy.core.bytes :as bb]
|
||||
[buddy.core.codecs :as bc]
|
||||
[clojure.java.io :as io]
|
||||
[clojure.java.shell :as sh]
|
||||
[clojure.spec.alpha :as s]
|
||||
[datoteka.core :as fs]
|
||||
[mount.core :refer [defstate]])
|
||||
[cuerdas.core :as str]
|
||||
[datoteka.core :as fs])
|
||||
(:import
|
||||
java.io.ByteArrayInputStream
|
||||
java.util.concurrent.Semaphore
|
||||
java.io.OutputStream
|
||||
org.apache.commons.io.IOUtils
|
||||
org.im4java.core.ConvertCmd
|
||||
org.im4java.core.IMOperation
|
||||
org.im4java.core.Info))
|
||||
|
||||
(declare semaphore)
|
||||
|
||||
(defstate semaphore
|
||||
:start (Semaphore. (:image-process-max-threads cfg/config 1)))
|
||||
|
||||
|
||||
;; --- Generic specs
|
||||
(s/def ::image-content-type cm/valid-image-types)
|
||||
(s/def ::font-content-type cm/valid-font-types)
|
||||
|
||||
(s/def :internal.http.upload/filename ::us/string)
|
||||
(s/def :internal.http.upload/size ::us/integer)
|
||||
(s/def :internal.http.upload/content-type cm/valid-media-types)
|
||||
(s/def :internal.http.upload/content-type ::us/string)
|
||||
(s/def :internal.http.upload/tempfile any?)
|
||||
|
||||
(s/def ::upload
|
||||
@@ -46,8 +42,37 @@
|
||||
:internal.http.upload/tempfile
|
||||
:internal.http.upload/content-type]))
|
||||
|
||||
(defn validate-media-type
|
||||
([mtype] (validate-media-type mtype cm/valid-image-types))
|
||||
([mtype allowed]
|
||||
(when-not (contains? allowed mtype)
|
||||
(ex/raise :type :validation
|
||||
:code :media-type-not-allowed
|
||||
:hint "Seems like you are uploading an invalid media object"))))
|
||||
|
||||
(defmulti process :cmd)
|
||||
(defmulti process-error class)
|
||||
|
||||
(defmethod process :default
|
||||
[{:keys [cmd] :as params}]
|
||||
(ex/raise :type :internal
|
||||
:code :not-implemented
|
||||
:hint (str/fmt "No impl found for process cmd: %s" cmd)))
|
||||
|
||||
(defmethod process-error :default
|
||||
[error]
|
||||
(throw error))
|
||||
|
||||
(defn run
|
||||
[params]
|
||||
(try
|
||||
(process params)
|
||||
(catch Throwable e
|
||||
(process-error e))))
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;; --- Thumbnails Generation
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
|
||||
(s/def ::cmd keyword?)
|
||||
|
||||
@@ -75,7 +100,7 @@
|
||||
(let [{:keys [path mtype]} input
|
||||
format (or (cm/mtype->format mtype) format)
|
||||
ext (cm/format->extension format)
|
||||
tmp (fs/create-tempfile :suffix ext)]
|
||||
tmp (fs/create-tempfile :suffix ext)]
|
||||
|
||||
(doto (ConvertCmd.)
|
||||
(.run operation (into-array (map str [path tmp]))))
|
||||
@@ -85,10 +110,9 @@
|
||||
(assoc params
|
||||
:format format
|
||||
:mtype (cm/format->mtype format)
|
||||
:size (alength ^bytes thumbnail-data)
|
||||
:data (ByteArrayInputStream. thumbnail-data)))))
|
||||
|
||||
(defmulti process :cmd)
|
||||
|
||||
(defmethod process :generic-thumbnail
|
||||
[{:keys [quality width height] :as params}]
|
||||
(us/assert ::thumbnail-params params)
|
||||
@@ -96,7 +120,7 @@
|
||||
(.addImage)
|
||||
(.autoOrient)
|
||||
(.strip)
|
||||
(.thumbnail (int width) (int height) ">")
|
||||
(.thumbnail ^Integer (int width) ^Integer (int height) ">")
|
||||
(.quality (double quality))
|
||||
(.addImage))]
|
||||
(generic-process (assoc params :operation op))))
|
||||
@@ -108,85 +132,204 @@
|
||||
(.addImage)
|
||||
(.autoOrient)
|
||||
(.strip)
|
||||
(.thumbnail (int width) (int height) "^")
|
||||
(.thumbnail ^Integer (int width) ^Integer (int height) "^")
|
||||
(.gravity "center")
|
||||
(.extent (int width) (int height))
|
||||
(.quality (double quality))
|
||||
(.addImage))]
|
||||
(generic-process (assoc params :operation op))))
|
||||
|
||||
(defn get-basic-info-from-svg
|
||||
[{:keys [tag attrs] :as data}]
|
||||
(when (not= tag :svg)
|
||||
(ex/raise :type :validation
|
||||
:code :unable-to-parse-svg
|
||||
:hint "uploaded svg has invalid content"))
|
||||
(reduce (fn [default f]
|
||||
(if-let [res (f attrs)]
|
||||
(reduced res)
|
||||
default))
|
||||
{:width 100 :height 100}
|
||||
[(fn parse-width-and-height
|
||||
[{:keys [width height]}]
|
||||
(when (and (string? width)
|
||||
(string? height))
|
||||
(let [width (d/parse-double width)
|
||||
height (d/parse-double height)]
|
||||
(when (and width height)
|
||||
{:width (int width)
|
||||
:height (int height)}))))
|
||||
(fn parse-viewbox
|
||||
[{:keys [viewBox]}]
|
||||
(let [[x y width height] (->> (str/split viewBox #"\s+" 4)
|
||||
(map d/parse-double))]
|
||||
(when (and x y width height)
|
||||
{:width (int width)
|
||||
:height (int height)})))]))
|
||||
|
||||
(defmethod process :info
|
||||
[{:keys [input] :as params}]
|
||||
(us/assert ::input input)
|
||||
(let [{:keys [path mtype]} input]
|
||||
(if (= mtype "image/svg+xml")
|
||||
{:width 100
|
||||
:height 100
|
||||
:mtype mtype}
|
||||
(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
|
||||
:hint "uploaded svg does not provides dimensions"))
|
||||
(assoc info :mtype mtype))
|
||||
|
||||
(let [instance (Info. (str path))
|
||||
mtype' (.getProperty instance "Mime type")]
|
||||
(when (and (string? mtype)
|
||||
(not= mtype mtype'))
|
||||
(ex/raise :type :validation
|
||||
:code :media-type-mismatch
|
||||
:hint "Seems like you are uploading a file whose content does not match the extension."))
|
||||
{:width (.getImageWidth instance)
|
||||
:height (.getImageHeight instance)
|
||||
:mtype mtype'}))))
|
||||
:hint (str "Seems like you are uploading a file whose content does not match the extension."
|
||||
"Expected: " mtype ". Got: " mtype')))
|
||||
;; For an animated GIF, getImageWidth/Height returns the delta size of one frame (if no frame given
|
||||
;; it returns size of the last one), whereas getPageWidth/Height always return the full size of
|
||||
;; any frame.
|
||||
{:width (.getPageWidth instance)
|
||||
:height (.getPageHeight instance)
|
||||
:mtype mtype}))))
|
||||
|
||||
(defmethod process :default
|
||||
[{:keys [cmd] :as params}]
|
||||
(ex/raise :type :internal
|
||||
:code :not-implemented
|
||||
:hint (str "No impl found for process cmd:" cmd)))
|
||||
(defmethod process-error org.im4java.core.InfoException
|
||||
[error]
|
||||
(ex/raise :type :validation
|
||||
:code :invalid-image
|
||||
:hint "invalid image"
|
||||
:cause error))
|
||||
|
||||
(defn run
|
||||
[params]
|
||||
(try
|
||||
(.acquire semaphore)
|
||||
(let [res (a/<!! (a/thread
|
||||
(try
|
||||
(process params)
|
||||
(catch Throwable e
|
||||
e))))]
|
||||
(if (instance? Throwable res)
|
||||
(throw res)
|
||||
res))
|
||||
(finally
|
||||
(.release semaphore))))
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;; Fonts Generation
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
|
||||
(defmethod process :generate-fonts
|
||||
[{:keys [input] :as params}]
|
||||
(letfn [(ttf->otf [data]
|
||||
(let [input-file (fs/create-tempfile :prefix "penpot")
|
||||
output-file (fs/path (str input-file ".otf"))
|
||||
_ (with-open [out (io/output-stream input-file)]
|
||||
(IOUtils/writeChunked ^bytes data ^OutputStream out)
|
||||
(.flush ^OutputStream out))
|
||||
res (sh/sh "fontforge" "-lang=ff" "-c"
|
||||
(str/fmt "Open('%s'); Generate('%s')"
|
||||
(str input-file)
|
||||
(str output-file)))]
|
||||
(when (zero? (:exit res))
|
||||
(fs/slurp-bytes output-file))))
|
||||
|
||||
|
||||
;; --- Utility functions
|
||||
(otf->ttf [data]
|
||||
(let [input-file (fs/create-tempfile :prefix "penpot")
|
||||
output-file (fs/path (str input-file ".ttf"))
|
||||
_ (with-open [out (io/output-stream input-file)]
|
||||
(IOUtils/writeChunked ^bytes data ^OutputStream out)
|
||||
(.flush ^OutputStream out))
|
||||
res (sh/sh "fontforge" "-lang=ff" "-c"
|
||||
(str/fmt "Open('%s'); Generate('%s')"
|
||||
(str input-file)
|
||||
(str output-file)))]
|
||||
(when (zero? (:exit res))
|
||||
(fs/slurp-bytes output-file))))
|
||||
|
||||
(defn validate-media-type
|
||||
[media-type]
|
||||
(when-not (cm/valid-media-types media-type)
|
||||
(ex/raise :type :validation
|
||||
:code :media-type-not-allowed
|
||||
:hint "Seems like you are uploading an invalid media object")))
|
||||
(ttf-or-otf->woff [data]
|
||||
(let [input-file (fs/create-tempfile :prefix "penpot" :suffix "")
|
||||
output-file (fs/path (str input-file ".woff"))
|
||||
_ (with-open [out (io/output-stream input-file)]
|
||||
(IOUtils/writeChunked ^bytes data ^OutputStream out)
|
||||
(.flush ^OutputStream out))
|
||||
res (sh/sh "sfnt2woff" (str input-file))]
|
||||
(when (zero? (:exit res))
|
||||
(fs/slurp-bytes output-file))))
|
||||
|
||||
(ttf-or-otf->woff2 [data]
|
||||
(let [input-file (fs/create-tempfile :prefix "penpot" :suffix "")
|
||||
output-file (fs/path (str input-file ".woff2"))
|
||||
_ (with-open [out (io/output-stream input-file)]
|
||||
(IOUtils/writeChunked ^bytes data ^OutputStream out)
|
||||
(.flush ^OutputStream out))
|
||||
res (sh/sh "woff2_compress" (str input-file))]
|
||||
(when (zero? (:exit res))
|
||||
(fs/slurp-bytes output-file))))
|
||||
|
||||
(woff->sfnt [data]
|
||||
(let [input-file (fs/create-tempfile :prefix "penpot" :suffix "")
|
||||
_ (with-open [out (io/output-stream input-file)]
|
||||
(IOUtils/writeChunked ^bytes data ^OutputStream out)
|
||||
(.flush ^OutputStream out))
|
||||
res (sh/sh "woff2sfnt" (str input-file)
|
||||
:out-enc :bytes)]
|
||||
(when (zero? (:exit res))
|
||||
(:out res))))
|
||||
|
||||
;; Documented here:
|
||||
;; https://docs.microsoft.com/en-us/typography/opentype/spec/otff#table-directory
|
||||
(get-sfnt-type [data]
|
||||
(let [buff (bb/slice data 0 4)
|
||||
type (bc/bytes->hex buff)]
|
||||
(case type
|
||||
"4f54544f" :otf
|
||||
"00010000" :ttf
|
||||
(ex/raise :type :internal
|
||||
:code :unexpected-data
|
||||
:hint "unexpected font data"))))
|
||||
|
||||
(gen-if-nil [val factory]
|
||||
(if (nil? val)
|
||||
(factory)
|
||||
val))]
|
||||
|
||||
(let [current (into #{} (keys input))]
|
||||
(cond
|
||||
(contains? current "font/ttf")
|
||||
(let [data (get input "font/ttf")]
|
||||
(-> input
|
||||
(update "font/otf" gen-if-nil #(ttf->otf data))
|
||||
(update "font/woff" gen-if-nil #(ttf-or-otf->woff data))
|
||||
(assoc "font/woff2" (ttf-or-otf->woff2 data))))
|
||||
|
||||
(contains? current "font/otf")
|
||||
(let [data (get input "font/otf")]
|
||||
(-> input
|
||||
(update "font/woff" gen-if-nil #(ttf-or-otf->woff data))
|
||||
(assoc "font/ttf" (otf->ttf data))
|
||||
(assoc "font/woff2" (ttf-or-otf->woff2 data))))
|
||||
|
||||
(contains? current "font/woff")
|
||||
(let [data (get input "font/woff")
|
||||
sfnt (woff->sfnt data)]
|
||||
(when-not sfnt
|
||||
(ex/raise :type :validation
|
||||
:code :invalid-woff-file
|
||||
:hint "invalid woff file"))
|
||||
(let [stype (get-sfnt-type sfnt)]
|
||||
(cond-> input
|
||||
true
|
||||
(-> (assoc "font/woff" data)
|
||||
(assoc "font/woff2" (ttf-or-otf->woff2 sfnt)))
|
||||
|
||||
(= stype :otf)
|
||||
(-> (assoc "font/otf" sfnt)
|
||||
(assoc "font/ttf" (otf->ttf sfnt)))
|
||||
|
||||
(= stype :ttf)
|
||||
(-> (assoc "font/otf" (ttf->otf sfnt))
|
||||
(assoc "font/ttf" sfnt)))))))))
|
||||
|
||||
|
||||
;; TODO: rewrite using jetty http client instead of jvm
|
||||
;; builtin (because builtin http client uses a lot of memory for the
|
||||
;; same operation.
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;; Utility functions
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
|
||||
(defn download-media-object
|
||||
[url]
|
||||
(let [result (http/get! url {:as :byte-array})
|
||||
data (:body result)
|
||||
content-type (get (:headers result) "content-type")
|
||||
format (cm/mtype->format content-type)]
|
||||
(if (nil? format)
|
||||
(ex/raise :type :validation
|
||||
:code :media-type-not-allowed
|
||||
:hint "Seems like the url points to an invalid media object.")
|
||||
(let [tempfile (fs/create-tempfile)
|
||||
base-filename (first (fs/split-ext (fs/name tempfile)))
|
||||
filename (str base-filename (cm/format->extension format))]
|
||||
(with-open [ostream (io/output-stream tempfile)]
|
||||
(.write ostream data))
|
||||
{:filename filename
|
||||
:size (count data)
|
||||
:tempfile tempfile
|
||||
:content-type content-type}))))
|
||||
(defn configure-assets-storage
|
||||
"Given storage map, returns a storage configured with the appropriate
|
||||
backend for assets."
|
||||
([storage]
|
||||
(assoc storage :backend (cf/get :assets-storage-backend :assets-fs)))
|
||||
([storage conn]
|
||||
(-> storage
|
||||
(assoc :conn conn)
|
||||
(assoc :backend (cf/get :assets-storage-backend :assets-fs)))))
|
||||
|
||||
|
||||
@@ -1,37 +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/.
|
||||
;;
|
||||
;; This Source Code Form is "Incompatible With Secondary Licenses", as
|
||||
;; defined by the Mozilla Public License, v. 2.0.
|
||||
;;
|
||||
;; Copyright (c) 2020 UXBOX Labs SL
|
||||
|
||||
(ns app.media-storage
|
||||
"A media storage impl for app."
|
||||
(:require
|
||||
[app.config :refer [config]]
|
||||
[app.util.storage :as ust]
|
||||
[mount.core :refer [defstate]]))
|
||||
|
||||
;; --- State
|
||||
|
||||
(declare assets-storage)
|
||||
|
||||
(defstate assets-storage
|
||||
:start (ust/create {:base-path (:assets-directory config)
|
||||
:base-uri (:assets-uri config)}))
|
||||
|
||||
(declare media-storage)
|
||||
|
||||
(defstate media-storage
|
||||
:start (ust/create {:base-path (:media-directory config)
|
||||
:base-uri (:media-uri config)
|
||||
:xf (comp ust/random-path
|
||||
ust/slugify-filename)}))
|
||||
|
||||
;; --- Public Api
|
||||
|
||||
(defn resolve-asset
|
||||
[path]
|
||||
(str (ust/public-uri assets-storage path)))
|
||||
@@ -2,177 +2,272 @@
|
||||
;; 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/.
|
||||
;;
|
||||
;; This Source Code Form is "Incompatible With Secondary Licenses", as
|
||||
;; defined by the Mozilla Public License, v. 2.0.
|
||||
;;
|
||||
;; Copyright (c) 2020 UXBOX Labs SL
|
||||
;; Copyright (c) UXBOX Labs SL
|
||||
|
||||
(ns app.metrics
|
||||
(:refer-clojure :exclude [run!])
|
||||
(:require
|
||||
[app.common.logging :as l]
|
||||
[clojure.spec.alpha :as s]
|
||||
[integrant.core :as ig])
|
||||
(:import
|
||||
io.prometheus.client.CollectorRegistry
|
||||
io.prometheus.client.Counter
|
||||
io.prometheus.client.Counter$Child
|
||||
io.prometheus.client.Gauge
|
||||
io.prometheus.client.Gauge$Child
|
||||
io.prometheus.client.Summary
|
||||
io.prometheus.client.Summary$Child
|
||||
io.prometheus.client.Summary$Builder
|
||||
io.prometheus.client.Histogram
|
||||
io.prometheus.client.Histogram$Child
|
||||
io.prometheus.client.exporter.common.TextFormat
|
||||
io.prometheus.client.hotspot.DefaultExports
|
||||
io.prometheus.client.jetty.JettyStatisticsCollector
|
||||
org.eclipse.jetty.server.handler.StatisticsHandler
|
||||
java.io.StringWriter))
|
||||
|
||||
(defn- create-registry
|
||||
(set! *warn-on-reflection* true)
|
||||
|
||||
(declare create-registry)
|
||||
(declare create)
|
||||
(declare handler)
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;; METRICS SERVICE PROVIDER
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
|
||||
(def default-metrics
|
||||
{: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}
|
||||
|
||||
:rpc-mutation-timing
|
||||
{:name "rpc_mutation_timing"
|
||||
:help "RPC mutation method call timming."
|
||||
:labels ["name"]
|
||||
:type :histogram}
|
||||
|
||||
:rpc-query-timing
|
||||
{:name "rpc_query_timing"
|
||||
:help "RPC query method call timing."
|
||||
:labels ["name"]
|
||||
:type :histogram}
|
||||
|
||||
:websocket-active-connections
|
||||
{:name "websocket_active_connections"
|
||||
:help "Active websocket connections gauge"
|
||||
:type :gauge}
|
||||
|
||||
:websocket-messages-total
|
||||
{:name "websocket_message_total"
|
||||
:help "Counter of processed messages."
|
||||
:labels ["op"]
|
||||
:type :counter}
|
||||
|
||||
:websocket-session-timing
|
||||
{:name "websocket_session_timing"
|
||||
:help "Websocket session timing (seconds)."
|
||||
:type :summary}
|
||||
|
||||
:session-update-total
|
||||
{:name "http_session_update_total"
|
||||
:help "A counter of session update batch events."
|
||||
:type :counter}
|
||||
|
||||
:tasks-timing
|
||||
{:name "penpot_tasks_timing"
|
||||
:help "Background tasks timing (milliseconds)."
|
||||
:labels ["name"]
|
||||
:type :summary}
|
||||
|
||||
:rlimit-queued-submissions
|
||||
{:name "penpot_rlimit_queued_submissions"
|
||||
:help "Current number of queued submissions on RLIMIT."
|
||||
:labels ["name"]
|
||||
:type :gauge}
|
||||
|
||||
:rlimit-used-permits
|
||||
{:name "penpot_rlimit_used_permits"
|
||||
:help "Current number of used permits on RLIMIT."
|
||||
:labels ["name"]
|
||||
:type :gauge}
|
||||
|
||||
:rlimit-acquires-total
|
||||
{:name "penpot_rlimit_acquires_total"
|
||||
:help "Total number of acquire operations on RLIMIT."
|
||||
:labels ["name"]
|
||||
:type :counter}
|
||||
|
||||
:executors-active-threads
|
||||
{:name "penpot_executors_active_threads"
|
||||
:help "Current number of threads available in the executor service."
|
||||
:labels ["name"]
|
||||
:type :gauge}
|
||||
|
||||
:executors-completed-tasks
|
||||
{:name "penpot_executors_completed_tasks_total"
|
||||
:help "Aproximate number of completed tasks by the executor."
|
||||
:labels ["name"]
|
||||
:type :counter}
|
||||
|
||||
:executors-running-threads
|
||||
{:name "penpot_executors_running_threads"
|
||||
:help "Current number of threads with state RUNNING."
|
||||
:labels ["name"]
|
||||
:type :gauge}
|
||||
|
||||
:executors-queued-submissions
|
||||
{:name "penpot_executors_queued_submissions"
|
||||
:help "Current number of queued submissions."
|
||||
:labels ["name"]
|
||||
:type :gauge}})
|
||||
|
||||
(defmethod ig/init-key ::metrics
|
||||
[_ _]
|
||||
(l/info :action "initialize metrics")
|
||||
(let [registry (create-registry)
|
||||
definitions (reduce-kv (fn [res k v]
|
||||
(->> (assoc v :registry registry)
|
||||
(create)
|
||||
(assoc res k)))
|
||||
{}
|
||||
default-metrics)]
|
||||
{:handler (partial handler registry)
|
||||
:definitions definitions
|
||||
:registry registry}))
|
||||
|
||||
(s/def ::handler fn?)
|
||||
(s/def ::registry #(instance? CollectorRegistry %))
|
||||
(s/def ::metrics
|
||||
(s/keys :req-un [::registry ::handler]))
|
||||
|
||||
(defn- handler
|
||||
[registry _ respond _]
|
||||
(let [samples (.metricFamilySamples ^CollectorRegistry registry)
|
||||
writer (StringWriter.)]
|
||||
(TextFormat/write004 writer samples)
|
||||
(respond {:headers {"content-type" TextFormat/CONTENT_TYPE_004}
|
||||
:body (.toString writer)})))
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;; Implementation
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
|
||||
(def default-empty-labels (into-array String []))
|
||||
|
||||
(def default-quantiles
|
||||
[[0.5 0.01]
|
||||
[0.90 0.01]
|
||||
[0.99 0.001]])
|
||||
|
||||
(def default-histogram-buckets
|
||||
[1 5 10 25 50 75 100 250 500 750 1000 2500 5000 7500])
|
||||
|
||||
(defn run!
|
||||
[{:keys [definitions]} {:keys [id] :as params}]
|
||||
(when-let [mobj (get definitions id)]
|
||||
((::fn mobj) params)
|
||||
true))
|
||||
|
||||
(defn create-registry
|
||||
[]
|
||||
(let [registry (CollectorRegistry.)]
|
||||
(DefaultExports/register registry)
|
||||
registry))
|
||||
|
||||
(defonce registry (create-registry))
|
||||
(defonce cache (atom {}))
|
||||
|
||||
(defmacro with-measure
|
||||
[sym expr teardown]
|
||||
`(let [~sym (System/nanoTime)]
|
||||
(try
|
||||
~expr
|
||||
(finally
|
||||
(let [~sym (/ (- (System/nanoTime) ~sym) 1000000)]
|
||||
~teardown)))))
|
||||
(defn- is-array?
|
||||
[o]
|
||||
(let [oc (class o)]
|
||||
(and (.isArray ^Class oc)
|
||||
(= (.getComponentType oc) String))))
|
||||
|
||||
(defn make-counter
|
||||
[{:keys [id help] :as props}]
|
||||
(let [instance (doto (Counter/build)
|
||||
(.name id)
|
||||
(.help help))
|
||||
[{:keys [name help registry reg labels] :as props}]
|
||||
(let [registry (or registry reg)
|
||||
instance (.. (Counter/build)
|
||||
(name name)
|
||||
(help help))
|
||||
_ (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 val]
|
||||
(case cmd
|
||||
:wrap (fn
|
||||
([a]
|
||||
(.inc ^Counter instance)
|
||||
(val a))
|
||||
([a b]
|
||||
(.inc ^Counter instance)
|
||||
(val a b))
|
||||
([a b c]
|
||||
(.inc ^Counter instance)
|
||||
(val a b c)))
|
||||
|
||||
(throw (IllegalArgumentException. "invalid arguments")))))))
|
||||
|
||||
(defn counter
|
||||
[{:keys [id] :as props}]
|
||||
(or (get @cache id)
|
||||
(let [v (make-counter props)]
|
||||
(swap! cache assoc id v)
|
||||
v)))
|
||||
{::instance instance
|
||||
::fn (fn [{:keys [inc labels] :or {inc 1 labels default-empty-labels}}]
|
||||
(let [instance (.labels instance (if (is-array? labels) labels (into-array String labels)))]
|
||||
(.inc ^Counter$Child instance (double inc))))}))
|
||||
|
||||
(defn make-gauge
|
||||
[{:keys [id help] :as props}]
|
||||
(let [instance (doto (Gauge/build)
|
||||
(.name id)
|
||||
(.help help))
|
||||
[{:keys [name help registry reg labels] :as props}]
|
||||
(let [registry (or registry reg)
|
||||
instance (.. (Gauge/build)
|
||||
(name name)
|
||||
(help help))
|
||||
_ (when (seq labels)
|
||||
(.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))))))
|
||||
|
||||
(defn gauge
|
||||
[{:keys [id] :as props}]
|
||||
(or (get @cache id)
|
||||
(let [v (make-gauge props)]
|
||||
(swap! cache assoc id v)
|
||||
v)))
|
||||
{::instance instance
|
||||
::fn (fn [{:keys [inc dec labels val] :or {labels default-empty-labels}}]
|
||||
(let [instance (.labels ^Gauge instance (if (is-array? labels) labels (into-array String labels)))]
|
||||
(cond (number? inc) (.inc ^Gauge$Child instance (double inc))
|
||||
(number? dec) (.dec ^Gauge$Child instance (double dec))
|
||||
(number? val) (.set ^Gauge$Child instance (double val)))))}))
|
||||
|
||||
(defn make-summary
|
||||
[{:keys [id help] :as props}]
|
||||
(let [instance (doto (Summary/build)
|
||||
(.name id)
|
||||
[{:keys [name help registry reg labels max-age quantiles buckets]
|
||||
:or {max-age 3600 buckets 12 quantiles default-quantiles} :as props}]
|
||||
(let [registry (or registry reg)
|
||||
builder (doto (Summary/build)
|
||||
(.name name)
|
||||
(.help help))
|
||||
_ (when (seq quantiles)
|
||||
(.maxAgeSeconds ^Summary$Builder builder ^long max-age)
|
||||
(.ageBuckets ^Summary$Builder builder buckets))
|
||||
_ (doseq [[q e] quantiles]
|
||||
(.quantile ^Summary$Builder builder q e))
|
||||
_ (when (seq labels)
|
||||
(.labelNames ^Summary$Builder builder (into-array String labels)))
|
||||
instance (.register ^Summary$Builder builder registry)]
|
||||
|
||||
{::instance instance
|
||||
::fn (fn [{:keys [val labels] :or {labels default-empty-labels}}]
|
||||
(let [instance (.labels ^Summary instance (if (is-array? labels) labels (into-array String labels)))]
|
||||
(.observe ^Summary$Child instance val)))}))
|
||||
|
||||
(defn make-histogram
|
||||
[{:keys [name help registry reg labels buckets]
|
||||
:or {buckets default-histogram-buckets}}]
|
||||
(let [registry (or registry reg)
|
||||
instance (doto (Histogram/build)
|
||||
(.name name)
|
||||
(.help help)
|
||||
(.quantile 0.5 0.05)
|
||||
(.quantile 0.9 0.01)
|
||||
(.quantile 0.99 0.001))
|
||||
instance (.register instance registry)]
|
||||
(reify
|
||||
clojure.lang.IDeref
|
||||
(deref [_] instance)
|
||||
(.buckets (into-array Double/TYPE buckets)))
|
||||
_ (when (seq labels)
|
||||
(.labelNames instance (into-array String labels)))
|
||||
instance (.register instance registry)]
|
||||
|
||||
clojure.lang.IFn
|
||||
(invoke [_ val]
|
||||
(.observe ^Summary instance val))
|
||||
{::instance instance
|
||||
::fn (fn [{:keys [val labels] :or {labels default-empty-labels}}]
|
||||
(let [instance (.labels ^Histogram instance (if (is-array? labels) labels (into-array String labels)))]
|
||||
(.observe ^Histogram$Child instance val)))}))
|
||||
|
||||
(invoke [_ cmd val]
|
||||
(case cmd
|
||||
:wrap (fn
|
||||
([a]
|
||||
(with-measure $$
|
||||
(val a)
|
||||
(.observe ^Summary instance $$)))
|
||||
([a b]
|
||||
(with-measure $$
|
||||
(val a b)
|
||||
(.observe ^Summary instance $$)))
|
||||
([a b c]
|
||||
(with-measure $$
|
||||
(val a b c)
|
||||
(.observe ^Summary instance $$))))
|
||||
(defn create
|
||||
[{:keys [type] :as props}]
|
||||
(case type
|
||||
:counter (make-counter props)
|
||||
:gauge (make-gauge props)
|
||||
:summary (make-summary props)
|
||||
:histogram (make-histogram props)))
|
||||
|
||||
(throw (IllegalArgumentException. "invalid arguments")))))))
|
||||
|
||||
(defn summary
|
||||
[{:keys [id] :as props}]
|
||||
(or (get @cache id)
|
||||
(let [v (make-summary props)]
|
||||
(swap! cache assoc id v)
|
||||
v)))
|
||||
|
||||
(defn wrap-summary
|
||||
[f props]
|
||||
(let [sm (summary props)]
|
||||
(sm :wrap f)))
|
||||
|
||||
(defn wrap-counter
|
||||
[f props]
|
||||
(let [cnt (counter props)]
|
||||
(cnt :wrap f)))
|
||||
|
||||
(defn instrument-with-counter!
|
||||
[{:keys [var] :as props}]
|
||||
(let [cnt (counter props)
|
||||
vars (if (var? var) [var] var)]
|
||||
(doseq [var vars]
|
||||
(alter-var-root var (fn [root]
|
||||
(let [mdata (meta root)
|
||||
original (::counter-original mdata root)]
|
||||
(with-meta
|
||||
(cnt :wrap original)
|
||||
(assoc mdata ::counter-original original))))))))
|
||||
|
||||
(defn instrument-with-summary!
|
||||
[{:keys [var] :as props}]
|
||||
(let [sm (summary props)]
|
||||
(alter-var-root var (fn [root]
|
||||
(let [mdata (meta root)
|
||||
original (::summary-original mdata root)]
|
||||
(with-meta
|
||||
(sm :wrap original)
|
||||
(assoc mdata ::summary-original original)))))))
|
||||
|
||||
(defn dump
|
||||
[& _args]
|
||||
(let [samples (.metricFamilySamples ^CollectorRegistry registry)
|
||||
writer (StringWriter.)]
|
||||
(TextFormat/write004 writer samples)
|
||||
{:headers {"content-type" TextFormat/CONTENT_TYPE_004}
|
||||
:body (.toString writer)}))
|
||||
(defn instrument-jetty!
|
||||
[^CollectorRegistry registry ^StatisticsHandler handler]
|
||||
(doto (JettyStatisticsCollector. handler)
|
||||
(.register registry))
|
||||
nil)
|
||||
|
||||
|
||||
@@ -2,133 +2,213 @@
|
||||
;; 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/.
|
||||
;;
|
||||
;; This Source Code Form is "Incompatible With Secondary Licenses", as
|
||||
;; defined by the Mozilla Public License, v. 2.0.
|
||||
;;
|
||||
;; Copyright (c) 2020 UXBOX Labs SL
|
||||
;; Copyright (c) UXBOX Labs SL
|
||||
|
||||
(ns app.migrations
|
||||
(:require
|
||||
[app.db :as db]
|
||||
[app.migrations.migration-0023 :as mg0023]
|
||||
[app.util.migrations :as mg]
|
||||
[mount.core :as mount :refer [defstate]]))
|
||||
[integrant.core :as ig]))
|
||||
|
||||
(def +migrations+
|
||||
{:name "uxbox-main"
|
||||
:steps
|
||||
[{:name "0001-add-extensions"
|
||||
:fn (mg/resource "app/migrations/sql/0001-add-extensions.sql")}
|
||||
(def migrations
|
||||
[{:name "0001-add-extensions"
|
||||
:fn (mg/resource "app/migrations/sql/0001-add-extensions.sql")}
|
||||
|
||||
{:name "0002-add-profile-tables"
|
||||
:fn (mg/resource "app/migrations/sql/0002-add-profile-tables.sql")}
|
||||
{:name "0002-add-profile-tables"
|
||||
:fn (mg/resource "app/migrations/sql/0002-add-profile-tables.sql")}
|
||||
|
||||
{:name "0003-add-project-tables"
|
||||
:fn (mg/resource "app/migrations/sql/0003-add-project-tables.sql")}
|
||||
{:name "0003-add-project-tables"
|
||||
:fn (mg/resource "app/migrations/sql/0003-add-project-tables.sql")}
|
||||
|
||||
{:name "0004-add-tasks-tables"
|
||||
:fn (mg/resource "app/migrations/sql/0004-add-tasks-tables.sql")}
|
||||
{:name "0004-add-tasks-tables"
|
||||
:fn (mg/resource "app/migrations/sql/0004-add-tasks-tables.sql")}
|
||||
|
||||
{:name "0005-add-libraries-tables"
|
||||
:fn (mg/resource "app/migrations/sql/0005-add-libraries-tables.sql")}
|
||||
{:name "0005-add-libraries-tables"
|
||||
:fn (mg/resource "app/migrations/sql/0005-add-libraries-tables.sql")}
|
||||
|
||||
{:name "0006-add-presence-tables"
|
||||
:fn (mg/resource "app/migrations/sql/0006-add-presence-tables.sql")}
|
||||
{:name "0006-add-presence-tables"
|
||||
:fn (mg/resource "app/migrations/sql/0006-add-presence-tables.sql")}
|
||||
|
||||
{:name "0007-drop-version-field-from-page-table"
|
||||
:fn (mg/resource "app/migrations/sql/0007-drop-version-field-from-page-table.sql")}
|
||||
{:name "0007-drop-version-field-from-page-table"
|
||||
:fn (mg/resource "app/migrations/sql/0007-drop-version-field-from-page-table.sql")}
|
||||
|
||||
{:name "0008-add-generic-token-table"
|
||||
:fn (mg/resource "app/migrations/sql/0008-add-generic-token-table.sql")}
|
||||
{:name "0008-add-generic-token-table"
|
||||
:fn (mg/resource "app/migrations/sql/0008-add-generic-token-table.sql")}
|
||||
|
||||
{:name "0009-drop-profile-email-table"
|
||||
:fn (mg/resource "app/migrations/sql/0009-drop-profile-email-table.sql")}
|
||||
{:name "0009-drop-profile-email-table"
|
||||
:fn (mg/resource "app/migrations/sql/0009-drop-profile-email-table.sql")}
|
||||
|
||||
{:name "0010-add-http-session-table"
|
||||
:fn (mg/resource "app/migrations/sql/0010-add-http-session-table.sql")}
|
||||
{:name "0010-add-http-session-table"
|
||||
:fn (mg/resource "app/migrations/sql/0010-add-http-session-table.sql")}
|
||||
|
||||
{:name "0011-add-session-id-field-to-page-change-table"
|
||||
:fn (mg/resource "app/migrations/sql/0011-add-session-id-field-to-page-change-table.sql")}
|
||||
{:name "0011-add-session-id-field-to-page-change-table"
|
||||
:fn (mg/resource "app/migrations/sql/0011-add-session-id-field-to-page-change-table.sql")}
|
||||
|
||||
{:name "0012-make-libraries-linked-to-a-file"
|
||||
:fn (mg/resource "app/migrations/sql/0012-make-libraries-linked-to-a-file.sql")}
|
||||
{:name "0012-make-libraries-linked-to-a-file"
|
||||
:fn (mg/resource "app/migrations/sql/0012-make-libraries-linked-to-a-file.sql")}
|
||||
|
||||
{:name "0013-mark-files-shareable"
|
||||
:fn (mg/resource "app/migrations/sql/0013-mark-files-shareable.sql")}
|
||||
{:name "0013-mark-files-shareable"
|
||||
:fn (mg/resource "app/migrations/sql/0013-mark-files-shareable.sql")}
|
||||
|
||||
{:name "0014-refactor-media-storage.sql"
|
||||
:fn (mg/resource "app/migrations/sql/0014-refactor-media-storage.sql")}
|
||||
{:name "0014-refactor-media-storage.sql"
|
||||
:fn (mg/resource "app/migrations/sql/0014-refactor-media-storage.sql")}
|
||||
|
||||
{:name "0015-improve-tasks-tables"
|
||||
:fn (mg/resource "app/migrations/sql/0015-improve-tasks-tables.sql")}
|
||||
{:name "0015-improve-tasks-tables"
|
||||
:fn (mg/resource "app/migrations/sql/0015-improve-tasks-tables.sql")}
|
||||
|
||||
{:name "0016-truncate-and-alter-tokens-table"
|
||||
:fn (mg/resource "app/migrations/sql/0016-truncate-and-alter-tokens-table.sql")}
|
||||
{:name "0016-truncate-and-alter-tokens-table"
|
||||
:fn (mg/resource "app/migrations/sql/0016-truncate-and-alter-tokens-table.sql")}
|
||||
|
||||
{:name "0017-link-files-to-libraries"
|
||||
:fn (mg/resource "app/migrations/sql/0017-link-files-to-libraries.sql")}
|
||||
{:name "0017-link-files-to-libraries"
|
||||
:fn (mg/resource "app/migrations/sql/0017-link-files-to-libraries.sql")}
|
||||
|
||||
{:name "0018-add-file-trimming-triggers"
|
||||
:fn (mg/resource "app/migrations/sql/0018-add-file-trimming-triggers.sql")}
|
||||
{:name "0018-add-file-trimming-triggers"
|
||||
:fn (mg/resource "app/migrations/sql/0018-add-file-trimming-triggers.sql")}
|
||||
|
||||
{:name "0019-add-improved-scheduled-tasks"
|
||||
:fn (mg/resource "app/migrations/sql/0019-add-improved-scheduled-tasks.sql")}
|
||||
{:name "0019-add-improved-scheduled-tasks"
|
||||
:fn (mg/resource "app/migrations/sql/0019-add-improved-scheduled-tasks.sql")}
|
||||
|
||||
{:name "0020-minor-fixes-to-media-object"
|
||||
:fn (mg/resource "app/migrations/sql/0020-minor-fixes-to-media-object.sql")}
|
||||
{:name "0020-minor-fixes-to-media-object"
|
||||
:fn (mg/resource "app/migrations/sql/0020-minor-fixes-to-media-object.sql")}
|
||||
|
||||
{:name "0021-http-session-improvements"
|
||||
:fn (mg/resource "app/migrations/sql/0021-http-session-improvements.sql")}
|
||||
{:name "0021-http-session-improvements"
|
||||
:fn (mg/resource "app/migrations/sql/0021-http-session-improvements.sql")}
|
||||
|
||||
{:name "0022-page-file-refactor"
|
||||
:fn (mg/resource "app/migrations/sql/0022-page-file-refactor.sql")}
|
||||
{:name "0022-page-file-refactor"
|
||||
:fn (mg/resource "app/migrations/sql/0022-page-file-refactor.sql")}
|
||||
|
||||
{:name "0023-adapt-old-pages-and-files"
|
||||
:fn mg0023/migrate}
|
||||
{:name "0023-adapt-old-pages-and-files"
|
||||
:fn mg0023/migrate}
|
||||
|
||||
{:name "0024-mod-profile-table"
|
||||
:fn (mg/resource "app/migrations/sql/0024-mod-profile-table.sql")}
|
||||
{:name "0024-mod-profile-table"
|
||||
:fn (mg/resource "app/migrations/sql/0024-mod-profile-table.sql")}
|
||||
|
||||
{:name "0025-del-generic-tokens-table"
|
||||
:fn (mg/resource "app/migrations/sql/0025-del-generic-tokens-table.sql")}
|
||||
{:name "0025-del-generic-tokens-table"
|
||||
:fn (mg/resource "app/migrations/sql/0025-del-generic-tokens-table.sql")}
|
||||
|
||||
{:name "0026-mod-file-library-rel-table-synced-date"
|
||||
:fn (mg/resource "app/migrations/sql/0026-mod-file-library-rel-table-synced-date.sql")}
|
||||
{:name "0026-mod-file-library-rel-table-synced-date"
|
||||
:fn (mg/resource "app/migrations/sql/0026-mod-file-library-rel-table-synced-date.sql")}
|
||||
|
||||
{:name "0027-mod-file-table-ignore-sync"
|
||||
:fn (mg/resource "app/migrations/sql/0027-mod-file-table-ignore-sync.sql")}
|
||||
{:name "0027-mod-file-table-ignore-sync"
|
||||
:fn (mg/resource "app/migrations/sql/0027-mod-file-table-ignore-sync.sql")}
|
||||
|
||||
{:name "0028-add-team-project-profile-rel-table"
|
||||
:fn (mg/resource "app/migrations/sql/0028-add-team-project-profile-rel-table.sql")}
|
||||
{:name "0028-add-team-project-profile-rel-table"
|
||||
:fn (mg/resource "app/migrations/sql/0028-add-team-project-profile-rel-table.sql")}
|
||||
|
||||
{:name "0029-del-project-profile-rel-indexes"
|
||||
:fn (mg/resource "app/migrations/sql/0029-del-project-profile-rel-indexes.sql")}
|
||||
{:name "0029-del-project-profile-rel-indexes"
|
||||
:fn (mg/resource "app/migrations/sql/0029-del-project-profile-rel-indexes.sql")}
|
||||
|
||||
{:name "0030-mod-file-table-add-missing-index"
|
||||
:fn (mg/resource "app/migrations/sql/0030-mod-file-table-add-missing-index.sql")}
|
||||
{:name "0030-mod-file-table-add-missing-index"
|
||||
:fn (mg/resource "app/migrations/sql/0030-mod-file-table-add-missing-index.sql")}
|
||||
|
||||
{:name "0031-add-conversation-related-tables"
|
||||
:fn (mg/resource "app/migrations/sql/0031-add-conversation-related-tables.sql")}
|
||||
{:name "0031-add-conversation-related-tables"
|
||||
:fn (mg/resource "app/migrations/sql/0031-add-conversation-related-tables.sql")}
|
||||
|
||||
{:name "0032-del-unused-tables"
|
||||
:fn (mg/resource "app/migrations/sql/0032-del-unused-tables.sql")}
|
||||
{:name "0032-del-unused-tables"
|
||||
:fn (mg/resource "app/migrations/sql/0032-del-unused-tables.sql")}
|
||||
|
||||
{:name "0033-mod-comment-thread-table"
|
||||
:fn (mg/resource "app/migrations/sql/0033-mod-comment-thread-table.sql")}
|
||||
{:name "0033-mod-comment-thread-table"
|
||||
:fn (mg/resource "app/migrations/sql/0033-mod-comment-thread-table.sql")}
|
||||
|
||||
{:name "0034-mod-profile-table-add-props-field"
|
||||
:fn (mg/resource "app/migrations/sql/0034-mod-profile-table-add-props-field.sql")}
|
||||
]})
|
||||
{:name "0034-mod-profile-table-add-props-field"
|
||||
:fn (mg/resource "app/migrations/sql/0034-mod-profile-table-add-props-field.sql")}
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;; Entry point
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
{:name "0035-add-storage-tables"
|
||||
:fn (mg/resource "app/migrations/sql/0035-add-storage-tables.sql")}
|
||||
|
||||
(defn migrate
|
||||
[]
|
||||
(with-open [conn (db/open)]
|
||||
(mg/setup! conn)
|
||||
(mg/migrate! conn +migrations+)))
|
||||
{:name "0036-mod-storage-referenced-tables"
|
||||
:fn (mg/resource "app/migrations/sql/0036-mod-storage-referenced-tables.sql")}
|
||||
|
||||
(defstate migrations
|
||||
:start (migrate))
|
||||
{:name "0037-del-obsolete-triggers"
|
||||
:fn (mg/resource "app/migrations/sql/0037-del-obsolete-triggers.sql")}
|
||||
|
||||
{:name "0038-add-storage-on-delete-triggers"
|
||||
:fn (mg/resource "app/migrations/sql/0038-add-storage-on-delete-triggers.sql")}
|
||||
|
||||
{:name "0039-fix-some-on-delete-triggers"
|
||||
:fn (mg/resource "app/migrations/sql/0039-fix-some-on-delete-triggers.sql")}
|
||||
|
||||
{:name "0040-add-error-report-tables"
|
||||
:fn (mg/resource "app/migrations/sql/0040-add-error-report-tables.sql")}
|
||||
|
||||
{:name "0041-mod-pg-storage-options"
|
||||
:fn (mg/resource "app/migrations/sql/0041-mod-pg-storage-options.sql")}
|
||||
|
||||
{:name "0042-add-server-prop-table"
|
||||
:fn (mg/resource "app/migrations/sql/0042-add-server-prop-table.sql")}
|
||||
|
||||
{:name "0043-drop-old-tables-and-fields"
|
||||
:fn (mg/resource "app/migrations/sql/0043-drop-old-tables-and-fields.sql")}
|
||||
|
||||
{:name "0044-add-storage-refcount"
|
||||
:fn (mg/resource "app/migrations/sql/0044-add-storage-refcount.sql")}
|
||||
|
||||
{:name "0045-add-index-to-file-change-table"
|
||||
:fn (mg/resource "app/migrations/sql/0045-add-index-to-file-change-table.sql")}
|
||||
|
||||
{:name "0046-add-profile-complaint-table"
|
||||
:fn (mg/resource "app/migrations/sql/0046-add-profile-complaint-table.sql")}
|
||||
|
||||
{:name "0047-mod-file-change-table"
|
||||
:fn (mg/resource "app/migrations/sql/0047-mod-file-change-table.sql")}
|
||||
|
||||
{:name "0048-mod-storage-tables"
|
||||
:fn (mg/resource "app/migrations/sql/0048-mod-storage-tables.sql")}
|
||||
|
||||
{:name "0049-mod-http-session-table"
|
||||
:fn (mg/resource "app/migrations/sql/0049-mod-http-session-table.sql")}
|
||||
|
||||
{:name "0050-mod-server-prop-table"
|
||||
:fn (mg/resource "app/migrations/sql/0050-mod-server-prop-table.sql")}
|
||||
|
||||
{:name "0051-mod-file-library-rel-table"
|
||||
:fn (mg/resource "app/migrations/sql/0051-mod-file-library-rel-table.sql")}
|
||||
|
||||
{:name "0052-del-legacy-user-and-team"
|
||||
:fn (mg/resource "app/migrations/sql/0052-del-legacy-user-and-team.sql")}
|
||||
|
||||
{:name "0053-add-team-font-variant-table"
|
||||
:fn (mg/resource "app/migrations/sql/0053-add-team-font-variant-table.sql")}
|
||||
|
||||
{:name "0054-add-audit-log-table"
|
||||
:fn (mg/resource "app/migrations/sql/0054-add-audit-log-table.sql")}
|
||||
|
||||
{: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")}
|
||||
|
||||
{:name "0065-add-trivial-spelling-fixes"
|
||||
:fn (mg/resource "app/migrations/sql/0065-add-trivial-spelling-fixes.sql")}
|
||||
|
||||
{:name "0066-add-frame-thumbnail-table"
|
||||
:fn (mg/resource "app/migrations/sql/0066-add-frame-thumbnail-table.sql")}
|
||||
])
|
||||
|
||||
|
||||
(defmethod ig/init-key ::migrations [_ _] migrations)
|
||||
|
||||
@@ -2,10 +2,7 @@
|
||||
;; 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/.
|
||||
;;
|
||||
;; This Source Code Form is "Incompatible With Secondary Licenses", as
|
||||
;; defined by the Mozilla Public License, v. 2.0.
|
||||
;;
|
||||
;; Copyright (c) 2020 UXBOX Labs SL
|
||||
;; Copyright (c) UXBOX Labs SL
|
||||
|
||||
(ns app.migrations.migration-0023
|
||||
(:require
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
|
||||
CREATE EXTENSION IF NOT EXISTS "pgcrypto";
|
||||
|
||||
CREATE FUNCTION update_modified_at()
|
||||
RETURNS TRIGGER AS $updt$
|
||||
|
||||
@@ -106,8 +106,6 @@ CREATE TRIGGER file_image__on_delete__tgr
|
||||
AFTER DELETE ON file_image
|
||||
FOR EACH ROW EXECUTE PROCEDURE handle_delete();
|
||||
|
||||
|
||||
|
||||
CREATE TABLE page (
|
||||
id uuid PRIMARY KEY DEFAULT uuid_generate_v4(),
|
||||
file_id uuid NOT NULL REFERENCES file(id) ON DELETE CASCADE,
|
||||
|
||||