mirror of
https://github.com/docusealco/docuseal
synced 2026-11-04 06:42:32 -05:00
Compare commits
1180
Commits
2.0.3
..
44dcc7f312
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
44dcc7f312 | ||
|
|
218c22da21 | ||
|
|
a8433e9bd3 | ||
|
|
800101f747 | ||
|
|
f637ee907c | ||
|
|
6a9b123cdc | ||
|
|
0a2f8dfab3 | ||
|
|
3dc00ed7fc | ||
|
|
af160b3f3f | ||
|
|
1a9e4026d9 | ||
|
|
5ef32bcf9e | ||
|
|
2d6f4c521e | ||
|
|
439f95d87e | ||
|
|
ca7b9671f2 | ||
|
|
20ec614d3a | ||
|
|
887ebbbcf6 | ||
|
|
4225295295 | ||
|
|
89367bc905 | ||
|
|
4bacaa26e1 | ||
|
|
0f9fcf3fdc | ||
|
|
437f61dc26 | ||
|
|
e55542a000 | ||
|
|
e004ac2d40 | ||
|
|
92bfedd01d | ||
|
|
e7fd1e4006 | ||
|
|
56bba60d3f | ||
|
|
6b6a917f60 | ||
|
|
df7b3f9dfe | ||
|
|
1b21b0c65b | ||
|
|
258133d65c | ||
|
|
8f33c8d8b6 | ||
|
|
e11f4bddbe | ||
|
|
d071e8c429 | ||
|
|
79aa034183 | ||
|
|
f5dad673de | ||
|
|
7e8375aee8 | ||
|
|
1999611b26 | ||
|
|
188c3a8c84 | ||
|
|
d35538e2ba | ||
|
|
a86f896694 | ||
|
|
35eeeba0c3 | ||
|
|
e3e5c237d4 | ||
|
|
9e89cb92a9 | ||
|
|
827850646d | ||
|
|
eafae4cda2 | ||
|
|
e012b7f5ac | ||
|
|
da9e7c6366 | ||
|
|
20cc5d1e2e | ||
|
|
ae2cedbd70 | ||
|
|
1f46f74400 | ||
|
|
930445258d | ||
|
|
be533e492e | ||
|
|
66758d45bc | ||
|
|
6f31bd7af8 | ||
|
|
f4d2fa4c28 | ||
|
|
5d3369d556 | ||
|
|
5ea55c733f | ||
|
|
547bc9aa94 | ||
|
|
53ca45a67a | ||
|
|
ef28b80589 | ||
|
|
05fa75f9a2 | ||
|
|
87b57279a4 | ||
|
|
f09732df90 | ||
|
|
a588c2f4a4 | ||
|
|
960d36f628 | ||
|
|
456dbae1cd | ||
|
|
0fc1c0d1ed | ||
|
|
714324ea99 | ||
|
|
5a29ed22db | ||
|
|
061580594c | ||
|
|
8723ce7f7a | ||
|
|
522df7f476 | ||
|
|
026637c5ab | ||
|
|
fde993a579 | ||
|
|
a960ea268a | ||
|
|
d59776e8f5 | ||
|
|
27e5c2c81e | ||
|
|
0609a70544 | ||
|
|
3ed15e383b | ||
|
|
d29be7c2cf | ||
|
|
79f72c92b3 | ||
|
|
7e6331d1ad | ||
|
|
0986dfba73 | ||
|
|
7cf0b5aaf7 | ||
|
|
ae5f87f8fb | ||
|
|
8259ebdb6c | ||
|
|
a4ca0abba1 | ||
|
|
ca009252ac | ||
|
|
9678cd1286 | ||
|
|
8cb0a7a020 | ||
|
|
d3cc2e1ca8 | ||
|
|
08ac60c7e6 | ||
|
|
49afb2e87c | ||
|
|
be621339c3 | ||
|
|
5f5b49a1ba | ||
|
|
c0dd4b85ed | ||
|
|
95f7cd5220 | ||
|
|
f0753de99e | ||
|
|
38ecda82fa | ||
|
|
073859a4b7 | ||
|
|
44972fcc39 | ||
|
|
1232dfb248 | ||
|
|
5de334522c | ||
|
|
7a6b4acea6 | ||
|
|
198a599a2b | ||
|
|
d36f40cafd | ||
|
|
9abdb68bc5 | ||
|
|
359f695eb5 | ||
|
|
2b9e5a86b6 | ||
|
|
c07d76e2a4 | ||
|
|
f5b29642ee | ||
|
|
3f6cdcd15b | ||
|
|
bde8097288 | ||
|
|
617d75a4ed | ||
|
|
aa1874cdb4 | ||
|
|
22b2e7475a | ||
|
|
ad3367dae0 | ||
|
|
47c090e1f1 | ||
|
|
678997fb36 | ||
|
|
24f195c7e5 | ||
|
|
5741bb4db2 | ||
|
|
1d169668f0 | ||
|
|
ef3d5bec7e | ||
|
|
d578ca3cc9 | ||
|
|
5432978c0d | ||
|
|
de1af49348 | ||
|
|
2f0bcb4a80 | ||
|
|
11f27c8cf0 | ||
|
|
6e2430e5a2 | ||
|
|
85fbae1813 | ||
|
|
9941122d90 | ||
|
|
71be6db358 | ||
|
|
994c1b3155 | ||
|
|
92b297a620 | ||
|
|
bd03498b0f | ||
|
|
660b24710e | ||
|
|
1416cf72e8 | ||
|
|
ae7b0ce608 | ||
|
|
a3828ec390 | ||
|
|
b8c1691bd7 | ||
|
|
4b00c896e4 | ||
|
|
781da32a07 | ||
|
|
307036eae6 | ||
|
|
9944a9b1a5 | ||
|
|
11856a17ac | ||
|
|
7b4c74d711 | ||
|
|
e0b0d4e803 | ||
|
|
ec04a95e36 | ||
|
|
887d80f6d7 | ||
|
|
9ad281d457 | ||
|
|
e316d2006f | ||
|
|
f9669b0e16 | ||
|
|
785eb789ad | ||
|
|
436d947f6a | ||
|
|
033ce3a169 | ||
|
|
c216e43d24 | ||
|
|
d2373c1578 | ||
|
|
474603e1c9 | ||
|
|
1fc1444b8f | ||
|
|
47361da26f | ||
|
|
11dea042ad | ||
|
|
8756931638 | ||
|
|
180b201dcf | ||
|
|
a1c30c2093 | ||
|
|
66bf6af7ba | ||
|
|
1a65e7bcff | ||
|
|
59b5f03948 | ||
|
|
4626518c83 | ||
|
|
c18e263f34 | ||
|
|
c3dae1bfe4 | ||
|
|
b44152fd52 | ||
|
|
2889a7c937 | ||
|
|
91e65e4a19 | ||
|
|
2a1a2cbd82 | ||
|
|
99b3247cf7 | ||
|
|
fa91c6d6be | ||
|
|
27a0d10525 | ||
|
|
d25f0812ca | ||
|
|
9e2aa6f8c8 | ||
|
|
11c76bf554 | ||
|
|
e5733d88c0 | ||
|
|
6d38c9f375 | ||
|
|
91ec29ad2c | ||
|
|
d374c7fa10 | ||
|
|
ca0daf6d0b | ||
|
|
076c3d805a | ||
|
|
1f149ac67a | ||
|
|
616f7b4fd8 | ||
|
|
6f4527038b | ||
|
|
8ed2883504 | ||
|
|
651b84a460 | ||
|
|
73c4f158ff | ||
|
|
6f1cc34433 | ||
|
|
a77029ffcb | ||
|
|
0480572ef5 | ||
|
|
807463c465 | ||
|
|
0f61053976 | ||
|
|
b8cf525034 | ||
|
|
fef203daed | ||
|
|
41b38cc594 | ||
|
|
4e4354af99 | ||
|
|
113b61588b | ||
|
|
c1aac6b763 | ||
|
|
f85fd3cff7 | ||
|
|
004a22c1c8 | ||
|
|
69343b1bdd | ||
|
|
81a306928c | ||
|
|
2bf3d42ed6 | ||
|
|
eec08266c9 | ||
|
|
cbbc75b485 | ||
|
|
3d93bbe5aa | ||
|
|
5308e3e15b | ||
|
|
22c146d564 | ||
|
|
c8d3af9841 | ||
|
|
ee1d45d461 | ||
|
|
27d075ed63 | ||
|
|
df63acf0c8 | ||
|
|
185827c951 | ||
|
|
e361cbabf4 | ||
|
|
e867bf5c48 | ||
|
|
726d0ea4f3 | ||
|
|
90be34b243 | ||
|
|
e87c06d811 | ||
|
|
54454d1dfc | ||
|
|
726174deff | ||
|
|
52eb8ac863 | ||
|
|
97d91d8f27 | ||
|
|
dd358a8401 | ||
|
|
8e254b7d3c | ||
|
|
b9018fa442 | ||
|
|
09310f3690 | ||
|
|
2dd38260ba | ||
|
|
ffb3247cdc | ||
|
|
c59c470321 | ||
|
|
f35510ffd6 | ||
|
|
eeec852b3b | ||
|
|
485b7f2c8f | ||
|
|
b6498ec049 | ||
|
|
dc1de72e18 | ||
|
|
7dddef4c94 | ||
|
|
a2d8b85549 | ||
|
|
4d68783b82 | ||
|
|
b05c3b2030 | ||
|
|
784ae9aa4b | ||
|
|
03b5956cbc | ||
|
|
2876c9a18e | ||
|
|
c4b7be3925 | ||
|
|
e2ffbdac1d | ||
|
|
0d197bb2eb | ||
|
|
55089bee7f | ||
|
|
b96fe74db1 | ||
|
|
2fc1ba3037 | ||
|
|
c67d37207e | ||
|
|
dba50cd1bf | ||
|
|
edf2c0d9ed | ||
|
|
35f7c36f28 | ||
|
|
3a24ecac63 | ||
|
|
50ec7682b5 | ||
|
|
3667993caf | ||
|
|
68e8cd1188 | ||
|
|
3eebf6d2cf | ||
|
|
34d0e00910 | ||
|
|
b8ab7190d2 | ||
|
|
59fe8a7917 | ||
|
|
d56070827b | ||
|
|
74dbd4bed3 | ||
|
|
807e82b21e | ||
|
|
66862e93c8 | ||
|
|
270461ad3b | ||
|
|
3edd1ab32d | ||
|
|
e77998e53c | ||
|
|
1421bc2bce | ||
|
|
cbbbfdda80 | ||
|
|
2d510081be | ||
|
|
704893ee1d | ||
|
|
88612913a6 | ||
|
|
ce6823ed38 | ||
|
|
9074d23b3c | ||
|
|
76d0e76fd5 | ||
|
|
5ec8e4ccc7 | ||
|
|
b8c29324b8 | ||
|
|
972f38242e | ||
|
|
7ce6c29f4d | ||
|
|
2c6ddd7f90 | ||
|
|
ae50b2e323 | ||
|
|
152cbc11a6 | ||
|
|
3a406de655 | ||
|
|
2e31cda08c | ||
|
|
094b7f47bd | ||
|
|
99ac349ecc | ||
|
|
8830e03cf3 | ||
|
|
706f3d6d65 | ||
|
|
12b49f5fa7 | ||
|
|
85327760b9 | ||
|
|
c94ed9c3d4 | ||
|
|
bc50735d2a | ||
|
|
d05e125fa9 | ||
|
|
48d595ca81 | ||
|
|
a156de5e75 | ||
|
|
a7003bf4d0 | ||
|
|
5fe75c84ff | ||
|
|
4549a04fc5 | ||
|
|
0ea6b912a3 | ||
|
|
9b86fa3d40 | ||
|
|
374f66c59f | ||
|
|
da43d881f1 | ||
|
|
9cbfa3a0ba | ||
|
|
77a925f33b | ||
|
|
3f7d7beedd | ||
|
|
40d91df732 | ||
|
|
c8e783a210 | ||
|
|
61fc24d35d | ||
|
|
73734b0de7 | ||
|
|
09bd831d54 | ||
|
|
2c9f1b999e | ||
|
|
6a3a185310 | ||
|
|
7d381b7aee | ||
|
|
bba5626dea | ||
|
|
bdf5e58929 | ||
|
|
035168230e | ||
|
|
28168f082d | ||
|
|
a29e09ed2f | ||
|
|
e88874e757 | ||
|
|
b6fb7c6977 | ||
|
|
8a60f17d13 | ||
|
|
e1d860a42c | ||
|
|
56da326709 | ||
|
|
672008f8af | ||
|
|
d86b16de6b | ||
|
|
d41710f507 | ||
|
|
2b53166763 | ||
|
|
4e56a16a58 | ||
|
|
ac7cc77017 | ||
|
|
9194879c43 | ||
|
|
1902cdaa55 | ||
|
|
6314987e69 | ||
|
|
65bcbd7737 | ||
|
|
9b25b8538e | ||
|
|
bbf8bb2a94 | ||
|
|
2085227cd9 | ||
|
|
93e0730210 | ||
|
|
25c5c11ab3 | ||
|
|
6ed5b5d35d | ||
|
|
673cc1e0df | ||
|
|
065dcf6f9f | ||
|
|
a66b16ce1c | ||
|
|
e4c0b30284 | ||
|
|
3cb37748e9 | ||
|
|
e54d76f0e2 | ||
|
|
f19e037011 | ||
|
|
ee3f47d918 | ||
|
|
a49a645022 | ||
|
|
cf30441ac6 | ||
|
|
c9bcf866ab | ||
|
|
506609f3cd | ||
|
|
721172c980 | ||
|
|
b183db5b80 | ||
|
|
c7f277f404 | ||
|
|
8ac03a9f8a | ||
|
|
dc1c386d7e | ||
|
|
1eaf4e5bab | ||
|
|
3c0f1c2e73 | ||
|
|
b2790776ef | ||
|
|
2840359396 | ||
|
|
c18ddb99a8 | ||
|
|
432435aa23 | ||
|
|
90c1a6f4ad | ||
|
|
0e8f478c2e | ||
|
|
5f23d5a5ab | ||
|
|
76821a4c2d | ||
|
|
e4a8cca90a | ||
|
|
6024ed4cd1 | ||
|
|
3811f8bbb8 | ||
|
|
306218e5df | ||
|
|
cabfe45608 | ||
|
|
4ade6a5911 | ||
|
|
10d45c3e20 | ||
|
|
6fb247ec06 | ||
|
|
1546258529 | ||
|
|
7ec83b8af6 | ||
|
|
21d0a87c17 | ||
|
|
528f59c1bf | ||
|
|
d1535c4cd8 | ||
|
|
da44d51a3b | ||
|
|
73ce10b26a | ||
|
|
13874b8830 | ||
|
|
ac66809a05 | ||
|
|
f5229d5dec | ||
|
|
222291d11d | ||
|
|
28f47e1093 | ||
|
|
7784346a97 | ||
|
|
ed46af8418 | ||
|
|
095f25a6e9 | ||
|
|
3cdab30dc8 | ||
|
|
8e462047ba | ||
|
|
b7f8b65d9d | ||
|
|
da21c37254 | ||
|
|
a89aef4a89 | ||
|
|
dd6516fd11 | ||
|
|
344a3ce198 | ||
|
|
62f8283587 | ||
|
|
1f89accac3 | ||
|
|
9fcaef4cf7 | ||
|
|
d5738a0631 | ||
|
|
8bf7a1f95a | ||
|
|
04cf36891e | ||
|
|
b2d9948c30 | ||
|
|
a2c9ac1707 | ||
|
|
46a6bd0108 | ||
|
|
5f069e7a40 | ||
|
|
ff57e5c6ae | ||
|
|
f65b6e2d76 | ||
|
|
58fd180ae0 | ||
|
|
cd6503c4c3 | ||
|
|
e8b36c2b6d | ||
|
|
51743f1359 | ||
|
|
504c42646b | ||
|
|
c61e84d1b4 | ||
|
|
b8ab01c46c | ||
|
|
9558060bde | ||
|
|
0741c879f1 | ||
|
|
2e6dd2867e | ||
|
|
9f3fcccb61 | ||
|
|
ae08d7abad | ||
|
|
16808a7ec6 | ||
|
|
b1dfcd6283 | ||
|
|
369f4c0f08 | ||
|
|
827de03579 | ||
|
|
0c7f4e5b43 | ||
|
|
9c700a3fb6 | ||
|
|
89bf83febb | ||
|
|
d44b35cf0a | ||
|
|
891833e273 | ||
|
|
29d3a80d3f | ||
|
|
90fef58684 | ||
|
|
07841722ac | ||
|
|
9e4da5948b | ||
|
|
dc6e4313a1 | ||
|
|
a68cc0b689 | ||
|
|
d057fb0f67 | ||
|
|
7e8f045a29 | ||
|
|
5249d8d18d | ||
|
|
f0fafbadd4 | ||
|
|
8b51079dcc | ||
|
|
354bccd6e8 | ||
|
|
6806772346 | ||
|
|
60082655d4 | ||
|
|
7fe56941fd | ||
|
|
abd498dd33 | ||
|
|
99ca0136ed | ||
|
|
e52830c9b4 | ||
|
|
755decca27 | ||
|
|
41604008d1 | ||
|
|
5bddce8969 | ||
|
|
b25503f141 | ||
|
|
bdf1850448 | ||
|
|
230d3ccf69 | ||
|
|
e378025a2d | ||
|
|
04129ded90 | ||
|
|
a7891f89f8 | ||
|
|
37d4a8e834 | ||
|
|
528a1216f8 | ||
|
|
f2479bd259 | ||
|
|
135f0826d2 | ||
|
|
5710f0177b | ||
|
|
1a3a0528ba | ||
|
|
59793ff374 | ||
|
|
3c4ed42419 | ||
|
|
6a17b61550 | ||
|
|
76659497b7 | ||
|
|
ee50c957cb | ||
|
|
a9dd200919 | ||
|
|
1fa953104a | ||
|
|
50eb5b070e | ||
|
|
10fd624bec | ||
|
|
04ec2f8260 | ||
|
|
d828d79574 | ||
|
|
01dd3fefe5 | ||
|
|
45ae954c0c | ||
|
|
1304849b55 | ||
|
|
0875faa079 | ||
|
|
3aa15d6ea6 | ||
|
|
6557329e97 | ||
|
|
be27ce4161 | ||
|
|
a3391a970e | ||
|
|
744d45d2c5 | ||
|
|
310b16f54a | ||
|
|
6d3ad4a869 | ||
|
|
1ce2448820 | ||
|
|
07b4ada5be | ||
|
|
f70bf530ba | ||
|
|
a26aa0f411 | ||
|
|
584577c21f | ||
|
|
1503375eed | ||
|
|
2dec099f20 | ||
|
|
bf6eed4ea2 | ||
|
|
e0dc33cfb4 | ||
|
|
9c9dc27537 | ||
|
|
7305637b8c | ||
|
|
a6e22adf2e | ||
|
|
809c6b1aa8 | ||
|
|
1536ded2a6 | ||
|
|
76d54971ce | ||
|
|
6dec56198c | ||
|
|
d96d252df3 | ||
|
|
3fca76a716 | ||
|
|
a184fa11f1 | ||
|
|
e9c0b6a703 | ||
|
|
ec7df08496 | ||
|
|
6627d7eac5 | ||
|
|
1f8945d710 | ||
|
|
daaa289a5c | ||
|
|
81a6a48d70 | ||
|
|
b93c7cd261 | ||
|
|
570482e12d | ||
|
|
39407557f2 | ||
|
|
3b040d558d | ||
|
|
56ba3c2a52 | ||
|
|
c7afe33c72 | ||
|
|
82500c2d7d | ||
|
|
fb1c1f117b | ||
|
|
7c6a7513fd | ||
|
|
6d13119ee0 | ||
|
|
1e2c752937 | ||
|
|
e82faf8320 | ||
|
|
4adb1001b0 | ||
|
|
992a1b26c0 | ||
|
|
c07bc6687c | ||
|
|
eea44bda34 | ||
|
|
7cdf263da1 | ||
|
|
ee65a5693c | ||
|
|
0af6ccf35f | ||
|
|
888f1ec6df | ||
|
|
c95a8616ac | ||
|
|
5ea6289b7a | ||
|
|
d4a79ca5db | ||
|
|
70015ce1c4 | ||
|
|
6b85c28944 | ||
|
|
6c289cf273 | ||
|
|
a64bc3c618 | ||
|
|
46cf1e3067 | ||
|
|
565e1eb2bc | ||
|
|
e689687805 | ||
|
|
3c3b61fb47 | ||
|
|
1355d350c5 | ||
|
|
fda911e178 | ||
|
|
f995e1864c | ||
|
|
911e55ccc3 | ||
|
|
41fedfcc40 | ||
|
|
2f9fc95af3 | ||
|
|
11b0b16ca7 | ||
|
|
f8cb7ffdab | ||
|
|
8b59c0aaa0 | ||
|
|
bb2fb7a0c2 | ||
|
|
8f8b36617a | ||
|
|
ff53436fd4 | ||
|
|
cf09a4f733 | ||
|
|
2303c21cea | ||
|
|
89e797b95f | ||
|
|
20375c3a42 | ||
|
|
339ceda18d | ||
|
|
97ce32fd52 | ||
|
|
8c4cbd86f7 | ||
|
|
eaedaa96bb | ||
|
|
bd7ad5fce8 | ||
|
|
759dac1b70 | ||
|
|
38577c6235 | ||
|
|
b24fff0f55 | ||
|
|
9e0efeb092 | ||
|
|
41152f90fc | ||
|
|
4486f8040e | ||
|
|
a98caad968 | ||
|
|
ffb75238b4 | ||
|
|
cba3e74f3a | ||
|
|
8d3f0b5b98 | ||
|
|
a2598b30f7 | ||
|
|
8d11f5ddae | ||
|
|
6ffc88b4ed | ||
|
|
640b451727 | ||
|
|
db6e2eb7ca | ||
|
|
407816b576 | ||
|
|
486cc493e6 | ||
|
|
3eb0b8a89f | ||
|
|
c23eca0ade | ||
|
|
67de153016 | ||
|
|
ba2da69a67 | ||
|
|
111bef14e1 | ||
|
|
22d4c1bd10 | ||
|
|
e3dfb0ae83 | ||
|
|
8d7c3f0e26 | ||
|
|
b11775ebda | ||
|
|
608191183a | ||
|
|
2ba903e170 | ||
|
|
342b64d502 | ||
|
|
1a8f048aaa | ||
|
|
7309581b75 | ||
|
|
36d59c40a7 | ||
|
|
ed3c7c324b | ||
|
|
fd868360a3 | ||
|
|
8dc2ebfbe0 | ||
|
|
997711b34a | ||
|
|
ac01b7129d | ||
|
|
f4b7528c0d | ||
|
|
d7b9ae5df5 | ||
|
|
4db29126e9 | ||
|
|
4cfee2fe45 | ||
|
|
db5dbd6ac2 | ||
|
|
44db775b89 | ||
|
|
8e2e780a1a | ||
|
|
7aba2eb4da | ||
|
|
3e250bfb28 | ||
|
|
9c02ed66f5 | ||
|
|
f5ae8c60d3 | ||
|
|
e90ac391b2 | ||
|
|
0e5f949bcf | ||
|
|
b7c270bef8 | ||
|
|
5f16b506f5 | ||
|
|
176148fc71 | ||
|
|
861daaedf4 | ||
|
|
d8430f4e63 | ||
|
|
dd44864485 | ||
|
|
b3bb29f383 | ||
|
|
b4d137c0de | ||
|
|
62a969d8fe | ||
|
|
961f09e092 | ||
|
|
b2dfa83f18 | ||
|
|
37196ff89f | ||
|
|
b50d982497 | ||
|
|
4a484aca69 | ||
|
|
0bf37d571a | ||
|
|
d4e94ac2bc | ||
|
|
75bc789bb1 | ||
|
|
bc231d5bf1 | ||
|
|
ab9008b3be | ||
|
|
65491936c3 | ||
|
|
e589998840 | ||
|
|
0afba8d11e | ||
|
|
f622ed168f | ||
|
|
5a31df40d8 | ||
|
|
db3b80c96c | ||
|
|
bd28a761d0 | ||
|
|
4f2830a34d | ||
|
|
a5716cd518 | ||
|
|
d035525c7a | ||
|
|
13fa87c449 | ||
|
|
246ccfb049 | ||
|
|
cbffb1e5b8 | ||
|
|
c4b007411d | ||
|
|
04264fe937 | ||
|
|
0c37ad12b7 | ||
|
|
8c5298ed2e | ||
|
|
262118e047 | ||
|
|
e5b63ea2b0 | ||
|
|
746757d402 | ||
|
|
e6e640328b | ||
|
|
845782a69c | ||
|
|
fc6baa1b3b | ||
|
|
47822ecc15 | ||
|
|
3d0c7f1118 | ||
|
|
e9c8e4d325 | ||
|
|
39cc82ce0c | ||
|
|
451f83421a | ||
|
|
5073f2eefb | ||
|
|
231fff5508 | ||
|
|
f1d146eca3 | ||
|
|
b6635fcc4f | ||
|
|
fb5e13ee4c | ||
|
|
ba84741a64 | ||
|
|
65c275ac17 | ||
|
|
825322d489 | ||
|
|
739e2abdf8 | ||
|
|
1d2394e31e | ||
|
|
1b41af798d | ||
|
|
118f4a231b | ||
|
|
e48652f425 | ||
|
|
848f01edf8 | ||
|
|
43fbc42770 | ||
|
|
2c736a0eed | ||
|
|
33ca930055 | ||
|
|
3b396f8421 | ||
|
|
aa77e3a8d3 | ||
|
|
75316d8d87 | ||
|
|
a999109a5c | ||
|
|
bdd33c7d6b | ||
|
|
61c5ee22a0 | ||
|
|
12c5b909e0 | ||
|
|
871ef6dda6 | ||
|
|
40052a2d7c | ||
|
|
347be0137d | ||
|
|
fe6baba8bf | ||
|
|
ed8c313bd4 | ||
|
|
ca0acb34d6 | ||
|
|
34ea639c25 | ||
|
|
680ab9dbed | ||
|
|
9377766e52 | ||
|
|
7a2c37454e | ||
|
|
8f6f418f54 | ||
|
|
9b7745c565 | ||
|
|
a389e8cc38 | ||
|
|
2056301f70 | ||
|
|
93babe3660 | ||
|
|
4aef2d8eb0 | ||
|
|
c5ec2ed230 | ||
|
|
a156d5f6cc | ||
|
|
f84674cddd | ||
|
|
b7b62c197a | ||
|
|
f5350eac36 | ||
|
|
ee6228db15 | ||
|
|
3e434c0878 | ||
|
|
7c5372a8e2 | ||
|
|
6ab68cc36b | ||
|
|
ee2400a5c5 | ||
|
|
80be5d2cf5 | ||
|
|
76cf44a31b | ||
|
|
eb63113227 | ||
|
|
b7045fb3d9 | ||
|
|
11a5b5842d | ||
|
|
362994afd5 | ||
|
|
d7682f4ebd | ||
|
|
56d4fe7fa9 | ||
|
|
93a852ac14 | ||
|
|
964db0ab08 | ||
|
|
8547295da7 | ||
|
|
c75050fa46 | ||
|
|
65372c52aa | ||
|
|
12c767b7d3 | ||
|
|
2fc5e9cfb3 | ||
|
|
bccc78ee65 | ||
|
|
c0793f3ce7 | ||
|
|
a8dd8699a3 | ||
|
|
91bb25e8b6 | ||
|
|
c3f4480cdc | ||
|
|
33e226861e | ||
|
|
4f5acbec9d | ||
|
|
d2c9bbf6ab | ||
|
|
b5e57e1f2f | ||
|
|
c82c8b2272 | ||
|
|
43f1b598a7 | ||
|
|
5081e387d0 | ||
|
|
1e5f55c1d6 | ||
|
|
aa38773be3 | ||
|
|
96530bd809 | ||
|
|
127f95a9d0 | ||
|
|
cf31e12348 | ||
|
|
4adaa531b7 | ||
|
|
c7659563b8 | ||
|
|
7b5494468f | ||
|
|
d12c67fd54 | ||
|
|
e20eacb5ee | ||
|
|
98055f0762 | ||
|
|
a46c42c3a0 | ||
|
|
c3d1503361 | ||
|
|
65b47d27cd | ||
|
|
968c07f8be | ||
|
|
91814ca105 | ||
|
|
9128f24270 | ||
|
|
264897bbd5 | ||
|
|
67d59ff1ba | ||
|
|
81eee56e1e | ||
|
|
92ea8af786 | ||
|
|
df0616f77e | ||
|
|
a337dff85c | ||
|
|
48e349c1d2 | ||
|
|
df9fa0fec7 | ||
|
|
b3e72f0726 | ||
|
|
700ebc4727 | ||
|
|
745a18efca | ||
|
|
b4a8147ff0 | ||
|
|
0543c25054 | ||
|
|
b1a0afea66 | ||
|
|
43c41e2557 | ||
|
|
bc8eb33f05 | ||
|
|
d1cebc92f7 | ||
|
|
bf02cf4058 | ||
|
|
5212773d0a | ||
|
|
bf53204a41 | ||
|
|
092a19966c | ||
|
|
6060a2b798 | ||
|
|
ce12af68b2 | ||
|
|
c05f8d47aa | ||
|
|
f8be19c20e | ||
|
|
2fe32a2145 | ||
|
|
794ea4d0e8 | ||
|
|
307b0f5afd | ||
|
|
9011e3f4b4 | ||
|
|
0b45ce5c0c | ||
|
|
a8ab93a4cc | ||
|
|
d1768be636 | ||
|
|
b2cd7c82a7 | ||
|
|
b88d99afe0 | ||
|
|
4523d38551 | ||
|
|
c4ba892559 | ||
|
|
40f8093e00 | ||
|
|
4375459856 | ||
|
|
b3eb8a70bf | ||
|
|
6245a487b6 | ||
|
|
a89f4c1b10 | ||
|
|
7df729b8b5 | ||
|
|
f1db7c7e82 | ||
|
|
00af69cbf2 | ||
|
|
c6819a19bd | ||
|
|
f5cbcb3d24 | ||
|
|
6ca75c53ef | ||
|
|
e0ec443e10 | ||
|
|
9eed2fc2ef | ||
|
|
b9c6d3ff9f | ||
|
|
03c25a89b2 | ||
|
|
f5b4a0c5ab | ||
|
|
b10ed46ccc | ||
|
|
c55036a1b1 | ||
|
|
962a5dade5 | ||
|
|
369a09a6df | ||
|
|
e9a39e2571 | ||
|
|
1cf9cc18f1 | ||
|
|
1984d4ec8d | ||
|
|
fa99b4ebdc | ||
|
|
83c793a2a5 | ||
|
|
d078727070 | ||
|
|
0d8e2c5ff0 | ||
|
|
ea062d7494 | ||
|
|
d525e0597a | ||
|
|
964b5d4e74 | ||
|
|
abf17eb09f | ||
|
|
178809dad7 | ||
|
|
881d189715 | ||
|
|
48a8973910 | ||
|
|
ae891932c5 | ||
|
|
a8b5e00814 | ||
|
|
e98432d7e4 | ||
|
|
29ff58ed98 | ||
|
|
a621575ffc | ||
|
|
d7c7efc203 | ||
|
|
4f013ca927 | ||
|
|
d9823feef7 | ||
|
|
676d775544 | ||
|
|
840ad17258 | ||
|
|
ce0cd09b70 | ||
|
|
bb752fbc00 | ||
|
|
c71fb04404 | ||
|
|
e856cf8787 | ||
|
|
71aae65bc6 | ||
|
|
7a33444f9b | ||
|
|
e12ec9d986 | ||
|
|
94d1fb7dcb | ||
|
|
6e43a42274 | ||
|
|
0e0aa0259e | ||
|
|
75ab6d39ef | ||
|
|
6f84d84f96 | ||
|
|
cf46593e9b | ||
|
|
1808346348 | ||
|
|
ea59a68499 | ||
|
|
cba2d02990 | ||
|
|
e92fc0d3e1 | ||
|
|
f1eaa77092 | ||
|
|
5934bdec55 | ||
|
|
7a7d3c6ee3 | ||
|
|
cb2f58c04c | ||
|
|
a1c117f1ca | ||
|
|
1aacd98460 | ||
|
|
840162c9ca | ||
|
|
3519b0e009 | ||
|
|
53fae56c51 | ||
|
|
f4552dbaa8 | ||
|
|
dfab4c331c | ||
|
|
d2a0937038 | ||
|
|
fa1ef44b22 | ||
|
|
c1a5c91299 | ||
|
|
e6280e8f5a | ||
|
|
fd2ba57325 | ||
|
|
dc178ef03f | ||
|
|
68936e10c5 | ||
|
|
41f16e7d39 | ||
|
|
453ebbf90a | ||
|
|
7ca290c080 | ||
|
|
dd5b6a6aa6 | ||
|
|
bf2ebb995b | ||
|
|
3e929758fb | ||
|
|
4a53116586 | ||
|
|
2d1981acfa | ||
|
|
c610df269f | ||
|
|
96dac635f5 | ||
|
|
abd579103d | ||
|
|
7ed27bb413 | ||
|
|
180c542a9f | ||
|
|
89613113de | ||
|
|
3102900776 | ||
|
|
1a68053f4e | ||
|
|
5d4a032763 | ||
|
|
7481d27822 | ||
|
|
22afd01fd2 | ||
|
|
fd9eb09dd6 | ||
|
|
c53ed7f984 | ||
|
|
1d87e210be | ||
|
|
5fec952b8c | ||
|
|
c4fbcc103a | ||
|
|
9333a1ba45 | ||
|
|
de5cd92dba | ||
|
|
83e32bec9f | ||
|
|
4603efc04e | ||
|
|
6c73c82a27 | ||
|
|
8e71d9f9e9 | ||
|
|
80f60a5d88 | ||
|
|
2c33ec382d | ||
|
|
884865cffc | ||
|
|
9537fe8c91 | ||
|
|
c8fe36a2b6 | ||
|
|
7ec3282b56 | ||
|
|
ab8c41a520 | ||
|
|
4c1c70ff04 | ||
|
|
d7a6e80bb1 | ||
|
|
f87ef670d1 | ||
|
|
daba2505bc | ||
|
|
71528eda45 | ||
|
|
269e2beeaa | ||
|
|
72dd2c6bc5 | ||
|
|
9758f7a15f | ||
|
|
2521eb5a4a | ||
|
|
bf6afc5e61 | ||
|
|
291d869843 | ||
|
|
cdfecb2a8f | ||
|
|
c0df8e0ec3 | ||
|
|
a21f7d1653 | ||
|
|
79e2514416 | ||
|
|
80a7385183 | ||
|
|
1c8a7b6a7c | ||
|
|
b46ced2b6f | ||
|
|
5ae479644e | ||
|
|
cb66eb1293 | ||
|
|
b4d5a2cad4 | ||
|
|
32691b0c0d | ||
|
|
9163f744a3 | ||
|
|
23ecce51a3 | ||
|
|
747ffa8cf1 | ||
|
|
4e230899ed | ||
|
|
aff9f753c7 | ||
|
|
1127fac500 | ||
|
|
7707e1cd2b | ||
|
|
30274f7f1a | ||
|
|
a5da6cc781 | ||
|
|
201e4010c5 | ||
|
|
b9bbdbffae | ||
|
|
9e8e1996e8 | ||
|
|
b99235397a | ||
|
|
c0fd06daf3 | ||
|
|
90536294df | ||
|
|
f8e9787675 | ||
|
|
bf9b785946 | ||
|
|
dab3a6f031 | ||
|
|
88466193c5 | ||
|
|
c2f7601dad | ||
|
|
189f7b2d7b | ||
|
|
5d87160ee9 | ||
|
|
0cdcdb8898 | ||
|
|
c20619b5c8 | ||
|
|
93f84abce4 | ||
|
|
3c6a32caba | ||
|
|
72c08e5fdb | ||
|
|
022d80b5ce | ||
|
|
f48da51802 | ||
|
|
709d04b33f | ||
|
|
3a251dd063 | ||
|
|
9cc92b59b3 | ||
|
|
e0469b10a7 | ||
|
|
39eb67b162 | ||
|
|
8d9bea3b0f | ||
|
|
7015d8dde8 | ||
|
|
6b54072cb5 | ||
|
|
b898708306 | ||
|
|
1b1cf36839 | ||
|
|
18b89edc19 | ||
|
|
70c7ef4247 | ||
|
|
a8196709be | ||
|
|
a9d249ccf0 | ||
|
|
cd83d918e5 | ||
|
|
b64a84a362 | ||
|
|
f4426a8ee0 | ||
|
|
df8c44d2ce | ||
|
|
65ce3d4822 | ||
|
|
e5b1d94579 | ||
|
|
0b4754ca24 | ||
|
|
b3dc8d55be | ||
|
|
e5ab7667b9 | ||
|
|
2a4e6435f1 | ||
|
|
804431c44a | ||
|
|
f3b26bf7a0 | ||
|
|
aa27d90017 | ||
|
|
0fa8cf2dd0 | ||
|
|
634b5c37ba | ||
|
|
6e16f81fc6 | ||
|
|
ffc4024f70 | ||
|
|
a6981185b2 | ||
|
|
0361fabb66 | ||
|
|
04e7f101be | ||
|
|
86d680cfdf | ||
|
|
7bc2152057 | ||
|
|
74ed9a5040 | ||
|
|
30115fc0dc | ||
|
|
8e8e5e0e29 | ||
|
|
b58ce1b571 | ||
|
|
df5c4e4210 | ||
|
|
ba2e0999f1 | ||
|
|
a1f5dd08c7 | ||
|
|
c1123fef63 | ||
|
|
4dc6149530 | ||
|
|
1245ff2cce | ||
|
|
284204fd78 | ||
|
|
f7be74eb73 | ||
|
|
aeea619059 | ||
|
|
ea2b7f20e7 | ||
|
|
f40f1d25de | ||
|
|
c04bb2d7cf | ||
|
|
4c1ccd65bf | ||
|
|
54e064b0fc | ||
|
|
9a8b72883c | ||
|
|
5d75ee2e47 | ||
|
|
7357fa4871 | ||
|
|
f6850b5427 | ||
|
|
1e9a181e60 | ||
|
|
7224a1ccec | ||
|
|
9b935a8180 | ||
|
|
c4a693846e | ||
|
|
02c7cdcd97 | ||
|
|
36bee92e8e | ||
|
|
6542804f44 | ||
|
|
d805fd614c | ||
|
|
21299dc65f | ||
|
|
0df0946c2a | ||
|
|
1bb38d50a1 | ||
|
|
242987cb41 | ||
|
|
5456abde54 | ||
|
|
4a07427440 | ||
|
|
cf60dae2ff | ||
|
|
659997c6c1 | ||
|
|
389a47a152 | ||
|
|
b6b81ca487 | ||
|
|
719c1786f1 | ||
|
|
4685bac4b3 | ||
|
|
a05275cc08 | ||
|
|
1abf1b1658 | ||
|
|
8893dc5698 | ||
|
|
f8467b2da1 | ||
|
|
f39b8b000e | ||
|
|
961036fa31 | ||
|
|
8b9056894e | ||
|
|
7f979e9396 | ||
|
|
8db1192d0e | ||
|
|
3bd515951a | ||
|
|
2bd5b9676b | ||
|
|
993698ec01 | ||
|
|
fcdc44ddbe | ||
|
|
0cb6b0ae12 | ||
|
|
0388927c5b | ||
|
|
2c8f5d2129 | ||
|
|
2bffbf83c6 | ||
|
|
ec01529014 | ||
|
|
ea1abf5662 | ||
|
|
fcd5263af1 | ||
|
|
1fa8616dbc | ||
|
|
f627277b45 | ||
|
|
809c3ea270 | ||
|
|
9caa9d79d4 | ||
|
|
a8f1c1c323 | ||
|
|
59fbea5c5f | ||
|
|
d3273d879a | ||
|
|
ea227e82c9 | ||
|
|
676b330aa3 | ||
|
|
8d3295e125 | ||
|
|
8cb74e0bcc | ||
|
|
26749a4dbc | ||
|
|
14915d37b4 | ||
|
|
16d6d58efb | ||
|
|
b2b97a313a | ||
|
|
2974e6a64f | ||
|
|
dae34d0aa5 | ||
|
|
e9d0671184 | ||
|
|
897e33bf6a | ||
|
|
f37e93a409 | ||
|
|
561ac63c93 | ||
|
|
149e4be07a | ||
|
|
eb98dc9e06 | ||
|
|
ce7d965358 | ||
|
|
bb73024859 | ||
|
|
631a442e2a | ||
|
|
20a082ae8e | ||
|
|
154d706be1 | ||
|
|
aaae257c75 | ||
|
|
ec944a2283 | ||
|
|
69d63ecffe | ||
|
|
06a1e2992d | ||
|
|
ec9293bac0 | ||
|
|
8bf727c43c | ||
|
|
708b8afdf5 | ||
|
|
6f7b9b1ebf | ||
|
|
4f48c4fd9f | ||
|
|
9dac90e6cc | ||
|
|
0741a6f485 | ||
|
|
1f338dfd2b | ||
|
|
0310c26edd | ||
|
|
e43570d4c1 | ||
|
|
d7142c2935 | ||
|
|
fa6dc04ef1 | ||
|
|
c5ec7a4b37 | ||
|
|
bd3d643318 | ||
|
|
1ae635963c | ||
|
|
a61c3e798d | ||
|
|
931addedbe | ||
|
|
9aa4a547cf | ||
|
|
82a7df5e3f | ||
|
|
0ff87383d0 | ||
|
|
ea3dec443f | ||
|
|
d7a895b98b | ||
|
|
b90b7b7845 | ||
|
|
3d05d75f2d | ||
|
|
e701881377 | ||
|
|
4615e3e48f | ||
|
|
988a5361a6 | ||
|
|
1b60b42428 | ||
|
|
7b74487214 | ||
|
|
91d98ed267 | ||
|
|
24fbb7de06 | ||
|
|
91df5b2407 | ||
|
|
eef2d9d599 | ||
|
|
8ad70b7e74 | ||
|
|
8b775103c7 | ||
|
|
8c2475aac1 | ||
|
|
d3236ae4a1 | ||
|
|
8fd9af3a6a | ||
|
|
4f94858901 | ||
|
|
d0ac4ad222 | ||
|
|
10e4476f4a | ||
|
|
74411d883a | ||
|
|
18ee2d5316 | ||
|
|
b4f80422eb | ||
|
|
7fe9ab7601 | ||
|
|
13ad614e6b | ||
|
|
7b1b4bcf2c | ||
|
|
a6715d4034 | ||
|
|
254995c6c6 | ||
|
|
0e33e23d1f | ||
|
|
57503eefd2 | ||
|
|
cd477c880c | ||
|
|
bf2f5b24dc | ||
|
|
7eba4865bc | ||
|
|
64cd24a24d | ||
|
|
de5acd0949 | ||
|
|
6ebbc3edac | ||
|
|
2020f6c136 | ||
|
|
ee4acd5a82 | ||
|
|
2e487ac936 | ||
|
|
dee5fc7c07 | ||
|
|
e130584fec | ||
|
|
ade5c4832f | ||
|
|
33b14d5278 | ||
|
|
48e73c2c81 | ||
|
|
894d98e92b | ||
|
|
14c84eff93 | ||
|
|
01d982b093 | ||
|
|
35852ef6ab | ||
|
|
47b235dc5f | ||
|
|
0e11d2a581 | ||
|
|
e57e11f206 | ||
|
|
51f639065c | ||
|
|
6c1d4b466f | ||
|
|
b940c597dc | ||
|
|
1ce67bcb39 | ||
|
|
5050a67d08 | ||
|
|
76321cef3c | ||
|
|
8f3a203994 | ||
|
|
ea24562861 | ||
|
|
4a888d048e | ||
|
|
36331bb993 | ||
|
|
6f0e7c0ca8 | ||
|
|
2491b7b7e3 | ||
|
|
7965c17f46 | ||
|
|
5519459519 | ||
|
|
d8bf2cc101 | ||
|
|
2aece98d67 | ||
|
|
f602497255 | ||
|
|
11db68cdc7 | ||
|
|
bed98f4344 |
No files matched your search
@@ -1,6 +1,11 @@
|
||||
---
|
||||
EnableDefaultLinters: true
|
||||
linters:
|
||||
AllowedScriptType:
|
||||
enabled: true
|
||||
allowed_types:
|
||||
- text/javascript
|
||||
- application/json
|
||||
ErbSafety:
|
||||
enabled: false
|
||||
HardCodedString:
|
||||
|
||||
+24
-29
@@ -6,12 +6,13 @@ jobs:
|
||||
rubocop:
|
||||
name: Rubocop
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 10
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Install Ruby
|
||||
uses: ruby/setup-ruby@v1
|
||||
with:
|
||||
ruby-version: 3.4.2
|
||||
ruby-version: 4.0.5
|
||||
- name: Cache gems
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
@@ -30,12 +31,13 @@ jobs:
|
||||
erblint:
|
||||
name: Erblint
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 10
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Install Ruby
|
||||
uses: ruby/setup-ruby@v1
|
||||
with:
|
||||
ruby-version: 3.4.2
|
||||
ruby-version: 4.0.5
|
||||
- name: Cache gems
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
@@ -54,12 +56,13 @@ jobs:
|
||||
eslint:
|
||||
name: ESLint
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 10
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Install Node.js
|
||||
uses: actions/setup-node@v1
|
||||
with:
|
||||
node-version: 20.9.0
|
||||
node-version: 20.19.0
|
||||
- name: Cache directory path
|
||||
id: yarn-cache-dir-path
|
||||
run: echo "::set-output name=dir::$(yarn cache dir)"
|
||||
@@ -80,12 +83,13 @@ jobs:
|
||||
brakeman:
|
||||
name: Brakeman
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 10
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Install Ruby
|
||||
uses: ruby/setup-ruby@v1
|
||||
with:
|
||||
ruby-version: 3.4.2
|
||||
ruby-version: 4.0.5
|
||||
- name: Cache gems
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
@@ -100,54 +104,46 @@ jobs:
|
||||
bundle install --jobs 4 --retry 4
|
||||
yarn install
|
||||
sudo apt-get update
|
||||
sudo apt-get install libvips
|
||||
sudo apt-get install libvips liblept5
|
||||
- name: Run Brakeman
|
||||
run: bundle exec brakeman -q --exit-on-warn
|
||||
|
||||
rspec:
|
||||
name: RSpec
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 15
|
||||
container:
|
||||
image: ruby:4.0.5-alpine
|
||||
|
||||
services:
|
||||
postgres:
|
||||
image: postgres:14
|
||||
image: postgres:18
|
||||
env:
|
||||
POSTGRES_USER: postgres
|
||||
POSTGRES_PASSWORD: postgres
|
||||
POSTGRES_DB: docuseal_test
|
||||
ports: ["5432:5432"]
|
||||
options: >-
|
||||
--health-cmd pg_isready
|
||||
--health-cmd "pg_isready -U postgres -d docuseal_test"
|
||||
--health-interval 10s
|
||||
--health-timeout 5s
|
||||
--health-retries 5
|
||||
|
||||
steps:
|
||||
- name: Install packages
|
||||
run: apk add --no-cache build-base git libpq-dev yaml-dev nodejs yarn tar zstd gzip wget unzip vips leptonica chromium
|
||||
- uses: actions/checkout@v4
|
||||
- name: Install Ruby
|
||||
uses: ruby/setup-ruby@v1
|
||||
with:
|
||||
ruby-version: 3.4.2
|
||||
- name: Set up Node
|
||||
uses: actions/setup-node@v1
|
||||
with:
|
||||
node-version: 20.9.0
|
||||
- name: Install Chrome
|
||||
uses: browser-actions/setup-chrome@latest
|
||||
with:
|
||||
chrome-version: 125
|
||||
- name: Cache node_modules
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: node_modules
|
||||
key: ${{ runner.os }}-node-${{ hashFiles('**/yarn.lock') }}
|
||||
key: ${{ runner.os }}-musl-node-${{ hashFiles('**/yarn.lock') }}
|
||||
- name: Cache gems
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: vendor/bundle
|
||||
key: ${{ runner.os }}-gem-${{ hashFiles('**/Gemfile.lock') }}
|
||||
key: ${{ runner.os }}-musl-gem-${{ hashFiles('**/Gemfile.lock') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-gem-
|
||||
${{ runner.os }}-musl-gem-
|
||||
- name: Install dependencies
|
||||
env:
|
||||
RAILS_ENV: test
|
||||
@@ -156,17 +152,16 @@ jobs:
|
||||
bundle config path vendor/bundle
|
||||
bundle install --jobs 4 --retry 4
|
||||
yarn install
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y libvips
|
||||
wget -O pdfium-linux.tgz "https://github.com/docusealco/pdfium-binaries/releases/latest/download/pdfium-linux-$(uname -m | sed 's/x86_64/x64/;s/aarch64/arm64/').tgz"
|
||||
sudo tar -xzf pdfium-linux.tgz --strip-components=1 -C /usr/lib lib/libpdfium.so
|
||||
rm -f pdfium-linux.tgz
|
||||
wget -O pdfium-linux.zip "https://github.com/docusealco/pdfium-binaries/releases/latest/download/pdfium-musl-$(uname -m).zip"
|
||||
unzip -q pdfium-linux.zip -d /tmp/pdfium-linux
|
||||
cp /tmp/pdfium-linux/lib/libpdfium.so /usr/lib/libpdfium.so
|
||||
rm -rf pdfium-linux.zip /tmp/pdfium-linux
|
||||
- name: Run
|
||||
env:
|
||||
RAILS_ENV: test
|
||||
NODE_ENV: test
|
||||
COVERAGE: true
|
||||
DATABASE_URL: postgres://postgres:postgres@localhost:5432/docuseal_test
|
||||
DATABASE_URL: postgres://postgres:postgres@postgres:5432/docuseal_test
|
||||
run: |
|
||||
bundle exec rake db:create
|
||||
bundle exec rake db:migrate
|
||||
|
||||
@@ -8,6 +8,7 @@ on:
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-24.04-arm
|
||||
timeout-minutes: 30
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
@@ -15,15 +16,13 @@ jobs:
|
||||
with:
|
||||
submodules: recursive
|
||||
|
||||
-
|
||||
name: Docker meta
|
||||
- name: Docker meta
|
||||
id: meta
|
||||
uses: docker/metadata-action@v4
|
||||
with:
|
||||
images: |
|
||||
docuseal/docuseal
|
||||
tags: |
|
||||
type=semver,pattern={{version}}
|
||||
images: docuseal/docuseal
|
||||
tags: type=semver,pattern={{version}}
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3
|
||||
|
||||
@@ -31,7 +30,9 @@ jobs:
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Create .version file
|
||||
run: echo ${{ github.ref_name }} > .version
|
||||
env:
|
||||
REF_NAME: ${{ github.ref_name }}
|
||||
run: echo "$REF_NAME" > .version
|
||||
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v3
|
||||
|
||||
@@ -37,3 +37,4 @@ yarn-debug.log*
|
||||
/docuseal
|
||||
/ee
|
||||
dump.rdb
|
||||
*.onnx
|
||||
+43
-6
@@ -1,4 +1,4 @@
|
||||
require:
|
||||
plugins:
|
||||
- rubocop-performance
|
||||
- rubocop-rails
|
||||
- rubocop-rspec
|
||||
@@ -10,7 +10,7 @@ AllCops:
|
||||
- node_modules/**/*
|
||||
- bin/*
|
||||
- vendor/**/*
|
||||
TargetRubyVersion: '3.3'
|
||||
TargetRubyVersion: '4.0'
|
||||
SuggestExtensions: false
|
||||
|
||||
Metrics/BlockLength:
|
||||
@@ -28,7 +28,7 @@ Lint/MissingSuper:
|
||||
Enabled: false
|
||||
|
||||
Metrics/ParameterLists:
|
||||
Max: 10
|
||||
Max: 12
|
||||
|
||||
Metrics/MethodLength:
|
||||
Max: 30
|
||||
@@ -42,6 +42,27 @@ Metrics/CyclomaticComplexity:
|
||||
Metrics/PerceivedComplexity:
|
||||
Max: 15
|
||||
|
||||
Style/MultipleComparison:
|
||||
Enabled: false
|
||||
|
||||
Style/NumericPredicate:
|
||||
Enabled: false
|
||||
|
||||
Style/MinMaxComparison:
|
||||
Enabled: false
|
||||
|
||||
Style/EmptyClassDefinition:
|
||||
Enabled: false
|
||||
|
||||
Style/OneClassPerFile:
|
||||
Enabled: false
|
||||
|
||||
Layout/MultilineMethodCallIndentation:
|
||||
Enabled: false
|
||||
|
||||
Naming/PredicateMethod:
|
||||
Enabled: false
|
||||
|
||||
Layout/LineLength:
|
||||
AllowedPatterns: ['\A\s*#']
|
||||
|
||||
@@ -63,13 +84,16 @@ RSpec/MultipleExpectations:
|
||||
Max: 25
|
||||
|
||||
RSpec/ExampleLength:
|
||||
Max: 50
|
||||
Max: 500
|
||||
|
||||
RSpec/MultipleMemoizedHelpers:
|
||||
Max: 9
|
||||
Max: 15
|
||||
|
||||
RSpec/AnyInstance:
|
||||
Enabled: false
|
||||
|
||||
Metrics/BlockNesting:
|
||||
Max: 5
|
||||
Max: 6
|
||||
|
||||
Rails/I18nLocaleTexts:
|
||||
Enabled: false
|
||||
@@ -85,3 +109,16 @@ Rails/ApplicationController:
|
||||
|
||||
Rails/Output:
|
||||
Enabled: false
|
||||
|
||||
Rails/StrongParametersExpect:
|
||||
Enabled: false
|
||||
|
||||
Rails/RedirectBackOrTo:
|
||||
Enabled: false
|
||||
|
||||
Rails/UnknownEnv:
|
||||
Environments:
|
||||
- development
|
||||
- test
|
||||
- production
|
||||
- local
|
||||
+43
-33
@@ -1,21 +1,23 @@
|
||||
FROM ruby:3.4.2-alpine AS download
|
||||
FROM ruby:4.0.5-alpine AS download
|
||||
|
||||
WORKDIR /fonts
|
||||
|
||||
RUN apk --no-cache add fontforge wget && \
|
||||
RUN apk --no-cache add wget unzip && \
|
||||
wget https://github.com/satbyy/go-noto-universal/releases/download/v7.0/GoNotoKurrent-Regular.ttf && \
|
||||
wget https://github.com/satbyy/go-noto-universal/releases/download/v7.0/GoNotoKurrent-Bold.ttf && \
|
||||
wget https://github.com/impallari/DancingScript/raw/master/fonts/DancingScript-Regular.otf && \
|
||||
wget https://cdn.jsdelivr.net/gh/notofonts/notofonts.github.io/fonts/NotoSansSymbols2/hinted/ttf/NotoSansSymbols2-Regular.ttf && \
|
||||
wget https://github.com/Maxattax97/gnu-freefont/raw/master/ttf/FreeSans.ttf && \
|
||||
wget https://github.com/impallari/DancingScript/raw/master/OFL.txt && \
|
||||
wget -O pdfium-linux.tgz "https://github.com/docusealco/pdfium-binaries/releases/latest/download/pdfium-linux-$(uname -m | sed 's/x86_64/x64/;s/aarch64/arm64/').tgz" && \
|
||||
wget https://raw.githubusercontent.com/impallari/DancingScript/master/OFL.txt && \
|
||||
wget https://raw.githubusercontent.com/notofonts/noto-fonts/refs/heads/main/LICENSE && \
|
||||
wget -O /model.onnx "https://github.com/docusealco/fields-detection/releases/download/2.0.0/model_704_int8.onnx" && \
|
||||
wget -O pdfium-linux.zip "https://github.com/docusealco/pdfium-binaries/releases/download/20260920/pdfium-musl-$(uname -m).zip" && \
|
||||
case "$(uname -m)" in \
|
||||
x86_64) echo "bbca8a648dbd1ba81f9c6c223f21dc394cf1ad381c6c17d60c6dcc4a7d3a2ae0 pdfium-linux.zip" ;; \
|
||||
aarch64) echo "73b72ed79b9bfdd494dee7e863482c24971a8d4b11f6cd1289aa170d31304a1f pdfium-linux.zip" ;; \
|
||||
esac | sha256sum -c - && \
|
||||
mkdir -p /pdfium-linux && \
|
||||
tar -xzf pdfium-linux.tgz -C /pdfium-linux
|
||||
unzip -q pdfium-linux.zip -d /pdfium-linux
|
||||
|
||||
RUN fontforge -lang=py -c 'font1 = fontforge.open("FreeSans.ttf"); font2 = fontforge.open("NotoSansSymbols2-Regular.ttf"); font1.mergeFonts(font2); font1.generate("FreeSans.ttf")'
|
||||
|
||||
FROM ruby:3.4.2-alpine AS webpack
|
||||
FROM ruby:4.0.5-alpine AS webpack
|
||||
|
||||
ENV RAILS_ENV=production
|
||||
ENV NODE_ENV=production
|
||||
@@ -35,22 +37,25 @@ COPY ./config/shakapacker.yml ./config/shakapacker.yml
|
||||
COPY ./postcss.config.js ./postcss.config.js
|
||||
COPY ./tailwind.config.js ./tailwind.config.js
|
||||
COPY ./tailwind.form.config.js ./tailwind.form.config.js
|
||||
COPY ./tailwind.dynamic.config.js ./tailwind.dynamic.config.js
|
||||
COPY ./tailwind.application.config.js ./tailwind.application.config.js
|
||||
COPY ./app/javascript ./app/javascript
|
||||
COPY ./app/views ./app/views
|
||||
|
||||
RUN echo "gem 'shakapacker'" > Gemfile && ./bin/shakapacker
|
||||
|
||||
FROM ruby:3.4.2-alpine AS app
|
||||
FROM ruby:4.0.5-alpine AS app
|
||||
|
||||
ENV RAILS_ENV=production
|
||||
ENV BUNDLE_WITHOUT="development:test"
|
||||
ENV LD_PRELOAD=/lib/libgcompat.so.0
|
||||
ENV OPENSSL_CONF=/app/openssl_legacy.cnf
|
||||
ENV OPENSSL_CONF=/etc/openssl_legacy.cnf
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
RUN echo '@edge https://dl-cdn.alpinelinux.org/alpine/edge/community' >> /etc/apk/repositories && apk add --no-cache sqlite-dev libpq-dev mariadb-dev vips-dev@edge redis libheif@edge vips-heif@edge gcompat ttf-freefont && mkdir /fonts && rm /usr/share/fonts/freefont/FreeSans.otf
|
||||
RUN apk add --no-cache libpq vips redis onnxruntime leptonica && \
|
||||
rm -f /usr/bin/onnx_test_runner /usr/bin/onnxruntime_test
|
||||
|
||||
RUN addgroup -g 2000 docuseal && adduser -u 2000 -G docuseal -s /bin/sh -D -h /home/docuseal docuseal
|
||||
|
||||
RUN echo $'.include = /etc/ssl/openssl.cnf\n\
|
||||
\n\
|
||||
@@ -62,34 +67,39 @@ legacy = legacy_sect\n\
|
||||
activate = 1\n\
|
||||
\n\
|
||||
[legacy_sect]\n\
|
||||
activate = 1' >> /app/openssl_legacy.cnf
|
||||
activate = 1' >> /etc/openssl_legacy.cnf
|
||||
|
||||
COPY ./Gemfile ./Gemfile.lock ./
|
||||
COPY --chown=docuseal:docuseal ./Gemfile ./Gemfile.lock ./
|
||||
|
||||
RUN apk add --no-cache build-base && bundle install && apk del --no-cache build-base && rm -rf ~/.bundle /usr/local/bundle/cache && ruby -e "puts Dir['/usr/local/bundle/**/{spec,rdoc,resources/shared,resources/collation,resources/locales}']" | xargs rm -rf
|
||||
RUN apk add --no-cache build-base git libpq-dev yaml-dev && bundle install && apk del --no-cache build-base git libpq-dev yaml-dev && rm -rf ~/.bundle /usr/local/bundle/cache && ruby -e "puts Dir['/usr/local/bundle/**/{spec,rdoc,resources/shared,resources/collation,resources/locales,resources/unicode_data/properties}'] + Dir['/usr/local/bundle/gems/*/{test,tests,examples,sample,misc,doc,docs}'] + Dir['/usr/local/bundle/gems/*/ext/**/*.{c,h,o,S}']" | xargs rm -rf && ln -sf /usr/lib/libonnxruntime.so.1 $(ruby -e "print Dir[Gem::Specification.find_by_name('onnxruntime').gem_dir + '/vendor/*.so'].first")
|
||||
|
||||
COPY ./bin ./bin
|
||||
COPY ./app ./app
|
||||
COPY ./config ./config
|
||||
COPY ./db/migrate ./db/migrate
|
||||
COPY ./log ./log
|
||||
COPY ./lib ./lib
|
||||
COPY ./public ./public
|
||||
COPY ./tmp ./tmp
|
||||
COPY LICENSE README.md Rakefile config.ru .version ./
|
||||
COPY .version ./public/version
|
||||
COPY --chown=docuseal:docuseal ./bin ./bin
|
||||
COPY --chown=docuseal:docuseal ./app ./app
|
||||
COPY --chown=docuseal:docuseal ./config ./config
|
||||
COPY --chown=docuseal:docuseal ./db/migrate ./db/migrate
|
||||
COPY --chown=docuseal:docuseal ./log ./log
|
||||
COPY --chown=docuseal:docuseal ./lib ./lib
|
||||
COPY --chown=docuseal:docuseal ./public ./public
|
||||
COPY --chown=docuseal:docuseal ./tmp ./tmp
|
||||
COPY --chown=docuseal:docuseal LICENSE LICENSE_ADDITIONAL_TERMS README.md Rakefile config.ru .version ./
|
||||
COPY --chown=docuseal:docuseal .version ./public/version
|
||||
|
||||
COPY --from=download /fonts/GoNotoKurrent-Regular.ttf /fonts/GoNotoKurrent-Bold.ttf /fonts/DancingScript-Regular.otf /fonts/OFL.txt /fonts
|
||||
COPY --from=download /fonts/FreeSans.ttf /usr/share/fonts/freefont
|
||||
COPY --chown=docuseal:docuseal --from=download /fonts/GoNotoKurrent-Regular.ttf /fonts/GoNotoKurrent-Bold.ttf /fonts/DancingScript-Regular.otf /fonts/OFL.txt /fonts/LICENSE /fonts/
|
||||
COPY --from=download /pdfium-linux/lib/libpdfium.so /usr/lib/libpdfium.so
|
||||
COPY --from=download /pdfium-linux/licenses/pdfium.txt /usr/lib/libpdfium-LICENSE.txt
|
||||
COPY --from=webpack /app/public/packs ./public/packs
|
||||
COPY --from=download /pdfium-linux/licenses/ /usr/lib/libpdfium-licenses/
|
||||
COPY --chown=docuseal:docuseal --from=download /model.onnx /app/tmp/model.onnx
|
||||
COPY --chown=docuseal:docuseal --from=webpack /app/public/packs ./public/packs
|
||||
|
||||
RUN ln -s /fonts /app/public/fonts
|
||||
RUN bundle exec bootsnap precompile --gemfile app/ lib/
|
||||
RUN mkdir -p /app/public/fonts && ln -s /fonts/DancingScript-Regular.otf /app/public/fonts/ && \
|
||||
mkdir -p /usr/share/fonts/noto && ln -s /fonts/GoNotoKurrent-Regular.ttf /usr/share/fonts/noto/ && ln -s /fonts/GoNotoKurrent-Bold.ttf /usr/share/fonts/noto/ && fc-cache -f && \
|
||||
bundle exec bootsnap precompile -j 1 --gemfile app/ lib/ && \
|
||||
chown -R docuseal:docuseal /app/tmp/cache
|
||||
|
||||
WORKDIR /data/docuseal
|
||||
ENV HOME=/home/docuseal
|
||||
ENV WORKDIR=/data/docuseal
|
||||
ENV VIPS_MAX_COORD=17000
|
||||
ENV VIPS_BLOCK_UNTRUSTED=1
|
||||
|
||||
EXPOSE 3000
|
||||
CMD ["/app/bin/bundle", "exec", "puma", "-C", "/app/config/puma.rb", "--dir", "/app"]
|
||||
@@ -2,15 +2,17 @@
|
||||
|
||||
source 'https://rubygems.org'
|
||||
|
||||
ruby '3.4.2'
|
||||
ruby '4.0.5'
|
||||
|
||||
gem 'arabic-letter-connector', require: 'arabic-letter-connector/logic'
|
||||
gem 'addressable'
|
||||
gem 'arabic-letter-connector', require: false
|
||||
gem 'aws-sdk-s3', require: false
|
||||
gem 'aws-sdk-secretsmanager', require: false
|
||||
gem 'azure-storage-blob', require: false
|
||||
gem 'azure-blob', require: false
|
||||
gem 'bootsnap', require: false
|
||||
gem 'cancancan'
|
||||
gem 'csv'
|
||||
gem 'csv', require: false
|
||||
gem 'csv-safe', require: false
|
||||
gem 'devise'
|
||||
gem 'devise-two-factor'
|
||||
gem 'dotenv', require: false
|
||||
@@ -19,31 +21,31 @@ gem 'faraday'
|
||||
gem 'faraday-follow_redirects'
|
||||
gem 'google-cloud-storage', require: false
|
||||
gem 'hexapdf'
|
||||
gem 'image_processing'
|
||||
gem 'jwt'
|
||||
gem 'jwt', require: false
|
||||
gem 'lograge'
|
||||
gem 'mysql2', require: false
|
||||
gem 'oj'
|
||||
gem 'numo-narray-alt', require: false
|
||||
gem 'onnxruntime', require: false
|
||||
gem 'pagy'
|
||||
gem 'pg', require: false
|
||||
gem 'premailer-rails'
|
||||
gem 'pretender'
|
||||
gem 'puma', require: false
|
||||
gem 'rack'
|
||||
gem 'rails'
|
||||
gem 'rails_autolink'
|
||||
gem 'rails-i18n'
|
||||
gem 'rotp'
|
||||
gem 'rqrcode'
|
||||
gem 'rouge', require: false
|
||||
gem 'rqrcode', require: false
|
||||
gem 'ruby-vips'
|
||||
gem 'rubyXL'
|
||||
gem 'rubyzip', require: false
|
||||
gem 'shakapacker'
|
||||
gem 'sidekiq'
|
||||
gem 'sqlite3', require: false
|
||||
gem 'strip_attributes'
|
||||
gem 'trilogy', require: false
|
||||
gem 'turbo-rails'
|
||||
gem 'twitter_cldr', require: false
|
||||
gem 'tzinfo-data'
|
||||
gem 'xlsxtream', require: false
|
||||
|
||||
group :development, :test do
|
||||
gem 'better_html'
|
||||
|
||||
+344
-343
@@ -1,29 +1,31 @@
|
||||
GEM
|
||||
remote: https://rubygems.org/
|
||||
specs:
|
||||
actioncable (8.0.1)
|
||||
actionpack (= 8.0.1)
|
||||
activesupport (= 8.0.1)
|
||||
action_text-trix (2.1.19)
|
||||
railties
|
||||
actioncable (8.1.3.1)
|
||||
actionpack (= 8.1.3.1)
|
||||
activesupport (= 8.1.3.1)
|
||||
nio4r (~> 2.0)
|
||||
websocket-driver (>= 0.6.1)
|
||||
zeitwerk (~> 2.6)
|
||||
actionmailbox (8.0.1)
|
||||
actionpack (= 8.0.1)
|
||||
activejob (= 8.0.1)
|
||||
activerecord (= 8.0.1)
|
||||
activestorage (= 8.0.1)
|
||||
activesupport (= 8.0.1)
|
||||
actionmailbox (8.1.3.1)
|
||||
actionpack (= 8.1.3.1)
|
||||
activejob (= 8.1.3.1)
|
||||
activerecord (= 8.1.3.1)
|
||||
activestorage (= 8.1.3.1)
|
||||
activesupport (= 8.1.3.1)
|
||||
mail (>= 2.8.0)
|
||||
actionmailer (8.0.1)
|
||||
actionpack (= 8.0.1)
|
||||
actionview (= 8.0.1)
|
||||
activejob (= 8.0.1)
|
||||
activesupport (= 8.0.1)
|
||||
actionmailer (8.1.3.1)
|
||||
actionpack (= 8.1.3.1)
|
||||
actionview (= 8.1.3.1)
|
||||
activejob (= 8.1.3.1)
|
||||
activesupport (= 8.1.3.1)
|
||||
mail (>= 2.8.0)
|
||||
rails-dom-testing (~> 2.2)
|
||||
actionpack (8.0.1)
|
||||
actionview (= 8.0.1)
|
||||
activesupport (= 8.0.1)
|
||||
actionpack (8.1.3.1)
|
||||
actionview (= 8.1.3.1)
|
||||
activesupport (= 8.1.3.1)
|
||||
nokogiri (>= 1.8.5)
|
||||
rack (>= 2.2.4)
|
||||
rack-session (>= 1.0.1)
|
||||
@@ -31,97 +33,97 @@ GEM
|
||||
rails-dom-testing (~> 2.2)
|
||||
rails-html-sanitizer (~> 1.6)
|
||||
useragent (~> 0.16)
|
||||
actiontext (8.0.1)
|
||||
actionpack (= 8.0.1)
|
||||
activerecord (= 8.0.1)
|
||||
activestorage (= 8.0.1)
|
||||
activesupport (= 8.0.1)
|
||||
actiontext (8.1.3.1)
|
||||
action_text-trix (~> 2.1.15)
|
||||
actionpack (= 8.1.3.1)
|
||||
activerecord (= 8.1.3.1)
|
||||
activestorage (= 8.1.3.1)
|
||||
activesupport (= 8.1.3.1)
|
||||
globalid (>= 0.6.0)
|
||||
nokogiri (>= 1.8.5)
|
||||
actionview (8.0.1)
|
||||
activesupport (= 8.0.1)
|
||||
actionview (8.1.3.1)
|
||||
activesupport (= 8.1.3.1)
|
||||
builder (~> 3.1)
|
||||
erubi (~> 1.11)
|
||||
rails-dom-testing (~> 2.2)
|
||||
rails-html-sanitizer (~> 1.6)
|
||||
activejob (8.0.1)
|
||||
activesupport (= 8.0.1)
|
||||
activejob (8.1.3.1)
|
||||
activesupport (= 8.1.3.1)
|
||||
globalid (>= 0.3.6)
|
||||
activemodel (8.0.1)
|
||||
activesupport (= 8.0.1)
|
||||
activerecord (8.0.1)
|
||||
activemodel (= 8.0.1)
|
||||
activesupport (= 8.0.1)
|
||||
activemodel (8.1.3.1)
|
||||
activesupport (= 8.1.3.1)
|
||||
activerecord (8.1.3.1)
|
||||
activemodel (= 8.1.3.1)
|
||||
activesupport (= 8.1.3.1)
|
||||
timeout (>= 0.4.0)
|
||||
activestorage (8.0.1)
|
||||
actionpack (= 8.0.1)
|
||||
activejob (= 8.0.1)
|
||||
activerecord (= 8.0.1)
|
||||
activesupport (= 8.0.1)
|
||||
activestorage (8.1.3.1)
|
||||
actionpack (= 8.1.3.1)
|
||||
activejob (= 8.1.3.1)
|
||||
activerecord (= 8.1.3.1)
|
||||
activesupport (= 8.1.3.1)
|
||||
marcel (~> 1.0)
|
||||
activesupport (8.0.1)
|
||||
activesupport (8.1.3.1)
|
||||
base64
|
||||
benchmark (>= 0.3)
|
||||
bigdecimal
|
||||
concurrent-ruby (~> 1.0, >= 1.3.1)
|
||||
connection_pool (>= 2.2.5)
|
||||
drb
|
||||
i18n (>= 1.6, < 2)
|
||||
json
|
||||
logger (>= 1.4.2)
|
||||
minitest (>= 5.1)
|
||||
securerandom (>= 0.3)
|
||||
tzinfo (~> 2.0, >= 2.0.5)
|
||||
uri (>= 0.13.1)
|
||||
addressable (2.8.7)
|
||||
public_suffix (>= 2.0.2, < 7.0)
|
||||
annotaterb (4.14.0)
|
||||
addressable (2.9.0)
|
||||
public_suffix (>= 2.0.2, < 8.0)
|
||||
annotaterb (4.22.0)
|
||||
activerecord (>= 6.0.0)
|
||||
activesupport (>= 6.0.0)
|
||||
arabic-letter-connector (0.1.1)
|
||||
ast (2.4.2)
|
||||
aws-eventstream (1.3.0)
|
||||
aws-partitions (1.1027.0)
|
||||
aws-sdk-core (3.214.0)
|
||||
ast (2.4.3)
|
||||
aws-eventstream (1.4.0)
|
||||
aws-partitions (1.1233.0)
|
||||
aws-sdk-core (3.244.0)
|
||||
aws-eventstream (~> 1, >= 1.3.0)
|
||||
aws-partitions (~> 1, >= 1.992.0)
|
||||
aws-sigv4 (~> 1.9)
|
||||
base64
|
||||
bigdecimal
|
||||
jmespath (~> 1, >= 1.6.1)
|
||||
aws-sdk-kms (1.96.0)
|
||||
aws-sdk-core (~> 3, >= 3.210.0)
|
||||
logger
|
||||
aws-sdk-kms (1.123.0)
|
||||
aws-sdk-core (~> 3, >= 3.244.0)
|
||||
aws-sigv4 (~> 1.5)
|
||||
aws-sdk-s3 (1.176.1)
|
||||
aws-sdk-core (~> 3, >= 3.210.0)
|
||||
aws-sdk-s3 (1.218.0)
|
||||
aws-sdk-core (~> 3, >= 3.244.0)
|
||||
aws-sdk-kms (~> 1)
|
||||
aws-sigv4 (~> 1.5)
|
||||
aws-sdk-secretsmanager (1.110.0)
|
||||
aws-sdk-core (~> 3, >= 3.210.0)
|
||||
aws-sdk-secretsmanager (1.129.0)
|
||||
aws-sdk-core (~> 3, >= 3.244.0)
|
||||
aws-sigv4 (~> 1.5)
|
||||
aws-sigv4 (1.10.1)
|
||||
aws-sigv4 (1.12.1)
|
||||
aws-eventstream (~> 1, >= 1.0.2)
|
||||
azure-storage-blob (2.0.3)
|
||||
azure-storage-common (~> 2.0)
|
||||
nokogiri (~> 1, >= 1.10.8)
|
||||
azure-storage-common (2.0.4)
|
||||
faraday (~> 1.0)
|
||||
faraday_middleware (~> 1.0, >= 1.0.0.rc1)
|
||||
net-http-persistent (~> 4.0)
|
||||
nokogiri (~> 1, >= 1.10.8)
|
||||
base64 (0.2.0)
|
||||
bcrypt (3.1.20)
|
||||
benchmark (0.4.0)
|
||||
better_html (2.1.1)
|
||||
actionview (>= 6.0)
|
||||
activesupport (>= 6.0)
|
||||
azure-blob (0.8.0)
|
||||
cgi
|
||||
rexml
|
||||
base64 (0.3.0)
|
||||
bcrypt (3.1.22)
|
||||
better_html (2.2.0)
|
||||
actionview (>= 7.0)
|
||||
activesupport (>= 7.0)
|
||||
ast (~> 2.0)
|
||||
erubi (~> 1.4)
|
||||
parser (>= 2.4)
|
||||
smart_properties
|
||||
bigdecimal (3.1.8)
|
||||
bigdecimal (4.1.2)
|
||||
bindex (0.8.1)
|
||||
bootsnap (1.18.4)
|
||||
bootsnap (1.23.0)
|
||||
msgpack (~> 1.2)
|
||||
brakeman (7.0.0)
|
||||
brakeman (8.0.4)
|
||||
racc
|
||||
builder (3.3.0)
|
||||
bullet (8.0.0)
|
||||
bullet (8.1.0)
|
||||
activesupport (>= 3.0.0)
|
||||
uniform_notifier (~> 1.11)
|
||||
camertron-eprun (1.1.1)
|
||||
@@ -135,48 +137,50 @@ GEM
|
||||
rack-test (>= 0.6.3)
|
||||
regexp_parser (>= 1.5, < 3.0)
|
||||
xpath (~> 3.2)
|
||||
cgi (0.5.1)
|
||||
childprocess (5.1.0)
|
||||
logger (~> 1.5)
|
||||
chunky_png (1.4.0)
|
||||
cldr-plurals-runtime-rb (1.1.0)
|
||||
cmdparse (3.0.7)
|
||||
coderay (1.1.3)
|
||||
concurrent-ruby (1.3.4)
|
||||
connection_pool (2.4.1)
|
||||
crack (1.0.0)
|
||||
concurrent-ruby (1.3.8)
|
||||
connection_pool (3.0.2)
|
||||
crack (1.0.1)
|
||||
bigdecimal
|
||||
rexml
|
||||
crass (1.0.6)
|
||||
css_parser (1.21.0)
|
||||
addressable
|
||||
csv (3.3.2)
|
||||
cuprite (0.15.1)
|
||||
crass (1.0.7)
|
||||
csv (3.3.5)
|
||||
csv-safe (3.3.1)
|
||||
csv (~> 3.0)
|
||||
cuprite (0.17)
|
||||
capybara (~> 3.0)
|
||||
ferrum (~> 0.15.0)
|
||||
date (3.4.1)
|
||||
debug (1.10.0)
|
||||
ferrum (~> 0.17.0)
|
||||
date (3.5.1)
|
||||
debug (1.11.1)
|
||||
irb (~> 1.10)
|
||||
reline (>= 0.3.8)
|
||||
declarative (0.0.20)
|
||||
devise (4.9.4)
|
||||
devise (5.0.4)
|
||||
bcrypt (~> 3.0)
|
||||
orm_adapter (~> 0.1)
|
||||
railties (>= 4.1.0)
|
||||
railties (>= 7.0)
|
||||
responders
|
||||
warden (~> 1.2.3)
|
||||
devise-two-factor (6.1.0)
|
||||
activesupport (>= 7.0, < 8.1)
|
||||
devise (~> 4.0)
|
||||
railties (>= 7.0, < 8.1)
|
||||
devise-two-factor (6.4.0)
|
||||
activesupport (>= 7.2, < 8.2)
|
||||
devise (>= 4.0, < 6.0)
|
||||
railties (>= 7.2, < 8.2)
|
||||
rotp (~> 6.0)
|
||||
diff-lcs (1.5.1)
|
||||
digest-crc (0.6.5)
|
||||
diff-lcs (1.6.2)
|
||||
digest-crc (0.7.0)
|
||||
rake (>= 12.0.0, < 14.0.0)
|
||||
docile (1.4.1)
|
||||
dotenv (3.1.7)
|
||||
drb (2.2.1)
|
||||
dotenv (3.2.0)
|
||||
drb (2.2.3)
|
||||
email_typo (0.2.3)
|
||||
erb_lint (0.7.0)
|
||||
erb (6.0.6)
|
||||
erb_lint (0.9.0)
|
||||
activesupport
|
||||
better_html (>= 2.0.1)
|
||||
parser (>= 2.7.1.4)
|
||||
@@ -184,115 +188,97 @@ GEM
|
||||
rubocop (>= 1)
|
||||
smart_properties
|
||||
erubi (1.13.1)
|
||||
factory_bot (6.5.0)
|
||||
activesupport (>= 5.0.0)
|
||||
factory_bot_rails (6.4.4)
|
||||
factory_bot (6.5.6)
|
||||
activesupport (>= 6.1.0)
|
||||
factory_bot_rails (6.5.1)
|
||||
factory_bot (~> 6.5)
|
||||
railties (>= 5.0.0)
|
||||
faker (3.5.1)
|
||||
railties (>= 6.1.0)
|
||||
faker (3.6.1)
|
||||
i18n (>= 1.8.11, < 2)
|
||||
faraday (1.10.4)
|
||||
faraday-em_http (~> 1.0)
|
||||
faraday-em_synchrony (~> 1.0)
|
||||
faraday-excon (~> 1.1)
|
||||
faraday-httpclient (~> 1.0)
|
||||
faraday-multipart (~> 1.0)
|
||||
faraday-net_http (~> 1.0)
|
||||
faraday-net_http_persistent (~> 1.0)
|
||||
faraday-patron (~> 1.0)
|
||||
faraday-rack (~> 1.0)
|
||||
faraday-retry (~> 1.0)
|
||||
ruby2_keywords (>= 0.0.4)
|
||||
faraday-em_http (1.0.0)
|
||||
faraday-em_synchrony (1.0.0)
|
||||
faraday-excon (1.1.0)
|
||||
faraday-follow_redirects (0.3.0)
|
||||
faraday (2.14.3)
|
||||
faraday-net_http (>= 2.0, < 3.5)
|
||||
json
|
||||
logger
|
||||
faraday-follow_redirects (0.5.0)
|
||||
faraday (>= 1, < 3)
|
||||
faraday-httpclient (1.0.1)
|
||||
faraday-multipart (1.1.0)
|
||||
multipart-post (~> 2.0)
|
||||
faraday-net_http (1.0.2)
|
||||
faraday-net_http_persistent (1.2.0)
|
||||
faraday-patron (1.0.0)
|
||||
faraday-rack (1.0.0)
|
||||
faraday-retry (1.0.3)
|
||||
faraday_middleware (1.2.1)
|
||||
faraday (~> 1.0)
|
||||
ferrum (0.15)
|
||||
faraday-net_http (3.4.4)
|
||||
net-http (~> 0.5)
|
||||
ferrum (0.17.2)
|
||||
addressable (~> 2.5)
|
||||
base64 (~> 0.2)
|
||||
concurrent-ruby (~> 1.1)
|
||||
webrick (~> 1.7)
|
||||
websocket-driver (~> 0.7)
|
||||
ffi (1.17.1)
|
||||
ffi (1.17.1-aarch64-linux-gnu)
|
||||
ffi (1.17.1-aarch64-linux-musl)
|
||||
ffi (1.17.1-arm64-darwin)
|
||||
ffi (1.17.1-x86_64-linux-gnu)
|
||||
ffi (1.17.1-x86_64-linux-musl)
|
||||
foreman (0.88.1)
|
||||
ffi (1.17.4-aarch64-linux-gnu)
|
||||
ffi (1.17.4-aarch64-linux-musl)
|
||||
ffi (1.17.4-arm64-darwin)
|
||||
ffi (1.17.4-x86_64-linux-gnu)
|
||||
ffi (1.17.4-x86_64-linux-musl)
|
||||
foreman (0.90.0)
|
||||
thor (~> 1.4)
|
||||
geom2d (0.4.1)
|
||||
globalid (1.2.1)
|
||||
globalid (1.4.0)
|
||||
activesupport (>= 6.1)
|
||||
google-apis-core (0.15.1)
|
||||
addressable (~> 2.5, >= 2.5.1)
|
||||
googleauth (~> 1.9)
|
||||
httpclient (>= 2.8.3, < 3.a)
|
||||
mini_mime (~> 1.0)
|
||||
mutex_m
|
||||
google-apis-core (1.0.2)
|
||||
addressable (~> 2.8, >= 2.8.7)
|
||||
faraday (~> 2.13)
|
||||
faraday-follow_redirects (~> 0.3)
|
||||
googleauth (~> 1.14)
|
||||
mini_mime (~> 1.1)
|
||||
representable (~> 3.0)
|
||||
retriable (>= 2.0, < 4.a)
|
||||
google-apis-iamcredentials_v1 (0.22.0)
|
||||
retriable (~> 3.1)
|
||||
google-apis-iamcredentials_v1 (0.26.0)
|
||||
google-apis-core (>= 0.15.0, < 2.a)
|
||||
google-apis-storage_v1 (0.49.0)
|
||||
google-apis-storage_v1 (0.61.0)
|
||||
google-apis-core (>= 0.15.0, < 2.a)
|
||||
google-cloud-core (1.7.1)
|
||||
google-cloud-core (1.8.0)
|
||||
google-cloud-env (>= 1.0, < 3.a)
|
||||
google-cloud-errors (~> 1.0)
|
||||
google-cloud-env (2.2.1)
|
||||
google-cloud-env (2.3.1)
|
||||
base64 (~> 0.2)
|
||||
faraday (>= 1.0, < 3.a)
|
||||
google-cloud-errors (1.4.0)
|
||||
google-cloud-storage (1.54.0)
|
||||
google-cloud-errors (1.6.0)
|
||||
google-cloud-storage (1.59.0)
|
||||
addressable (~> 2.8)
|
||||
digest-crc (~> 0.4)
|
||||
google-apis-core (~> 0.13)
|
||||
google-apis-core (>= 0.18, < 2)
|
||||
google-apis-iamcredentials_v1 (~> 0.18)
|
||||
google-apis-storage_v1 (~> 0.38)
|
||||
google-apis-storage_v1 (>= 0.42)
|
||||
google-cloud-core (~> 1.6)
|
||||
googleauth (~> 1.9)
|
||||
mini_mime (~> 1.0)
|
||||
google-logging-utils (0.1.0)
|
||||
googleauth (1.12.2)
|
||||
google-logging-utils (0.2.0)
|
||||
googleauth (1.16.2)
|
||||
faraday (>= 1.0, < 3.a)
|
||||
google-cloud-env (~> 2.2)
|
||||
google-logging-utils (~> 0.1)
|
||||
jwt (>= 1.4, < 3.0)
|
||||
jwt (>= 1.4, < 4.0)
|
||||
multi_json (~> 1.11)
|
||||
os (>= 0.9, < 2.0)
|
||||
signet (>= 0.16, < 2.a)
|
||||
hashdiff (1.1.2)
|
||||
hexapdf (1.0.3)
|
||||
hashdiff (1.2.1)
|
||||
hexapdf (1.7.0)
|
||||
cmdparse (~> 3.0, >= 3.0.3)
|
||||
geom2d (~> 0.4, >= 0.4.1)
|
||||
openssl (>= 2.2.1)
|
||||
htmlentities (4.3.4)
|
||||
httpclient (2.8.3)
|
||||
i18n (1.14.6)
|
||||
strscan (>= 3.1.2)
|
||||
i18n (1.15.2)
|
||||
concurrent-ruby (~> 1.0)
|
||||
image_processing (1.13.0)
|
||||
mini_magick (>= 4.9.5, < 5)
|
||||
ruby-vips (>= 2.0.17, < 3)
|
||||
io-console (0.8.0)
|
||||
irb (1.14.3)
|
||||
io-console (0.8.2)
|
||||
irb (1.18.0)
|
||||
pp (>= 0.6.0)
|
||||
prism (>= 1.3.0)
|
||||
rdoc (>= 4.0.0)
|
||||
reline (>= 0.4.2)
|
||||
jmespath (1.6.2)
|
||||
json (2.9.1)
|
||||
jwt (2.9.3)
|
||||
json (2.21.2)
|
||||
jwt (3.2.0)
|
||||
base64
|
||||
language_server-protocol (3.17.0.3)
|
||||
launchy (3.0.1)
|
||||
language_server-protocol (3.17.0.5)
|
||||
launchy (3.1.1)
|
||||
addressable (~> 2.8)
|
||||
childprocess (~> 5.0)
|
||||
logger (~> 1.6)
|
||||
letter_opener (1.10.0)
|
||||
launchy (>= 2.2, < 4)
|
||||
letter_opener_web (3.0.0)
|
||||
@@ -300,146 +286,151 @@ GEM
|
||||
letter_opener (~> 1.9)
|
||||
railties (>= 6.1)
|
||||
rexml
|
||||
logger (1.6.4)
|
||||
lint_roller (1.1.0)
|
||||
logger (1.7.0)
|
||||
lograge (0.14.0)
|
||||
actionpack (>= 4)
|
||||
activesupport (>= 4)
|
||||
railties (>= 4)
|
||||
request_store (~> 1.0)
|
||||
loofah (2.23.1)
|
||||
loofah (2.25.2)
|
||||
crass (~> 1.0.2)
|
||||
nokogiri (>= 1.12.0)
|
||||
mail (2.8.1)
|
||||
mail (2.9.1)
|
||||
logger
|
||||
mini_mime (>= 0.1.1)
|
||||
net-imap
|
||||
net-pop
|
||||
net-smtp
|
||||
marcel (1.0.4)
|
||||
matrix (0.4.2)
|
||||
marcel (1.2.1)
|
||||
matrix (0.4.3)
|
||||
method_source (1.1.0)
|
||||
mini_magick (4.13.2)
|
||||
mini_mime (1.1.5)
|
||||
mini_portile2 (2.8.8)
|
||||
minitest (5.25.4)
|
||||
msgpack (1.7.5)
|
||||
multi_json (1.15.0)
|
||||
multipart-post (2.4.1)
|
||||
mutex_m (0.3.0)
|
||||
mysql2 (0.5.6)
|
||||
net-http-persistent (4.0.5)
|
||||
connection_pool (~> 2.2)
|
||||
net-imap (0.5.6)
|
||||
minitest (6.0.6)
|
||||
drb (~> 2.0)
|
||||
prism (~> 1.5)
|
||||
msgpack (1.8.4)
|
||||
multi_json (1.19.1)
|
||||
net-http (0.9.1)
|
||||
uri (>= 0.11.1)
|
||||
net-imap (0.6.6)
|
||||
date
|
||||
net-protocol
|
||||
net-pop (0.1.2)
|
||||
net-protocol
|
||||
net-protocol (0.2.2)
|
||||
timeout
|
||||
net-smtp (0.5.0)
|
||||
net-smtp (0.5.1)
|
||||
net-protocol
|
||||
nio4r (2.7.4)
|
||||
nokogiri (1.18.8)
|
||||
mini_portile2 (~> 2.8.2)
|
||||
nio4r (2.7.5)
|
||||
nokogiri (1.19.4-aarch64-linux-gnu)
|
||||
racc (~> 1.4)
|
||||
nokogiri (1.18.8-aarch64-linux-gnu)
|
||||
nokogiri (1.19.4-aarch64-linux-musl)
|
||||
racc (~> 1.4)
|
||||
nokogiri (1.18.8-aarch64-linux-musl)
|
||||
nokogiri (1.19.4-arm64-darwin)
|
||||
racc (~> 1.4)
|
||||
nokogiri (1.18.8-arm64-darwin)
|
||||
nokogiri (1.19.4-x86_64-linux-gnu)
|
||||
racc (~> 1.4)
|
||||
nokogiri (1.18.8-x86_64-linux-gnu)
|
||||
nokogiri (1.19.4-x86_64-linux-musl)
|
||||
racc (~> 1.4)
|
||||
nokogiri (1.18.8-x86_64-linux-musl)
|
||||
racc (~> 1.4)
|
||||
oj (3.16.8)
|
||||
bigdecimal (>= 3.0)
|
||||
ostruct (>= 0.2)
|
||||
openssl (3.3.0)
|
||||
numo-narray-alt (0.10.3)
|
||||
onnxruntime (0.10.1-aarch64-linux)
|
||||
ffi
|
||||
onnxruntime (0.10.1-arm64-darwin)
|
||||
ffi
|
||||
onnxruntime (0.10.1-x86_64-linux)
|
||||
ffi
|
||||
openssl (4.0.1)
|
||||
orm_adapter (0.5.0)
|
||||
os (1.1.4)
|
||||
ostruct (0.6.1)
|
||||
package_json (0.1.0)
|
||||
pagy (9.3.3)
|
||||
parallel (1.26.3)
|
||||
parser (3.3.6.0)
|
||||
package_json (0.2.0)
|
||||
pagy (43.6.1)
|
||||
json
|
||||
uri
|
||||
yaml
|
||||
parallel (1.28.0)
|
||||
parser (3.3.11.1)
|
||||
ast (~> 2.4.1)
|
||||
racc
|
||||
pg (1.5.9)
|
||||
premailer (1.27.0)
|
||||
addressable
|
||||
css_parser (>= 1.19.0)
|
||||
htmlentities (>= 4.0.0)
|
||||
premailer-rails (1.12.0)
|
||||
actionmailer (>= 3)
|
||||
net-smtp
|
||||
premailer (~> 1.7, >= 1.7.9)
|
||||
pretender (0.5.0)
|
||||
actionpack (>= 6.1)
|
||||
pry (0.15.0)
|
||||
pg (1.6.3-aarch64-linux)
|
||||
pg (1.6.3-aarch64-linux-musl)
|
||||
pg (1.6.3-arm64-darwin)
|
||||
pg (1.6.3-x86_64-linux)
|
||||
pg (1.6.3-x86_64-linux-musl)
|
||||
pp (0.6.4)
|
||||
prettyprint
|
||||
pretender (1.0.0)
|
||||
actionpack (>= 7.2)
|
||||
prettyprint (0.2.0)
|
||||
prism (1.9.0)
|
||||
pry (0.16.0)
|
||||
coderay (~> 1.1)
|
||||
method_source (~> 1.0)
|
||||
reline (>= 0.6.0)
|
||||
pry-rails (0.3.11)
|
||||
pry (>= 0.13.0)
|
||||
psych (5.2.2)
|
||||
date
|
||||
stringio
|
||||
public_suffix (6.0.1)
|
||||
puma (6.5.0)
|
||||
public_suffix (7.0.5)
|
||||
puma (8.0.2)
|
||||
nio4r (~> 2.0)
|
||||
racc (1.8.1)
|
||||
rack (3.1.16)
|
||||
rack (3.2.6)
|
||||
rack-proxy (0.7.7)
|
||||
rack
|
||||
rack-session (2.0.0)
|
||||
rack-session (2.1.2)
|
||||
base64 (>= 0.1.0)
|
||||
rack (>= 3.0.0)
|
||||
rack-test (2.1.0)
|
||||
rack-test (2.2.0)
|
||||
rack (>= 1.3)
|
||||
rackup (2.2.1)
|
||||
rackup (2.3.1)
|
||||
rack (>= 3)
|
||||
rails (8.0.1)
|
||||
actioncable (= 8.0.1)
|
||||
actionmailbox (= 8.0.1)
|
||||
actionmailer (= 8.0.1)
|
||||
actionpack (= 8.0.1)
|
||||
actiontext (= 8.0.1)
|
||||
actionview (= 8.0.1)
|
||||
activejob (= 8.0.1)
|
||||
activemodel (= 8.0.1)
|
||||
activerecord (= 8.0.1)
|
||||
activestorage (= 8.0.1)
|
||||
activesupport (= 8.0.1)
|
||||
rails (8.1.3.1)
|
||||
actioncable (= 8.1.3.1)
|
||||
actionmailbox (= 8.1.3.1)
|
||||
actionmailer (= 8.1.3.1)
|
||||
actionpack (= 8.1.3.1)
|
||||
actiontext (= 8.1.3.1)
|
||||
actionview (= 8.1.3.1)
|
||||
activejob (= 8.1.3.1)
|
||||
activemodel (= 8.1.3.1)
|
||||
activerecord (= 8.1.3.1)
|
||||
activestorage (= 8.1.3.1)
|
||||
activesupport (= 8.1.3.1)
|
||||
bundler (>= 1.15.0)
|
||||
railties (= 8.0.1)
|
||||
rails-dom-testing (2.2.0)
|
||||
railties (= 8.1.3.1)
|
||||
rails-dom-testing (2.3.0)
|
||||
activesupport (>= 5.0.0)
|
||||
minitest
|
||||
nokogiri (>= 1.6)
|
||||
rails-html-sanitizer (1.6.2)
|
||||
loofah (~> 2.21)
|
||||
rails-html-sanitizer (1.7.1)
|
||||
loofah (~> 2.25, >= 2.25.2)
|
||||
nokogiri (>= 1.15.7, != 1.16.7, != 1.16.6, != 1.16.5, != 1.16.4, != 1.16.3, != 1.16.2, != 1.16.1, != 1.16.0.rc1, != 1.16.0)
|
||||
rails-i18n (8.0.1)
|
||||
rails-i18n (8.1.0)
|
||||
i18n (>= 0.7, < 2)
|
||||
railties (>= 8.0.0, < 9)
|
||||
rails_autolink (1.1.8)
|
||||
actionview (> 3.1)
|
||||
activesupport (> 3.1)
|
||||
railties (> 3.1)
|
||||
railties (8.0.1)
|
||||
actionpack (= 8.0.1)
|
||||
activesupport (= 8.0.1)
|
||||
railties (8.1.3.1)
|
||||
actionpack (= 8.1.3.1)
|
||||
activesupport (= 8.1.3.1)
|
||||
irb (~> 1.13)
|
||||
rackup (>= 1.0.0)
|
||||
rake (>= 12.2)
|
||||
thor (~> 1.0, >= 1.2.2)
|
||||
tsort (>= 0.2)
|
||||
zeitwerk (~> 2.6)
|
||||
rainbow (3.1.1)
|
||||
rake (13.2.1)
|
||||
rdoc (6.10.0)
|
||||
psych (>= 4.0.0)
|
||||
redis-client (0.23.0)
|
||||
rake (13.4.2)
|
||||
rbs (4.1.0)
|
||||
logger
|
||||
prism (>= 1.6.0)
|
||||
tsort
|
||||
rdoc (8.0.0)
|
||||
erb
|
||||
prism (>= 1.6.0)
|
||||
rbs (>= 4.0.0)
|
||||
tsort
|
||||
redis-client (0.29.0)
|
||||
connection_pool
|
||||
regexp_parser (2.9.3)
|
||||
reline (0.6.0)
|
||||
regexp_parser (2.11.3)
|
||||
reline (0.6.3)
|
||||
io-console (~> 0.5)
|
||||
representable (3.2.0)
|
||||
declarative (< 0.1.0)
|
||||
@@ -447,153 +438,162 @@ GEM
|
||||
uber (< 0.2.0)
|
||||
request_store (1.7.0)
|
||||
rack (>= 1.4)
|
||||
responders (3.1.1)
|
||||
actionpack (>= 5.2)
|
||||
railties (>= 5.2)
|
||||
retriable (3.1.2)
|
||||
rexml (3.4.0)
|
||||
rotp (6.3.0)
|
||||
rqrcode (2.2.0)
|
||||
chunky_png (~> 1.0)
|
||||
rqrcode_core (~> 1.0)
|
||||
rqrcode_core (1.2.0)
|
||||
rspec-core (3.13.2)
|
||||
rspec-support (~> 3.13.0)
|
||||
rspec-expectations (3.13.3)
|
||||
diff-lcs (>= 1.2.0, < 2.0)
|
||||
rspec-support (~> 3.13.0)
|
||||
rspec-mocks (3.13.2)
|
||||
diff-lcs (>= 1.2.0, < 2.0)
|
||||
rspec-support (~> 3.13.0)
|
||||
rspec-rails (7.1.0)
|
||||
responders (3.2.0)
|
||||
actionpack (>= 7.0)
|
||||
activesupport (>= 7.0)
|
||||
railties (>= 7.0)
|
||||
rspec-core (~> 3.13)
|
||||
rspec-expectations (~> 3.13)
|
||||
rspec-mocks (~> 3.13)
|
||||
rspec-support (~> 3.13)
|
||||
rspec-support (3.13.2)
|
||||
rubocop (1.69.2)
|
||||
retriable (3.4.1)
|
||||
rexml (3.4.4)
|
||||
rotp (6.3.0)
|
||||
rouge (4.7.0)
|
||||
rqrcode (3.2.0)
|
||||
chunky_png (~> 1.0)
|
||||
rqrcode_core (~> 2.0)
|
||||
rqrcode_core (2.1.0)
|
||||
rspec-core (3.13.6)
|
||||
rspec-support (~> 3.13.0)
|
||||
rspec-expectations (3.13.5)
|
||||
diff-lcs (>= 1.2.0, < 2.0)
|
||||
rspec-support (~> 3.13.0)
|
||||
rspec-mocks (3.13.8)
|
||||
diff-lcs (>= 1.2.0, < 2.0)
|
||||
rspec-support (~> 3.13.0)
|
||||
rspec-rails (8.0.4)
|
||||
actionpack (>= 7.2)
|
||||
activesupport (>= 7.2)
|
||||
railties (>= 7.2)
|
||||
rspec-core (>= 3.13.0, < 5.0.0)
|
||||
rspec-expectations (>= 3.13.0, < 5.0.0)
|
||||
rspec-mocks (>= 3.13.0, < 5.0.0)
|
||||
rspec-support (>= 3.13.0, < 5.0.0)
|
||||
rspec-support (3.13.7)
|
||||
rubocop (1.86.0)
|
||||
json (~> 2.3)
|
||||
language_server-protocol (>= 3.17.0)
|
||||
language_server-protocol (~> 3.17.0.2)
|
||||
lint_roller (~> 1.1.0)
|
||||
parallel (~> 1.10)
|
||||
parser (>= 3.3.0.2)
|
||||
rainbow (>= 2.2.2, < 4.0)
|
||||
regexp_parser (>= 2.9.3, < 3.0)
|
||||
rubocop-ast (>= 1.36.2, < 2.0)
|
||||
rubocop-ast (>= 1.49.0, < 2.0)
|
||||
ruby-progressbar (~> 1.7)
|
||||
unicode-display_width (>= 2.4.0, < 4.0)
|
||||
rubocop-ast (1.37.0)
|
||||
parser (>= 3.3.1.0)
|
||||
rubocop-performance (1.23.0)
|
||||
rubocop (>= 1.48.1, < 2.0)
|
||||
rubocop-ast (>= 1.31.1, < 2.0)
|
||||
rubocop-rails (2.27.0)
|
||||
rubocop-ast (1.49.1)
|
||||
parser (>= 3.3.7.2)
|
||||
prism (~> 1.7)
|
||||
rubocop-performance (1.26.1)
|
||||
lint_roller (~> 1.1)
|
||||
rubocop (>= 1.75.0, < 2.0)
|
||||
rubocop-ast (>= 1.47.1, < 2.0)
|
||||
rubocop-rails (2.34.3)
|
||||
activesupport (>= 4.2.0)
|
||||
lint_roller (~> 1.1)
|
||||
rack (>= 1.1)
|
||||
rubocop (>= 1.52.0, < 2.0)
|
||||
rubocop-ast (>= 1.31.1, < 2.0)
|
||||
rubocop-rspec (3.3.0)
|
||||
rubocop (~> 1.61)
|
||||
rubocop (>= 1.75.0, < 2.0)
|
||||
rubocop-ast (>= 1.44.0, < 2.0)
|
||||
rubocop-rspec (3.9.0)
|
||||
lint_roller (~> 1.1)
|
||||
rubocop (~> 1.81)
|
||||
ruby-progressbar (1.13.0)
|
||||
ruby-vips (2.2.2)
|
||||
ruby-vips (2.3.0)
|
||||
ffi (~> 1.12)
|
||||
logger
|
||||
ruby2_keywords (0.0.5)
|
||||
rubyXL (3.4.33)
|
||||
nokogiri (>= 1.10.8)
|
||||
rubyzip (>= 1.3.0)
|
||||
rubyzip (2.3.2)
|
||||
rubyzip (3.2.2)
|
||||
securerandom (0.4.1)
|
||||
semantic_range (3.1.0)
|
||||
shakapacker (8.0.2)
|
||||
semantic_range (3.1.1)
|
||||
shakapacker (9.7.0)
|
||||
activesupport (>= 5.2)
|
||||
package_json
|
||||
rack-proxy (>= 0.6.1)
|
||||
railties (>= 5.2)
|
||||
semantic_range (>= 2.3.0)
|
||||
sidekiq (7.3.7)
|
||||
connection_pool (>= 2.3.0)
|
||||
logger
|
||||
rack (>= 2.2.4)
|
||||
redis-client (>= 0.22.2)
|
||||
signet (0.19.0)
|
||||
sidekiq (8.1.6)
|
||||
connection_pool (>= 3.0.0)
|
||||
json (>= 2.16.0)
|
||||
logger (>= 1.7.0)
|
||||
rack (>= 3.2.0)
|
||||
redis-client (>= 0.29.0)
|
||||
signet (0.21.0)
|
||||
addressable (~> 2.8)
|
||||
faraday (>= 0.17.5, < 3.a)
|
||||
jwt (>= 1.5, < 3.0)
|
||||
jwt (>= 1.5, < 4.0)
|
||||
multi_json (~> 1.10)
|
||||
simplecov (0.22.0)
|
||||
docile (~> 1.1)
|
||||
simplecov-html (~> 0.11)
|
||||
simplecov_json_formatter (~> 0.1)
|
||||
simplecov-html (0.13.1)
|
||||
simplecov-html (0.13.2)
|
||||
simplecov_json_formatter (0.1.4)
|
||||
smart_properties (1.17.0)
|
||||
sqlite3 (2.5.0)
|
||||
mini_portile2 (~> 2.8.0)
|
||||
sqlite3 (2.5.0-aarch64-linux-gnu)
|
||||
sqlite3 (2.5.0-aarch64-linux-musl)
|
||||
sqlite3 (2.5.0-arm64-darwin)
|
||||
sqlite3 (2.5.0-x86_64-linux-gnu)
|
||||
sqlite3 (2.5.0-x86_64-linux-musl)
|
||||
stringio (3.1.2)
|
||||
strip_attributes (1.14.1)
|
||||
sqlite3 (2.9.5-aarch64-linux-gnu)
|
||||
sqlite3 (2.9.5-aarch64-linux-musl)
|
||||
sqlite3 (2.9.5-arm64-darwin)
|
||||
sqlite3 (2.9.5-x86_64-linux-gnu)
|
||||
sqlite3 (2.9.5-x86_64-linux-musl)
|
||||
strip_attributes (2.0.1)
|
||||
activemodel (>= 3.0, < 9.0)
|
||||
thor (1.3.2)
|
||||
timeout (0.4.3)
|
||||
strscan (3.1.8)
|
||||
thor (1.5.0)
|
||||
timeout (0.6.1)
|
||||
trailblazer-option (0.1.2)
|
||||
turbo-rails (2.0.11)
|
||||
actionpack (>= 6.0.0)
|
||||
railties (>= 6.0.0)
|
||||
twitter_cldr (6.12.1)
|
||||
trilogy (2.12.2)
|
||||
bigdecimal
|
||||
tsort (0.2.0)
|
||||
turbo-rails (2.0.23)
|
||||
actionpack (>= 7.1.0)
|
||||
railties (>= 7.1.0)
|
||||
twitter_cldr (6.14.0)
|
||||
base64
|
||||
camertron-eprun
|
||||
cldr-plurals-runtime-rb (~> 1.1)
|
||||
tzinfo
|
||||
tzinfo (2.0.6)
|
||||
concurrent-ruby (~> 1.0)
|
||||
tzinfo-data (1.2024.2)
|
||||
tzinfo-data (1.2026.1)
|
||||
tzinfo (>= 1.0.0)
|
||||
uber (0.1.0)
|
||||
unicode-display_width (3.1.2)
|
||||
unicode-emoji (~> 4.0, >= 4.0.4)
|
||||
unicode-emoji (4.0.4)
|
||||
uniform_notifier (1.16.0)
|
||||
uri (1.0.3)
|
||||
unicode-display_width (3.2.0)
|
||||
unicode-emoji (~> 4.1)
|
||||
unicode-emoji (4.2.0)
|
||||
uniform_notifier (1.18.0)
|
||||
uri (1.1.1)
|
||||
useragent (0.16.11)
|
||||
warden (1.2.9)
|
||||
rack (>= 2.0.9)
|
||||
web-console (4.2.1)
|
||||
actionview (>= 6.0.0)
|
||||
activemodel (>= 6.0.0)
|
||||
web-console (4.3.0)
|
||||
actionview (>= 8.0.0)
|
||||
bindex (>= 0.4.0)
|
||||
railties (>= 6.0.0)
|
||||
webmock (3.24.0)
|
||||
railties (>= 8.0.0)
|
||||
webmock (3.26.2)
|
||||
addressable (>= 2.8.0)
|
||||
crack (>= 0.3.2)
|
||||
hashdiff (>= 0.4.0, < 2.0.0)
|
||||
webrick (1.9.1)
|
||||
websocket-driver (0.7.6)
|
||||
webrick (1.9.2)
|
||||
websocket-driver (0.8.2)
|
||||
base64
|
||||
websocket-extensions (>= 0.1.0)
|
||||
websocket-extensions (0.1.5)
|
||||
xlsxtream (3.1.0)
|
||||
zip_kit (>= 6.2, < 7)
|
||||
xpath (3.2.0)
|
||||
nokogiri (~> 1.8)
|
||||
zeitwerk (2.7.1)
|
||||
yaml (0.4.0)
|
||||
zeitwerk (2.8.2)
|
||||
zip_kit (6.3.4)
|
||||
|
||||
PLATFORMS
|
||||
aarch64-linux
|
||||
aarch64-linux-musl
|
||||
arm64-darwin
|
||||
ruby
|
||||
x86_64-linux
|
||||
x86_64-linux-musl
|
||||
|
||||
DEPENDENCIES
|
||||
addressable
|
||||
annotaterb
|
||||
arabic-letter-connector
|
||||
aws-sdk-s3
|
||||
aws-sdk-secretsmanager
|
||||
azure-storage-blob
|
||||
azure-blob
|
||||
better_html
|
||||
bootsnap
|
||||
brakeman
|
||||
@@ -601,6 +601,7 @@ DEPENDENCIES
|
||||
cancancan
|
||||
capybara
|
||||
csv
|
||||
csv-safe
|
||||
cuprite
|
||||
debug
|
||||
devise
|
||||
@@ -615,23 +616,21 @@ DEPENDENCIES
|
||||
foreman
|
||||
google-cloud-storage
|
||||
hexapdf
|
||||
image_processing
|
||||
jwt
|
||||
letter_opener_web
|
||||
lograge
|
||||
mysql2
|
||||
oj
|
||||
numo-narray-alt
|
||||
onnxruntime
|
||||
pagy
|
||||
pg
|
||||
premailer-rails
|
||||
pretender
|
||||
pry-rails
|
||||
puma
|
||||
rack
|
||||
rails
|
||||
rails-i18n
|
||||
rails_autolink
|
||||
rotp
|
||||
rouge
|
||||
rqrcode
|
||||
rspec-rails
|
||||
rubocop
|
||||
@@ -639,20 +638,22 @@ DEPENDENCIES
|
||||
rubocop-rails
|
||||
rubocop-rspec
|
||||
ruby-vips
|
||||
rubyXL
|
||||
rubyzip
|
||||
shakapacker
|
||||
sidekiq
|
||||
simplecov
|
||||
sqlite3
|
||||
strip_attributes
|
||||
trilogy
|
||||
turbo-rails
|
||||
twitter_cldr
|
||||
tzinfo-data
|
||||
web-console
|
||||
webmock
|
||||
xlsxtream
|
||||
|
||||
RUBY VERSION
|
||||
ruby 3.4.2p28
|
||||
ruby 4.0.5
|
||||
|
||||
BUNDLED WITH
|
||||
2.5.3
|
||||
4.0.3
|
||||
@@ -0,0 +1,5 @@
|
||||
Additional Terms
|
||||
|
||||
In accordance with Section 7(b) of the GNU Affero General Public License,
|
||||
a covered work must retain the original DocuSeal attribution in interactive
|
||||
user interfaces.
|
||||
@@ -1,7 +1,7 @@
|
||||
<h1 align="center" style="border-bottom: none">
|
||||
<div>
|
||||
<a href="https://www.docuseal.com">
|
||||
<img alt="DocuSeal" src="https://github.com/docusealco/docuseal/assets/5418788/c12cd051-81cd-4402-bc3a-92f2cfdc1b06" width="80" />
|
||||
<img alt="DocuSeal" src="https://github.com/user-attachments/assets/38b45682-ffa4-4919-abde-d2d422325c44" width="80" />
|
||||
<br>
|
||||
</a>
|
||||
DocuSeal
|
||||
@@ -42,7 +42,7 @@ DocuSeal is an open source platform that provides secure and efficient digital d
|
||||
- PDF signature verification
|
||||
- Users management
|
||||
- Mobile-optimized
|
||||
- 6 UI languages with signing available in 14 languages
|
||||
- 7 UI languages with signing available in 14 languages
|
||||
- API and Webhooks for integrations
|
||||
- Easy to deploy in minutes
|
||||
|
||||
@@ -50,7 +50,7 @@ DocuSeal is an open source platform that provides secure and efficient digital d
|
||||
- Company logo and white-label
|
||||
- User roles
|
||||
- Automated reminders
|
||||
- Invitation and identify verification via SMS
|
||||
- Invitation and identity verification via SMS
|
||||
- Conditional fields and formulas
|
||||
- Bulk send with CSV, XLSX spreadsheet import
|
||||
- SSO / SAML
|
||||
@@ -64,7 +64,7 @@ DocuSeal is an open source platform that provides secure and efficient digital d
|
||||
|
||||
|Heroku|Railway|
|
||||
|:--:|:---:|
|
||||
| [<img alt="Deploy on Heroku" src="https://www.herokucdn.com/deploy/button.svg" height="40">](https://heroku.com/deploy?template=https://github.com/docusealco/docuseal-heroku) | [<img alt="Deploy on Railway" src="https://railway.app/button.svg" height="40">](https://railway.app/template/IGoDnc?referralCode=ruU7JR)|
|
||||
| [<img alt="Deploy on Heroku" src="https://www.herokucdn.com/deploy/button.svg" height="40">](https://heroku.com/deploy?template=https://github.com/docusealco/docuseal-heroku) | [<img alt="Deploy on Railway" src="https://railway.app/button.svg" height="40">](https://railway.com/deploy/IGoDnc?referralCode=ruU7JR)|
|
||||
|**DigitalOcean**|**Render**|
|
||||
| [<img alt="Deploy on DigitalOcean" src="https://www.deploytodo.com/do-btn-blue.svg" height="40">](https://cloud.digitalocean.com/apps/new?repo=https://github.com/docusealco/docuseal-digitalocean/tree/master&refcode=421d50f53990) | [<img alt="Deploy to Render" src="https://render.com/images/deploy-to-render-button.svg" height="40">](https://render.com/deploy?repo=https://github.com/docusealco/docuseal-render)
|
||||
|
||||
@@ -74,7 +74,7 @@ DocuSeal is an open source platform that provides secure and efficient digital d
|
||||
docker run --name docuseal -p 3000:3000 -v.:/data docuseal/docuseal
|
||||
```
|
||||
|
||||
By default DocuSeal docker container uses an SQLite database to store data and configurations. Alternatively, it is possible use PostgreSQL or MySQL databases by specifying the `DATABASE_URL` env variable.
|
||||
By default DocuSeal docker container uses an SQLite database to store data and configurations. Alternatively, it is possible to use PostgreSQL or MySQL databases by specifying the `DATABASE_URL` env variable.
|
||||
|
||||
#### Docker Compose
|
||||
|
||||
@@ -97,8 +97,8 @@ At DocuSeal we have expertise and technologies to make documents creation, filli
|
||||
|
||||
## License
|
||||
|
||||
Distributed under the AGPLv3 License. See [LICENSE](https://github.com/docusealco/docuseal/blob/master/LICENSE) for more information.
|
||||
Unless otherwise noted, all files © 2023 DocuSeal LLC.
|
||||
Distributed under the AGPLv3 License with Section 7(b) Additional Terms. See [LICENSE](https://github.com/docusealco/docuseal/blob/master/LICENSE) and [LICENSE_ADDITIONAL_TERMS](https://github.com/docusealco/docuseal/blob/master/LICENSE_ADDITIONAL_TERMS) for more information.
|
||||
Unless otherwise noted, all files © 2023-2026 DocuSeal LLC.
|
||||
|
||||
## Tools
|
||||
|
||||
|
||||
@@ -11,17 +11,21 @@ class AccountConfigsController < ApplicationController
|
||||
AccountConfig::FORCE_MFA,
|
||||
AccountConfig::ALLOW_TO_RESUBMIT,
|
||||
AccountConfig::ALLOW_TO_DECLINE_KEY,
|
||||
AccountConfig::ALLOW_TO_DELEGATE_KEY,
|
||||
AccountConfig::FORM_PREFILL_SIGNATURE_KEY,
|
||||
AccountConfig::ESIGNING_PREFERENCE_KEY,
|
||||
AccountConfig::FORM_WITH_CONFETTI_KEY,
|
||||
AccountConfig::DOWNLOAD_LINKS_AUTH_KEY,
|
||||
AccountConfig::DOWNLOAD_LINKS_EXPIRE_KEY,
|
||||
AccountConfig::FORCE_SSO_AUTH_KEY,
|
||||
AccountConfig::FLATTEN_RESULT_PDF_KEY,
|
||||
AccountConfig::ENFORCE_SIGNING_ORDER_KEY,
|
||||
AccountConfig::WITH_FILE_LINKS_KEY,
|
||||
AccountConfig::WITH_SIGNATURE_ID,
|
||||
AccountConfig::COMBINE_PDF_RESULT_KEY,
|
||||
AccountConfig::REQUIRE_SIGNING_REASON_KEY,
|
||||
AccountConfig::DOCUMENT_FILENAME_FORMAT_KEY
|
||||
AccountConfig::DOCUMENT_FILENAME_FORMAT_KEY,
|
||||
AccountConfig::ENABLE_MCP_KEY
|
||||
].freeze
|
||||
|
||||
InvalidKey = Class.new(StandardError)
|
||||
@@ -33,7 +37,7 @@ class AccountConfigsController < ApplicationController
|
||||
end
|
||||
|
||||
def destroy
|
||||
raise InvalidKey unless ALLOWED_KEYS.include?(@account_config.key)
|
||||
raise InvalidKey unless allowed_destroy_keys.include?(@account_config.key)
|
||||
|
||||
@account_config.destroy!
|
||||
|
||||
@@ -42,8 +46,16 @@ class AccountConfigsController < ApplicationController
|
||||
|
||||
private
|
||||
|
||||
def allowed_keys
|
||||
ALLOWED_KEYS
|
||||
end
|
||||
|
||||
def allowed_destroy_keys
|
||||
ALLOWED_KEYS
|
||||
end
|
||||
|
||||
def load_account_config
|
||||
raise InvalidKey unless ALLOWED_KEYS.include?(account_config_params[:key])
|
||||
raise InvalidKey unless allowed_keys.include?(account_config_params[:key])
|
||||
|
||||
@account_config =
|
||||
AccountConfig.find_or_initialize_by(account: current_account, key: account_config_params[:key])
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class AccountCustomFieldsController < ApplicationController
|
||||
before_action :load_account_config, only: :create
|
||||
|
||||
def create
|
||||
authorize!(:create, Template)
|
||||
|
||||
@account_config.update!(account_config_params)
|
||||
|
||||
render json: @account_config.value
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def load_account_config
|
||||
@account_config =
|
||||
AccountConfig.find_or_initialize_by(account: current_account, key: AccountConfig::TEMPLATE_CUSTOM_FIELDS_KEY)
|
||||
end
|
||||
|
||||
def account_config_params
|
||||
params.permit(
|
||||
value: [[:uuid, :name, :type,
|
||||
:required, :readonly, :default_value,
|
||||
:title, :description,
|
||||
{ preferences: {},
|
||||
default_value: [],
|
||||
options: [%i[value uuid]],
|
||||
validation: %i[message pattern min max step],
|
||||
areas: [%i[x y w h cell_w option_uuid]] }]]
|
||||
)
|
||||
end
|
||||
end
|
||||
@@ -8,7 +8,8 @@ class AccountsController < ApplicationController
|
||||
'es-ES' => 'Español',
|
||||
'pt-PT' => 'Português',
|
||||
'de-DE' => 'Deutsch',
|
||||
'it-IT' => 'Italiano'
|
||||
'it-IT' => 'Italiano',
|
||||
'nl-NL' => 'Nederlands'
|
||||
}.freeze
|
||||
|
||||
before_action :load_account
|
||||
@@ -22,35 +23,43 @@ class AccountsController < ApplicationController
|
||||
unless Docuseal.multitenant?
|
||||
@encrypted_config = EncryptedConfig.find_or_initialize_by(account: current_account,
|
||||
key: EncryptedConfig::APP_URL_KEY)
|
||||
@encrypted_config.assign_attributes(app_url_params)
|
||||
|
||||
unless URI.parse(@encrypted_config.value.to_s).class.in?([URI::HTTP, URI::HTTPS])
|
||||
@encrypted_config.errors.add(:value, I18n.t('should_be_a_valid_url'))
|
||||
if can?(:manage, @encrypted_config)
|
||||
@encrypted_config.assign_attributes(app_url_params)
|
||||
|
||||
return render :show, status: :unprocessable_entity
|
||||
unless URI.parse(@encrypted_config.value.to_s).class.in?([URI::HTTP, URI::HTTPS])
|
||||
@encrypted_config.errors.add(:value, I18n.t('should_be_a_valid_url'))
|
||||
|
||||
return render :show, status: :unprocessable_content
|
||||
end
|
||||
|
||||
@encrypted_config.save!
|
||||
|
||||
Docuseal.refresh_default_url_options!
|
||||
end
|
||||
|
||||
@encrypted_config.save!
|
||||
|
||||
Docuseal.refresh_default_url_options!
|
||||
end
|
||||
|
||||
with_locale do
|
||||
redirect_to settings_account_path, notice: I18n.t('account_information_has_been_updated')
|
||||
end
|
||||
rescue ActiveRecord::RecordInvalid
|
||||
render :show, status: :unprocessable_entity
|
||||
render :show, status: :unprocessable_content
|
||||
end
|
||||
|
||||
def destroy
|
||||
authorize!(:manage, current_account)
|
||||
|
||||
true_user.update!(locked_at: Time.current, email: true_user.email.sub('@', '+removed@'))
|
||||
true_user.skip_reconfirmation!
|
||||
true_user.update!(locked_at: Time.current, archived_at: Time.current,
|
||||
email: true_user.email.sub('@', '+removed@'))
|
||||
true_user.account.update!(archived_at: Time.current)
|
||||
|
||||
# rubocop:disable Layout/LineLength
|
||||
render turbo_stream: turbo_stream.replace(
|
||||
:account_delete_button,
|
||||
html: helpers.tag.p(I18n.t('your_account_removal_request_will_be_processed_within_2_months_please_contact_us_if_you_want_to_keep_your_account'))
|
||||
html: helpers.tag.p(I18n.t('your_account_will_be_permanently_deleted_within_2_months_please_contact_us_if_you_want_to_keep_your_account')) +
|
||||
helpers.tag.br +
|
||||
helpers.tag.p(I18n.t('your_email_address_has_been_released_immediately_you_can_now_be_added_to_your_company_team_without_waiting_for_the_deletion_period_to_end'))
|
||||
)
|
||||
# rubocop:enable Layout/LineLength
|
||||
end
|
||||
|
||||
@@ -9,11 +9,13 @@ module Api
|
||||
|
||||
before_action :set_cors_headers
|
||||
before_action :set_noindex_headers
|
||||
before_action :set_security_headers
|
||||
|
||||
# rubocop:disable Metrics
|
||||
def show
|
||||
blob_uuid, purp, exp = ApplicationRecord.signed_id_verifier.verified(params[:signed_uuid])
|
||||
|
||||
if blob_uuid.blank? || (purp.present? && purp != 'blob') || (exp && exp < Time.current.to_i)
|
||||
if blob_uuid.blank? || purp != 'blob'
|
||||
Rollbar.error('Blob not found') if defined?(Rollbar)
|
||||
|
||||
return head :not_found
|
||||
@@ -21,36 +23,54 @@ module Api
|
||||
|
||||
blob = ActiveStorage::Blob.find_by!(uuid: blob_uuid)
|
||||
|
||||
if FilenameUtils.dangerous_extension(blob.filename)
|
||||
Rollbar.error('Dangerous extension') if defined?(Rollbar)
|
||||
|
||||
return head :unprocessable_content
|
||||
end
|
||||
|
||||
attachment = blob.attachments.take
|
||||
|
||||
@record = attachment.record
|
||||
@record = @record.record if @record.is_a?(ActiveStorage::Attachment)
|
||||
|
||||
authorization_check!(attachment) if exp.blank?
|
||||
authorization_check!(attachment, @record, exp)
|
||||
|
||||
if request.headers['Range'].present?
|
||||
send_blob_byte_range_data blob, request.headers['Range']
|
||||
else
|
||||
http_cache_forever public: true do
|
||||
response.headers['Accept-Ranges'] = 'bytes'
|
||||
response.headers['Content-Length'] = blob.byte_size.to_s
|
||||
|
||||
send_blob_stream blob, disposition: params[:disposition]
|
||||
if request.head?
|
||||
response.headers['Content-Type'] = blob.content_type_for_serving
|
||||
head :ok
|
||||
else
|
||||
send_blob_stream blob, disposition: params[:disposition]
|
||||
end
|
||||
|
||||
response.headers['Content-Length'] = blob.byte_size.to_s
|
||||
end
|
||||
end
|
||||
end
|
||||
# rubocop:enable Metrics
|
||||
|
||||
private
|
||||
|
||||
def authorization_check!(attachment)
|
||||
is_authorized = attachment.name.in?(%w[logo preview_images]) ||
|
||||
(current_user && attachment.record.account.id == current_user.account_id) ||
|
||||
(current_user && !Docuseal.multitenant? && current_user.role == 'superadmin') ||
|
||||
!attachment.record.account.account_configs
|
||||
.find_or_initialize_by(key: AccountConfig::DOWNLOAD_LINKS_AUTH_KEY).value
|
||||
def authorization_check!(attachment, record, exp)
|
||||
return if attachment.name == 'logo'
|
||||
return if exp.to_i >= Time.current.to_i
|
||||
return if current_user && current_ability.can?(:read, record)
|
||||
|
||||
return if is_authorized
|
||||
if exp.blank?
|
||||
configs = record.account.account_configs.where(key: [AccountConfig::DOWNLOAD_LINKS_AUTH_KEY,
|
||||
AccountConfig::DOWNLOAD_LINKS_EXPIRE_KEY])
|
||||
|
||||
Rollbar.error('Blob aunauthorized') if defined?(Rollbar)
|
||||
require_auth = configs.any? { |c| c.key == AccountConfig::DOWNLOAD_LINKS_AUTH_KEY && c.value }
|
||||
require_ttl = configs.none? { |c| c.key == AccountConfig::DOWNLOAD_LINKS_EXPIRE_KEY && c.value == false }
|
||||
|
||||
return if !require_ttl && !require_auth
|
||||
end
|
||||
|
||||
raise CanCan::AccessDenied
|
||||
end
|
||||
|
||||
@@ -9,6 +9,7 @@ module Api
|
||||
|
||||
before_action :set_cors_headers
|
||||
before_action :set_noindex_headers
|
||||
before_action :set_security_headers
|
||||
|
||||
# rubocop:disable Metrics
|
||||
def show
|
||||
@@ -18,6 +19,12 @@ module Api
|
||||
|
||||
return head :not_found unless blob
|
||||
|
||||
if FilenameUtils.dangerous_extension(blob.filename)
|
||||
Rollbar.error('Dangerous extension') if defined?(Rollbar)
|
||||
|
||||
return head :unprocessable_content
|
||||
end
|
||||
|
||||
is_permitted = blob.attachments.any? do |a|
|
||||
(current_user && a.record.account.id == current_user.account_id) ||
|
||||
a.record.account.account_configs.any? { |e| e.key == 'legacy_blob_proxy' } ||
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
module Api
|
||||
class ApiBaseController < ActionController::API
|
||||
include ActiveStorage::SetCurrent
|
||||
include Pagy::Backend
|
||||
include Pagy::Method
|
||||
|
||||
DEFAULT_LIMIT = 10
|
||||
MAX_LIMIT = 100
|
||||
@@ -16,7 +16,7 @@ module Api
|
||||
check_authorization
|
||||
|
||||
rescue_from Params::BaseValidator::InvalidParameterError do |e|
|
||||
render json: { error: e.message }, status: :unprocessable_entity
|
||||
render json: { error: e.message }, status: :unprocessable_content
|
||||
end
|
||||
|
||||
rescue_from RateLimit::LimitApproached do |e|
|
||||
@@ -33,7 +33,7 @@ module Api
|
||||
rescue_from JSON::ParserError do |e|
|
||||
Rollbar.warning(e) if defined?(Rollbar)
|
||||
|
||||
render json: { error: "JSON parse error: #{e.message}" }, status: :unprocessable_entity
|
||||
render json: { error: "JSON parse error: #{e.message}" }, status: :unprocessable_content
|
||||
end
|
||||
end
|
||||
|
||||
@@ -102,6 +102,10 @@ module Api
|
||||
headers['X-Robots-Tag'] = 'noindex'
|
||||
end
|
||||
|
||||
def set_security_headers
|
||||
response.headers['X-Content-Type-Options'] = 'nosniff'
|
||||
end
|
||||
|
||||
def set_cors_headers
|
||||
headers['Access-Control-Allow-Origin'] = '*'
|
||||
headers['Access-Control-Allow-Methods'] = 'POST, GET, PUT, PATCH, DELETE, OPTIONS'
|
||||
|
||||
@@ -8,15 +8,52 @@ module Api
|
||||
COOKIE_STORE_LIMIT = 10
|
||||
|
||||
def create
|
||||
submitter = Submitter.find_by!(slug: params[:submitter_slug])
|
||||
@submitter = Submitter.find_by!(slug: params[:submitter_slug])
|
||||
|
||||
attachment = Submitters.create_attachment!(submitter, params)
|
||||
unless can_upload?(@submitter)
|
||||
return render json: { error: I18n.t('form_has_been_archived') }, status: :unprocessable_content
|
||||
end
|
||||
|
||||
if params[:remember_signature] == 'true' && submitter.email.present?
|
||||
cookies.encrypted[:signature_uuids] = build_new_cookie_signatures_json(submitter, attachment)
|
||||
file = params[:file]
|
||||
|
||||
if params[:type].in?(%w[initials signature])
|
||||
image = ImageUtils.load_vips(file.read, content_type: file.content_type)
|
||||
|
||||
if ImageUtils.blank?(image)
|
||||
Rollbar.error("Empty signature: #{@submitter.id}") if defined?(Rollbar)
|
||||
|
||||
return render json: { error: "#{params[:type]} is empty" }, status: :unprocessable_content
|
||||
end
|
||||
|
||||
if ImageUtils.error?(image)
|
||||
Rollbar.error("Error signature: #{@submitter.id}") if defined?(Rollbar)
|
||||
|
||||
return render json: { error: "#{params[:type]} error, try to sign on another device" },
|
||||
status: :unprocessable_content
|
||||
end
|
||||
|
||||
metadata = { analyzed: true, identified: true, width: image.width, height: image.height }
|
||||
end
|
||||
|
||||
attachment = Submitters.create_attachment!(@submitter, file, metadata:)
|
||||
|
||||
if params[:remember_signature] == 'true' && @submitter.email.present?
|
||||
cookies.encrypted[:signature_uuids] = build_new_cookie_signatures_json(@submitter, attachment)
|
||||
end
|
||||
|
||||
render json: attachment.as_json(only: %i[uuid created_at], methods: %i[url filename content_type])
|
||||
rescue Submitters::MaliciousFileExtension => e
|
||||
Rollbar.error(e) if defined?(Rollbar)
|
||||
|
||||
render json: { error: e.message }, status: :unprocessable_content
|
||||
end
|
||||
|
||||
def can_upload?(submitter)
|
||||
!submitter.declined_at? &&
|
||||
!submitter.completed_at? &&
|
||||
!submitter.submission.archived_at? &&
|
||||
!submitter.submission.expired? &&
|
||||
!submitter.submission.template&.archived_at?
|
||||
end
|
||||
|
||||
def build_new_cookie_signatures_json(submitter, attachment)
|
||||
|
||||
@@ -11,18 +11,21 @@ module Api
|
||||
params[:before] = Time.zone.at(params[:before].to_i) if params[:before].present?
|
||||
|
||||
submitters = paginate(
|
||||
submitters.preload(template: :folder, submission: [:submitters, { audit_trail_attachment: :blob,
|
||||
combined_document_attachment: :blob }],
|
||||
submitters.preload(template: { folder: :parent_folder },
|
||||
submission: [:submitters, { audit_trail_attachment: :blob,
|
||||
combined_document_attachment: :blob }],
|
||||
documents_attachments: :blob, attachments_attachments: :blob),
|
||||
field: :completed_at
|
||||
)
|
||||
|
||||
expires_at = Accounts.link_expires_at(current_account)
|
||||
|
||||
render json: {
|
||||
data: submitters.map do |s|
|
||||
{
|
||||
event_type: 'form.completed',
|
||||
timestamp: s.completed_at,
|
||||
data: Submitters::SerializeForWebhook.call(s)
|
||||
data: Submitters::SerializeForWebhook.call(s, expires_at:)
|
||||
}
|
||||
end,
|
||||
pagination: {
|
||||
|
||||
@@ -5,41 +5,71 @@ module Api
|
||||
load_and_authorize_resource :submission
|
||||
|
||||
def index
|
||||
is_merge = params[:merge] == 'true' &&
|
||||
(@submission.schema_documents || @submission.template.schema_documents).size > 1
|
||||
|
||||
documents =
|
||||
if @submission.submitters.all?(&:completed_at?)
|
||||
last_submitter = @submission.submitters.max_by(&:completed_at)
|
||||
|
||||
if last_submitter.documents_attachments.blank?
|
||||
last_submitter.documents_attachments = Submissions::EnsureResultGenerated.call(last_submitter)
|
||||
end
|
||||
|
||||
last_submitter.documents_attachments
|
||||
if @submission.completed_at?
|
||||
build_completed_documents(@submission, merge: is_merge)
|
||||
else
|
||||
values_hash = Submissions::GeneratePreviewAttachments.build_values_hash(@submission)
|
||||
|
||||
if @submission.preview_documents.present? &&
|
||||
@submission.preview_documents.all? { |s| s.metadata['values_hash'] == values_hash }
|
||||
@submission.preview_documents
|
||||
else
|
||||
ApplicationRecord.no_touching do
|
||||
@submission.preview_documents.each(&:destroy)
|
||||
end
|
||||
|
||||
Submissions::GeneratePreviewAttachments.call(@submission, values_hash:)
|
||||
end
|
||||
build_preview_documents(@submission, merge: is_merge)
|
||||
end
|
||||
|
||||
ActiveRecord::Associations::Preloader.new(
|
||||
records: documents,
|
||||
associations: [:blob]
|
||||
).call
|
||||
ActiveRecord::Associations::Preloader.new(records: documents, associations: [:blob]).call
|
||||
|
||||
expires_at = Accounts.link_expires_at(current_account)
|
||||
|
||||
render json: {
|
||||
id: @submission.id,
|
||||
documents: documents.map do |attachment|
|
||||
{ name: attachment.filename.base, url: ActiveStorage::Blob.proxy_url(attachment.blob) }
|
||||
{ name: attachment.filename.base, url: ActiveStorage::Blob.proxy_url(attachment.blob, expires_at:) }
|
||||
end
|
||||
}
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def build_completed_documents(submission, merge: false)
|
||||
last_submitter = submission.submitters.select(&:completed_at?).max_by(&:completed_at)
|
||||
|
||||
if merge
|
||||
if submission.merged_document_attachment.blank?
|
||||
submission.merged_document_attachment =
|
||||
Submissions::GenerateCombinedAttachment.call(last_submitter, with_audit: false)
|
||||
end
|
||||
|
||||
[submission.merged_document_attachment]
|
||||
else
|
||||
if last_submitter.documents_attachments.blank?
|
||||
last_submitter.documents_attachments = Submissions::EnsureResultGenerated.call(last_submitter)
|
||||
end
|
||||
|
||||
last_submitter.documents_attachments
|
||||
end
|
||||
end
|
||||
|
||||
def build_preview_documents(submission, merge: false)
|
||||
values_hash = Submissions::GeneratePreviewAttachments.build_values_hash(submission)
|
||||
|
||||
if merge
|
||||
if submission.preview_merged_document_attachment.present? &&
|
||||
submission.preview_merged_document_attachment.metadata['values_hash'] == values_hash
|
||||
[submission.preview_merged_document_attachment]
|
||||
else
|
||||
ApplicationRecord.no_touching { submission.preview_merged_document_attachment&.destroy }
|
||||
|
||||
Submissions::GeneratePreviewAttachments.call(submission, values_hash:, merge: true)
|
||||
end
|
||||
elsif submission.preview_documents.present? &&
|
||||
submission.preview_documents.all? { |s| s.metadata['values_hash'] == values_hash }
|
||||
submission.preview_documents
|
||||
else
|
||||
ApplicationRecord.no_touching do
|
||||
submission.preview_documents.each(&:destroy)
|
||||
end
|
||||
|
||||
Submissions::GeneratePreviewAttachments.call(submission, values_hash:)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -5,7 +5,7 @@ module Api
|
||||
load_and_authorize_resource :submission, parent: false
|
||||
|
||||
def index
|
||||
submissions = build_completed_query(@submissions)
|
||||
submissions = @submissions.active.where.not(completed_at: nil)
|
||||
|
||||
params[:after] = Time.zone.at(params[:after].to_i) if params[:after].present?
|
||||
params[:before] = Time.zone.at(params[:before].to_i) if params[:before].present?
|
||||
@@ -14,16 +14,19 @@ module Api
|
||||
:created_by_user, :submission_events,
|
||||
template: :folder,
|
||||
submitters: { documents_attachments: :blob, attachments_attachments: :blob },
|
||||
audit_trail_attachment: :blob
|
||||
audit_trail_attachment: :blob,
|
||||
combined_document_attachment: :blob
|
||||
),
|
||||
field: :completed_at)
|
||||
|
||||
expires_at = Accounts.link_expires_at(current_account)
|
||||
|
||||
render json: {
|
||||
data: submissions.map do |s|
|
||||
{
|
||||
event_type: 'submission.completed',
|
||||
timestamp: s.completed_at,
|
||||
data: Submissions::SerializeForApi.call(s, s.submitters)
|
||||
data: Submissions::SerializeForApi.call(s, s.submitters, expires_at:)
|
||||
}
|
||||
end,
|
||||
pagination: {
|
||||
@@ -33,20 +36,5 @@ module Api
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def build_completed_query(submissions)
|
||||
submissions = submissions.where(
|
||||
Submitter.where(completed_at: nil).where(
|
||||
Submitter.arel_table[:submission_id].eq(Submission.arel_table[:id])
|
||||
).select(1).arel.exists.not
|
||||
)
|
||||
|
||||
submissions.joins(:submitters)
|
||||
.group(:id)
|
||||
.select(Submission.arel_table[Arel.star],
|
||||
Submitter.arel_table[:completed_at].maximum.as('completed_at'))
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -2,26 +2,46 @@
|
||||
|
||||
module Api
|
||||
class SubmissionsController < ApiBaseController
|
||||
SUBMISSION_COLUMNS = %i[id name slug source submitters_order expire_at completed_at created_at updated_at
|
||||
archived_at variables template_id template_submitters created_by_user_id].freeze
|
||||
TEMPLATE_COLUMNS = %i[id name external_id created_at updated_at folder_id submitters].freeze
|
||||
|
||||
load_and_authorize_resource :template, only: :create
|
||||
load_and_authorize_resource :submission, only: %i[show index destroy]
|
||||
load_and_authorize_resource :submission, only: %i[show index update destroy]
|
||||
|
||||
before_action only: :create do
|
||||
authorize!(:create, Submission)
|
||||
end
|
||||
|
||||
before_action :maybe_return_template_error, only: :create
|
||||
|
||||
def index
|
||||
submissions = Submissions.search(current_user, @submissions, params[:q])
|
||||
submissions = filter_submissions(submissions, params)
|
||||
|
||||
submissions = paginate(submissions.preload(:created_by_user, :submitters,
|
||||
template: :folder,
|
||||
combined_document_attachment: :blob,
|
||||
audit_trail_attachment: :blob))
|
||||
with_fields = params[:include].to_s.include?('fields') || params[:include].to_s.include?('combined_document_url')
|
||||
|
||||
submissions = paginate(
|
||||
submissions.select(with_fields ? nil : SUBMISSION_COLUMNS)
|
||||
.preload(:created_by_user, :submitters, combined_document_attachment: :blob,
|
||||
audit_trail_attachment: :blob)
|
||||
)
|
||||
|
||||
ActiveRecord::Associations::Preloader.new(
|
||||
records: submissions,
|
||||
associations: :template,
|
||||
scope: with_fields ? nil : Template.select(TEMPLATE_COLUMNS)
|
||||
).call
|
||||
|
||||
ActiveRecord::Associations::Preloader.new(records: submissions.filter_map(&:template),
|
||||
associations: { folder: :parent_folder }).call
|
||||
|
||||
expires_at = Accounts.link_expires_at(current_account)
|
||||
|
||||
render json: {
|
||||
data: submissions.map do |s|
|
||||
Submissions::SerializeForApi.call(s, s.submitters, params,
|
||||
with_events: false, with_documents: false, with_values: false)
|
||||
with_events: false, with_documents: false, with_values: false, expires_at:)
|
||||
end,
|
||||
pagination: {
|
||||
count: submissions.size,
|
||||
@@ -40,8 +60,8 @@ module Api
|
||||
end
|
||||
end
|
||||
|
||||
if @submission.audit_trail_attachment.blank? && submitters.all?(&:completed_at?)
|
||||
@submission.audit_trail_attachment = Submissions::GenerateAuditTrail.call(@submission)
|
||||
if @submission.audit_trail_attachment.blank? && @submission.completed_at?
|
||||
@submission.audit_trail_attachment = Submissions::EnsureAuditGenerated.call(@submission)
|
||||
end
|
||||
|
||||
render json: Submissions::SerializeForApi.call(@submission, submitters, params)
|
||||
@@ -50,33 +70,27 @@ module Api
|
||||
def create
|
||||
Params::SubmissionCreateValidator.call(params)
|
||||
|
||||
return render json: { error: 'Template not found' }, status: :unprocessable_entity if @template.nil?
|
||||
|
||||
if @template.fields.blank?
|
||||
Rollbar.warning("Template does not contain fields: #{@template.id}") if defined?(Rollbar)
|
||||
|
||||
return render json: { error: 'Template does not contain fields' }, status: :unprocessable_entity
|
||||
end
|
||||
|
||||
params[:send_email] = true unless params.key?(:send_email)
|
||||
params[:send_sms] = false unless params.key?(:send_sms)
|
||||
|
||||
submissions = create_submissions(@template, params)
|
||||
|
||||
WebhookUrls.for_account_id(@template.account_id, 'submission.created').each do |webhook_url|
|
||||
submissions.each do |submission|
|
||||
SendSubmissionCreatedWebhookRequestJob.perform_async('submission_id' => submission.id,
|
||||
'webhook_url_id' => webhook_url.id)
|
||||
end
|
||||
end
|
||||
WebhookUrls.enqueue_events(submissions, 'submission.created')
|
||||
|
||||
Submissions.send_signature_requests(submissions)
|
||||
|
||||
submissions.each do |submission|
|
||||
if submission.submitters.all? { |s| s.viewer? || s.completed_at? } &&
|
||||
Submissions.maybe_update_completed_at(submission)
|
||||
last_submitter = submission.submitters.reject(&:viewer?).max_by(&:completed_at)
|
||||
end
|
||||
|
||||
submission.submitters.each do |submitter|
|
||||
next unless submitter.completed_at?
|
||||
|
||||
ProcessSubmitterCompletionJob.perform_async('submitter_id' => submitter.id, 'send_invitation_email' => false)
|
||||
ProcessSubmitterCompletionJob.perform_async('submitter_id' => submitter.id,
|
||||
'is_last' => submitter == last_submitter,
|
||||
'send_invitation_email' => false)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -87,7 +101,26 @@ module Api
|
||||
DownloadUtils::UnableToDownload => e
|
||||
Rollbar.warning(e) if defined?(Rollbar)
|
||||
|
||||
render json: { error: e.message }, status: :unprocessable_entity
|
||||
render json: { error: e.message }, status: :unprocessable_content
|
||||
end
|
||||
|
||||
def update
|
||||
@submission = assign_submission_attrs(@submission, submission_params)
|
||||
|
||||
@submission.save!
|
||||
|
||||
if @submission.saved_change_to_archived_at? && @submission.archived_at?
|
||||
WebhookUrls.enqueue_events(@submission, 'submission.archived')
|
||||
end
|
||||
|
||||
if @submission.saved_change_to_expire_at? && @submission.expire_at?
|
||||
ProcessSubmissionExpiredJob.perform_at(@submission.expire_at, 'submission_id' => @submission.id,
|
||||
'expire_at' => @submission.expire_at.to_i)
|
||||
end
|
||||
|
||||
SearchEntries.enqueue_reindex(@submission) if @submission.saved_change_to_name?
|
||||
|
||||
render json: Submissions::SerializeForApi.call(@submission, nil, params, with_events: false)
|
||||
end
|
||||
|
||||
def destroy
|
||||
@@ -96,10 +129,7 @@ module Api
|
||||
else
|
||||
@submission.update!(archived_at: Time.current)
|
||||
|
||||
WebhookUrls.for_account_id(@submission.account_id, 'submission.archived').each do |webhook_url|
|
||||
SendSubmissionArchivedWebhookRequestJob.perform_async('submission_id' => @submission.id,
|
||||
'webhook_url_id' => webhook_url.id)
|
||||
end
|
||||
WebhookUrls.enqueue_events(@submission, 'submission.archived')
|
||||
end
|
||||
|
||||
render json: @submission.as_json(only: %i[id archived_at])
|
||||
@@ -107,12 +137,49 @@ module Api
|
||||
|
||||
private
|
||||
|
||||
def assign_submission_attrs(submission, attrs)
|
||||
archived = attrs.key?(:archived) ? attrs[:archived] : attrs[:archived_at]
|
||||
|
||||
if archived.in?([true, false, 'true', 'false']) && current_ability.can?(:destroy, submission)
|
||||
submission.archived_at = archived.in?(Submitters::TRUE_VALUES) ? Time.current : nil
|
||||
end
|
||||
|
||||
submission.name = attrs[:name] if attrs.key?(:name)
|
||||
submission.expire_at = attrs[:expire_at].presence if attrs.key?(:expire_at)
|
||||
|
||||
submission
|
||||
end
|
||||
|
||||
def submission_params
|
||||
submission_params = params.key?(:submission) ? params.require(:submission) : params
|
||||
|
||||
submission_params.permit(:name, :expire_at, :archived, :archived_at)
|
||||
end
|
||||
|
||||
def maybe_return_template_error
|
||||
return render json: { error: 'Template not found' }, status: :unprocessable_content if @template.nil?
|
||||
|
||||
if @template.archived_at?
|
||||
Rollbar.warning("Archived template submission: #{@template.id}") if defined?(Rollbar)
|
||||
|
||||
return render json: { error: 'Template has been archived' }, status: :unprocessable_content
|
||||
end
|
||||
|
||||
return if @template.fields.present?
|
||||
|
||||
Rollbar.warning("Template does not contain fields: #{@template.id}") if defined?(Rollbar)
|
||||
|
||||
render json: { error: 'Template does not contain fields' }, status: :unprocessable_content
|
||||
end
|
||||
|
||||
def filter_submissions(submissions, params)
|
||||
submissions = submissions.where(template_id: params[:template_id]) if params[:template_id].present?
|
||||
submissions = submissions.where(slug: params[:slug]) if params[:slug].present?
|
||||
|
||||
if params[:template_folder].present?
|
||||
submissions = submissions.joins(template: :folder).where(folder: { name: params[:template_folder] })
|
||||
folders =
|
||||
TemplateFolders.filter_by_full_name(TemplateFolder.accessible_by(current_ability), params[:template_folder])
|
||||
|
||||
submissions = submissions.joins(:template).where(template: { folder_id: folders.pluck(:id) })
|
||||
end
|
||||
|
||||
if params.key?(:archived)
|
||||
@@ -150,7 +217,7 @@ module Api
|
||||
is_send_email = !params[:send_email].in?(['false', false])
|
||||
|
||||
if (emails = (params[:emails] || params[:email]).presence) &&
|
||||
(params[:submission].blank? && params[:submitters].blank?)
|
||||
params[:submission].blank? && params[:submitters].blank?
|
||||
Submissions.create_from_emails(template:,
|
||||
user: current_user,
|
||||
source: :api,
|
||||
@@ -159,7 +226,7 @@ module Api
|
||||
params:)
|
||||
else
|
||||
submissions_attrs, attachments =
|
||||
Submissions::NormalizeParamUtils.normalize_submissions_params!(submissions_params, template)
|
||||
Submissions::NormalizeParamUtils.normalize_submissions_params!(submissions_params, template, purpose: :api)
|
||||
|
||||
submissions = Submissions.create_from_submitters(
|
||||
template:,
|
||||
@@ -175,7 +242,10 @@ module Api
|
||||
Submissions::NormalizeParamUtils.save_default_value_attachments!(attachments, submitters)
|
||||
|
||||
submitters.each do |submitter|
|
||||
SubmissionEvents.create_with_tracking_data(submitter, 'api_complete_form', request) if submitter.completed_at?
|
||||
if submitter.completed_at?
|
||||
Submitters::SubmitValues.maybe_invite_via_field(submitter, request)
|
||||
SubmissionEvents.create_with_tracking_data(submitter, 'api_complete_form', request)
|
||||
end
|
||||
end
|
||||
|
||||
submissions
|
||||
@@ -185,15 +255,17 @@ module Api
|
||||
def submissions_params
|
||||
permitted_attrs = [
|
||||
:send_email, :send_sms, :bcc_completed, :completed_redirect_url, :reply_to, :go_to_last,
|
||||
:expire_at, :name,
|
||||
:require_phone_2fa, :require_email_2fa, :expire_at, :name,
|
||||
{
|
||||
variables: {},
|
||||
message: %i[subject body],
|
||||
submitters: [[:send_email, :send_sms, :completed_redirect_url, :uuid, :name, :email, :role,
|
||||
:completed, :phone, :application_key, :external_id, :reply_to, :go_to_last,
|
||||
:require_phone_2fa, :require_email_2fa, :order, :index, :invite_by,
|
||||
{ metadata: {}, values: {}, roles: [], readonly_fields: [], message: %i[subject body],
|
||||
fields: [:name, :uuid, :default_value, :value, :title, :description,
|
||||
:readonly, :required, :validation_pattern, :invalid_message,
|
||||
{ default_value: [], value: [], preferences: {} }] }]]
|
||||
{ default_value: [], value: [], preferences: {}, validation: {} }] }]]
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
@@ -1,10 +1,7 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Api
|
||||
class SubmitterEmailClicksController < ApiBaseController
|
||||
skip_before_action :authenticate_user!
|
||||
skip_authorization_check
|
||||
|
||||
class SubmitterEmailClicksController < ActionController::API
|
||||
def create
|
||||
@submitter = Submitter.find_by!(slug: params[:submitter_slug])
|
||||
|
||||
|
||||
@@ -1,10 +1,7 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Api
|
||||
class SubmitterFormViewsController < ApiBaseController
|
||||
skip_before_action :authenticate_user!
|
||||
skip_authorization_check
|
||||
|
||||
class SubmitterFormViewsController < ActionController::API
|
||||
def create
|
||||
@submitter = Submitter.find_by!(slug: params[:submitter_slug])
|
||||
|
||||
@@ -13,10 +10,7 @@ module Api
|
||||
|
||||
SubmissionEvents.create_with_tracking_data(@submitter, 'view_form', request)
|
||||
|
||||
WebhookUrls.for_account_id(@submitter.account_id, 'form.viewed').each do |webhook_url|
|
||||
SendFormViewedWebhookRequestJob.perform_async('submitter_id' => @submitter.id,
|
||||
'webhook_url_id' => webhook_url.id)
|
||||
end
|
||||
WebhookUrls.enqueue_events(@submitter, 'form.viewed')
|
||||
|
||||
render json: {}
|
||||
end
|
||||
|
||||
@@ -4,6 +4,8 @@ module Api
|
||||
class SubmittersController < ApiBaseController
|
||||
load_and_authorize_resource :submitter
|
||||
|
||||
before_action :maybe_return_submitter_error, only: :update
|
||||
|
||||
def index
|
||||
submitters = Submitters.search(current_user, @submitters, params[:q])
|
||||
|
||||
@@ -14,9 +16,11 @@ module Api
|
||||
documents_attachments: :blob, attachments_attachments: :blob)
|
||||
)
|
||||
|
||||
expires_at = Accounts.link_expires_at(current_account)
|
||||
|
||||
render json: {
|
||||
data: submitters.map do |s|
|
||||
Submitters::SerializeForApi.call(s, with_template: true, with_events: true, params:)
|
||||
Submitters::SerializeForApi.call(s, with_template: true, with_events: true, params:, expires_at:)
|
||||
end,
|
||||
pagination: {
|
||||
count: submitters.size,
|
||||
@@ -32,18 +36,16 @@ module Api
|
||||
render json: Submitters::SerializeForApi.call(@submitter, with_template: true, with_events: true, params:)
|
||||
end
|
||||
|
||||
# rubocop:disable Metrics/MethodLength
|
||||
def update
|
||||
if @submitter.completed_at?
|
||||
return render json: { error: 'Submitter has already completed the submission.' }, status: :unprocessable_entity
|
||||
end
|
||||
|
||||
submission = @submitter.submission
|
||||
role = submission.template_submitters.find { |e| e['uuid'] == @submitter.uuid }['name']
|
||||
|
||||
normalized_params, new_attachments = Submissions::NormalizeParamUtils.normalize_submitter_params!(
|
||||
submitter_params.merge(role:),
|
||||
@submitter.template || Template.new(submitters: submission.template_submitters, account: @submitter.account),
|
||||
for_submitter: @submitter
|
||||
for_submitter: @submitter,
|
||||
purpose: :api
|
||||
)
|
||||
|
||||
Submissions::CreateFromSubmitters.maybe_set_template_fields(submission, [normalized_params],
|
||||
@@ -58,11 +60,16 @@ module Api
|
||||
|
||||
@submitter.submission.save!
|
||||
|
||||
SubmissionEvents.create_with_tracking_data(@submitter, 'api_complete_form', request) if @submitter.completed_at?
|
||||
if @submitter.completed_at?
|
||||
Submitters::SubmitValues.maybe_invite_via_field(@submitter, request)
|
||||
SubmissionEvents.create_with_tracking_data(@submitter, 'api_complete_form', request)
|
||||
end
|
||||
end
|
||||
|
||||
if @submitter.completed_at?
|
||||
ProcessSubmitterCompletionJob.perform_async('submitter_id' => @submitter.id)
|
||||
is_last = Submissions.maybe_update_completed_at(@submitter.submission)
|
||||
|
||||
ProcessSubmitterCompletionJob.perform_async('submitter_id' => @submitter.id, 'is_last' => is_last)
|
||||
elsif normalized_params[:send_email] || normalized_params[:send_sms]
|
||||
Submitters.send_signature_requests([@submitter])
|
||||
end
|
||||
@@ -74,15 +81,16 @@ module Api
|
||||
rescue Submitters::NormalizeValues::BaseError, DownloadUtils::UnableToDownload => e
|
||||
Rollbar.warning(e) if defined?(Rollbar)
|
||||
|
||||
render json: { error: e.message }, status: :unprocessable_entity
|
||||
render json: { error: e.message }, status: :unprocessable_content
|
||||
end
|
||||
# rubocop:enable Metrics/MethodLength
|
||||
|
||||
def submitter_params
|
||||
submitter_params = params.key?(:submitter) ? params.require(:submitter) : params
|
||||
|
||||
submitter_params.permit(
|
||||
:send_email, :send_sms, :reply_to, :completed_redirect_url, :uuid, :name, :email, :role,
|
||||
:completed, :phone, :application_key, :external_id, :go_to_last,
|
||||
:completed, :phone, :application_key, :external_id, :go_to_last, :require_phone_2fa, :require_email_2fa,
|
||||
{ metadata: {}, values: {}, readonly_fields: [], message: %i[subject body],
|
||||
fields: [[:name, :uuid, :default_value, :value, :required,
|
||||
:readonly, :validation_pattern, :invalid_message,
|
||||
@@ -92,7 +100,17 @@ module Api
|
||||
|
||||
private
|
||||
|
||||
def maybe_filder_by_completed_at(submitters, params)
|
||||
def maybe_return_submitter_error
|
||||
if @submitter.completed_at? || @submitter.submission.completed_at?
|
||||
return render json: { error: 'Submitter has already completed the submission.' }, status: :unprocessable_content
|
||||
end
|
||||
|
||||
return unless @submitter.declined_at?
|
||||
|
||||
render json: { error: 'Submitter has already declined the submission.' }, status: :unprocessable_content
|
||||
end
|
||||
|
||||
def maybe_filter_by_completed_at(submitters, params)
|
||||
if params[:completed_after].present?
|
||||
submitters = submitters.where(completed_at: Time.zone.parse(params[:completed_after])..)
|
||||
end
|
||||
@@ -149,9 +167,7 @@ module Api
|
||||
submitter.values = Submitters::SubmitValues.maybe_remove_condition_values(submitter)
|
||||
end
|
||||
|
||||
submitter.values = submitter.values.transform_values do |v|
|
||||
v == '{{date}}' ? Time.current.in_time_zone(submitter.account.timezone).to_date.to_s : v
|
||||
end
|
||||
submitter.values = Submitters::SubmitValues.replace_current_date_placeholders(submitter)
|
||||
end
|
||||
|
||||
submitter
|
||||
@@ -167,7 +183,7 @@ module Api
|
||||
submitters = submitters.joins(:submission).where(submissions: { template_id: params[:template_id] })
|
||||
end
|
||||
|
||||
maybe_filder_by_completed_at(submitters, params)
|
||||
maybe_filter_by_completed_at(submitters, params)
|
||||
end
|
||||
|
||||
def assign_external_id(submitter, attrs)
|
||||
@@ -193,6 +209,15 @@ module Api
|
||||
|
||||
submitter.preferences['send_sms'] = submitter_preferences['send_sms'] if submitter_preferences.key?('send_sms')
|
||||
submitter.preferences['reply_to'] = submitter_preferences['reply_to'] if submitter_preferences.key?('reply_to')
|
||||
|
||||
if submitter_preferences.key?('require_phone_2fa')
|
||||
submitter.preferences['require_phone_2fa'] = submitter_preferences['require_phone_2fa']
|
||||
end
|
||||
|
||||
if submitter_preferences.key?('require_email_2fa')
|
||||
submitter.preferences['require_email_2fa'] = submitter_preferences['require_email_2fa']
|
||||
end
|
||||
|
||||
if submitter_preferences.key?('go_to_last')
|
||||
submitter.preferences['go_to_last'] = submitter_preferences['go_to_last']
|
||||
end
|
||||
|
||||
@@ -9,7 +9,7 @@ module Api
|
||||
|
||||
ActiveRecord::Associations::Preloader.new(
|
||||
records: [@template],
|
||||
associations: [schema_documents: :preview_images_attachments]
|
||||
associations: [{ schema_documents: :preview_images_attachments }]
|
||||
).call
|
||||
|
||||
cloned_template = Templates::Clone.call(
|
||||
@@ -26,16 +26,15 @@ module Api
|
||||
original_template: @template,
|
||||
documents: params[:documents])
|
||||
|
||||
Templates.maybe_assign_access(cloned_template)
|
||||
|
||||
cloned_template.save!
|
||||
|
||||
WebhookUrls.for_account_id(cloned_template.account_id, 'template.created').each do |webhook_url|
|
||||
SendTemplateCreatedWebhookRequestJob.perform_async('template_id' => cloned_template.id,
|
||||
'webhook_url_id' => webhook_url.id)
|
||||
end
|
||||
WebhookUrls.enqueue_events(cloned_template, 'template.created')
|
||||
|
||||
SearchEntries.enqueue_reindex(cloned_template)
|
||||
|
||||
render json: Templates::SerializeForApi.call(cloned_template, schema_documents)
|
||||
render json: Templates::SerializeForApi.call(cloned_template, schema_documents:)
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -5,32 +5,23 @@ module Api
|
||||
load_and_authorize_resource :template
|
||||
|
||||
def index
|
||||
@templates = Templates.shared(current_user) if params[:shared].in?(['true', true])
|
||||
|
||||
templates = filter_templates(@templates, params)
|
||||
|
||||
templates = paginate(templates.preload(:author, :folder))
|
||||
templates = paginate(templates.preload(:author, folder: :parent_folder))
|
||||
|
||||
schema_documents =
|
||||
ActiveStorage::Attachment.where(record_id: templates.map(&:id),
|
||||
record_type: 'Template',
|
||||
name: :documents,
|
||||
uuid: templates.flat_map { |t| t.schema.pluck('attachment_uuid') })
|
||||
.preload(:blob)
|
||||
schema_documents, dynamic_documents, preview_image_attachments = preload_relations(templates)
|
||||
|
||||
preview_image_attachments =
|
||||
ActiveStorage::Attachment.joins(:blob)
|
||||
.where(blob: { filename: ['0.png', '0.jpg'] })
|
||||
.where(record_id: schema_documents.map(&:id),
|
||||
record_type: 'ActiveStorage::Attachment',
|
||||
name: :preview_images)
|
||||
.preload(:blob)
|
||||
expires_at = Accounts.link_expires_at(current_account)
|
||||
|
||||
render json: {
|
||||
data: templates.map do |t|
|
||||
Templates::SerializeForApi.call(
|
||||
t,
|
||||
schema_documents.select { |e| e.record_id == t.id },
|
||||
preview_image_attachments
|
||||
)
|
||||
Templates::SerializeForApi.call(t,
|
||||
schema_documents: schema_documents.select { |e| e.record_id == t.id },
|
||||
dynamic_documents:,
|
||||
preview_image_attachments:,
|
||||
expires_at:)
|
||||
end,
|
||||
pagination: {
|
||||
count: templates.size,
|
||||
@@ -59,17 +50,18 @@ module Api
|
||||
|
||||
archived = params.key?(:archived) ? params[:archived] : params.dig(:template, :archived)
|
||||
|
||||
if archived.in?([true, false])
|
||||
if archived.in?([true, false]) && current_ability.can?(:destroy, @template)
|
||||
@template.archived_at = archived == true ? Time.current : nil
|
||||
end
|
||||
|
||||
@template.update!(template_params)
|
||||
|
||||
SearchEntries.enqueue_reindex(@template)
|
||||
SearchEntries.enqueue_reindex(@template) if @template.saved_change_to_name?
|
||||
|
||||
WebhookUrls.for_account_id(@template.account_id, 'template.updated').each do |webhook_url|
|
||||
SendTemplateUpdatedWebhookRequestJob.perform_async('template_id' => @template.id,
|
||||
'webhook_url_id' => webhook_url.id)
|
||||
WebhookUrls.enqueue_events(@template, 'template.updated')
|
||||
|
||||
if @template.saved_change_to_archived_at? && @template.archived_at?
|
||||
WebhookUrls.enqueue_events(@template, 'template.archived')
|
||||
end
|
||||
|
||||
render json: @template.as_json(only: %i[id updated_at])
|
||||
@@ -80,6 +72,8 @@ module Api
|
||||
@template.destroy!
|
||||
else
|
||||
@template.update!(archived_at: Time.current)
|
||||
|
||||
WebhookUrls.enqueue_events(@template, 'template.archived')
|
||||
end
|
||||
|
||||
render json: @template.as_json(only: %i[id archived_at])
|
||||
@@ -87,13 +81,59 @@ module Api
|
||||
|
||||
private
|
||||
|
||||
def preload_relations(templates)
|
||||
schema_documents =
|
||||
ActiveStorage::Attachment.where(record_id: templates.map(&:id),
|
||||
record_type: 'Template',
|
||||
name: :documents,
|
||||
uuid: templates.flat_map { |t| t.schema.pluck('attachment_uuid') })
|
||||
.preload(:blob)
|
||||
|
||||
dynamic_document_uuids =
|
||||
templates.flat_map { |t| t.schema.select { |item| item['dynamic'] }.pluck('attachment_uuid') }
|
||||
|
||||
dynamic_documents =
|
||||
if dynamic_document_uuids.present?
|
||||
DynamicDocument.where(template: templates.map(&:id))
|
||||
.where(uuid: dynamic_document_uuids)
|
||||
.preload(current_version: { document_attachment: :blob })
|
||||
.select(:id, :uuid, :template_id, :sha1, :created_at, :updated_at)
|
||||
else
|
||||
DynamicDocument.none
|
||||
end
|
||||
|
||||
preview_attachment_ids =
|
||||
schema_documents.map(&:id) + dynamic_documents.filter_map { |d| d.current_version&.document_attachment&.id }
|
||||
|
||||
preview_image_attachments =
|
||||
ActiveStorage::Attachment.joins(:blob)
|
||||
.where(blob: { filename: ['0.png', '0.jpg'] })
|
||||
.where(record_id: preview_attachment_ids,
|
||||
record_type: 'ActiveStorage::Attachment',
|
||||
name: :preview_images)
|
||||
.preload(:blob)
|
||||
|
||||
[schema_documents, dynamic_documents, preview_image_attachments]
|
||||
end
|
||||
|
||||
def filter_templates(templates, params)
|
||||
templates = Templates.search(current_user, templates, params[:q])
|
||||
templates =
|
||||
if params[:shared].in?(['true', true])
|
||||
Templates.search_shared(current_user, templates, params[:q])
|
||||
else
|
||||
Templates.search(current_user, templates, params[:q])
|
||||
end
|
||||
|
||||
templates = params[:archived].in?(['true', true]) ? templates.archived : templates.active
|
||||
templates = templates.where(external_id: params[:application_key]) if params[:application_key].present?
|
||||
templates = templates.where(external_id: params[:external_id]) if params[:external_id].present?
|
||||
templates = templates.where(slug: params[:slug]) if params[:slug].present?
|
||||
templates = templates.joins(:folder).where(folder: { name: params[:folder] }) if params[:folder].present?
|
||||
|
||||
if params[:folder].present?
|
||||
folders = TemplateFolders.filter_by_full_name(TemplateFolder.accessible_by(current_ability), params[:folder])
|
||||
|
||||
templates = templates.where(folder_id: folders.pluck(:id))
|
||||
end
|
||||
|
||||
templates
|
||||
end
|
||||
@@ -104,15 +144,17 @@ module Api
|
||||
:external_id,
|
||||
:shared_link,
|
||||
{
|
||||
submitters: [%i[name uuid is_requester invite_by_uuid optional_invite_by_uuid linked_to_uuid email]],
|
||||
submitters: [%i[name uuid is_requester invite_by_uuid invite_via_field_uuid
|
||||
optional_invite_by_uuid linked_to_uuid email order]],
|
||||
fields: [[:uuid, :submitter_uuid, :name, :type,
|
||||
:required, :readonly, :default_value,
|
||||
:title, :description,
|
||||
:title, :description, :prefillable,
|
||||
{ preferences: {},
|
||||
default_value: [],
|
||||
conditions: [%i[field_uuid value action operation]],
|
||||
options: [%i[value uuid]],
|
||||
validation: %i[message pattern],
|
||||
areas: [%i[x y w h cell_w attachment_uuid option_uuid page]] }]]
|
||||
validation: %i[message pattern min max step],
|
||||
areas: [%i[uuid x y w h cell_w attachment_uuid option_uuid page]] }]]
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
@@ -7,8 +7,8 @@ module Api
|
||||
def merge
|
||||
files = params[:files] || []
|
||||
|
||||
return render json: { error: 'Files are required' }, status: :unprocessable_entity if files.blank?
|
||||
return render json: { error: 'At least 2 files are required' }, status: :unprocessable_entity if files.size < 2
|
||||
return render json: { error: 'Files are required' }, status: :unprocessable_content if files.blank?
|
||||
return render json: { error: 'At least 2 files are required' }, status: :unprocessable_content if files.size < 2
|
||||
|
||||
render json: {
|
||||
data: Base64.encode64(PdfUtils.merge(files.map { |base64| StringIO.new(Base64.decode64(base64)) }).string)
|
||||
@@ -17,25 +17,24 @@ module Api
|
||||
|
||||
def verify
|
||||
file = Base64.decode64(params[:file])
|
||||
pdf = HexaPDF::Document.new(io: StringIO.new(file))
|
||||
|
||||
trusted_certs = Accounts.load_trusted_certs(current_account)
|
||||
is_checksum_found = CompletedDocument.exists?(sha256: Base64.urlsafe_encode64(Digest::SHA256.digest(file)))
|
||||
|
||||
render json: {
|
||||
checksum_status: is_checksum_found ? 'verified' : 'not_found',
|
||||
signatures: pdf.signatures.map do |sig|
|
||||
signatures: VerifyPdfSignature.call(StringIO.new(file), trusted_certs).map do |sig|
|
||||
{
|
||||
verification_result: sig.verify(trusted_certs:).messages,
|
||||
signer_name: sig.signer_name,
|
||||
signing_reason: sig.signing_reason,
|
||||
verification_result: sig.messages.map { |m| { type: m.status || :info, content: m.text } },
|
||||
signer_name: sig.common_name,
|
||||
signing_reason: sig.reason,
|
||||
signing_time: sig.signing_time,
|
||||
signature_type: sig.signature_type
|
||||
signature_type: sig.type
|
||||
}
|
||||
end
|
||||
}
|
||||
rescue HexaPDF::MalformedPDFError
|
||||
render json: { error: 'Malformed PDF' }, status: :unprocessable_entity
|
||||
rescue Pdfium::PdfiumError
|
||||
render json: { error: 'Malformed PDF' }, status: :unprocessable_content
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -4,7 +4,7 @@ class ApplicationController < ActionController::Base
|
||||
BROWSER_LOCALE_REGEXP = /\A\w{2}(?:-\w{2})?/
|
||||
|
||||
include ActiveStorage::SetCurrent
|
||||
include Pagy::Backend
|
||||
include Pagy::Method
|
||||
|
||||
check_authorization unless: :devise_controller?
|
||||
|
||||
@@ -13,21 +13,28 @@ class ApplicationController < ActionController::Base
|
||||
before_action :maybe_redirect_to_setup, unless: :signed_in?
|
||||
before_action :authenticate_user!, unless: :devise_controller?
|
||||
|
||||
before_action :set_csp, if: lambda {
|
||||
request.get? && !request.headers['HTTP_X_TURBO_REQUEST_ID'] && !turbo_infinite_scroll?
|
||||
}
|
||||
|
||||
helper_method :button_title,
|
||||
:current_account,
|
||||
:true_ability,
|
||||
:form_link_host,
|
||||
:svg_icon
|
||||
:svg_icon,
|
||||
:turbo_native_ios?,
|
||||
:turbo_native_android?
|
||||
|
||||
impersonates :user, with: ->(uuid) { User.find_by(uuid:) }
|
||||
|
||||
rescue_from Pagy::OverflowError do
|
||||
rescue_from Pagy::RangeError do
|
||||
redirect_to request.path
|
||||
end
|
||||
|
||||
rescue_from RateLimit::LimitApproached do |e|
|
||||
Rollbar.error(e) if defined?(Rollbar)
|
||||
|
||||
redirect_to request.referer, alert: 'Too many requests', status: :too_many_requests
|
||||
redirect_back_or_to root_path, alert: 'Too many requests'
|
||||
end
|
||||
|
||||
if Rails.env.production? || Rails.env.test?
|
||||
@@ -39,10 +46,6 @@ class ApplicationController < ActionController::Base
|
||||
end
|
||||
|
||||
def default_url_options
|
||||
if request.domain == 'docuseal.com'
|
||||
return { host: 'docuseal.com', protocol: ENV['FORCE_SSL'].present? ? 'https' : 'http' }
|
||||
end
|
||||
|
||||
Docuseal.default_url_options
|
||||
end
|
||||
|
||||
@@ -57,7 +60,7 @@ class ApplicationController < ActionController::Base
|
||||
|
||||
def pagy_auto(collection, **keyword_args)
|
||||
if current_ability.can?(:manage, :countless)
|
||||
pagy_countless(collection, **keyword_args)
|
||||
pagy(:countless, collection, **keyword_args)
|
||||
else
|
||||
pagy(collection, **keyword_args)
|
||||
end
|
||||
@@ -65,6 +68,28 @@ class ApplicationController < ActionController::Base
|
||||
|
||||
private
|
||||
|
||||
def turbo_infinite_scroll?
|
||||
request.headers['HTTP_X_TURBO_INFINITE_SCROLL'].present?
|
||||
end
|
||||
|
||||
def turbo_native_app?
|
||||
return @turbo_native_app if defined?(@turbo_native_app)
|
||||
|
||||
@turbo_native_app = request.user_agent.to_s.include?('Hotwire Native')
|
||||
end
|
||||
|
||||
def turbo_native_ios?
|
||||
return @turbo_native_ios if defined?(@turbo_native_ios)
|
||||
|
||||
@turbo_native_ios = request.user_agent.to_s.include?('Hotwire Native iOS')
|
||||
end
|
||||
|
||||
def turbo_native_android?
|
||||
return @turbo_native_android if defined?(@turbo_native_android)
|
||||
|
||||
@turbo_native_android = request.user_agent.to_s.include?('Hotwire Native Android')
|
||||
end
|
||||
|
||||
def with_locale(&)
|
||||
return yield unless current_account
|
||||
|
||||
@@ -100,6 +125,10 @@ class ApplicationController < ActionController::Base
|
||||
current_user&.account
|
||||
end
|
||||
|
||||
def true_ability
|
||||
@true_ability ||= Ability.new(true_user)
|
||||
end
|
||||
|
||||
def maybe_redirect_to_setup
|
||||
redirect_to setup_index_path unless User.exists?
|
||||
end
|
||||
@@ -123,4 +152,21 @@ class ApplicationController < ActionController::Base
|
||||
|
||||
redirect_to request.url.gsub('.co/', '.com/'), allow_other_host: true, status: :moved_permanently
|
||||
end
|
||||
|
||||
def set_csp
|
||||
request.content_security_policy = current_content_security_policy.tap do |policy|
|
||||
policy.default_src :self
|
||||
policy.script_src :self
|
||||
policy.style_src :self, :unsafe_inline
|
||||
policy.img_src :self, :https, :http, :blob, :data
|
||||
policy.font_src :self, :https, :http, :blob, :data
|
||||
policy.manifest_src :self
|
||||
policy.media_src :self
|
||||
policy.frame_src :self
|
||||
policy.worker_src :self, :blob
|
||||
policy.connect_src :self
|
||||
|
||||
policy.directives['connect-src'] << 'ws:' if Rails.env.development?
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -17,8 +17,10 @@ class ConsoleRedirectController < ApplicationController
|
||||
scope: :console,
|
||||
exp: 1.minute.from_now.to_i)
|
||||
|
||||
path = Addressable::URI.parse(params[:redir]).path if params[:redir].to_s.starts_with?(Docuseal::CONSOLE_URL)
|
||||
redir_uri = Addressable::URI.parse(params[:redir])
|
||||
path = redir_uri.path if params[:redir].to_s.starts_with?(Docuseal::CONSOLE_URL)
|
||||
|
||||
redirect_to("#{Docuseal::CONSOLE_URL}#{path}?#{{ auth: }.to_query}", allow_other_host: true)
|
||||
redirect_to "#{Docuseal::CONSOLE_URL}#{path}?#{{ **redir_uri&.query_values, 'auth' => auth }.to_query}",
|
||||
allow_other_host: true
|
||||
end
|
||||
end
|
||||
@@ -3,7 +3,7 @@
|
||||
class EmailSmtpSettingsController < ApplicationController
|
||||
before_action :load_encrypted_config
|
||||
authorize_resource :encrypted_config, only: :index
|
||||
authorize_resource :encrypted_config, parent: false, only: :create
|
||||
authorize_resource :encrypted_config, parent: false, only: %i[create destroy]
|
||||
|
||||
def index; end
|
||||
|
||||
@@ -15,12 +15,18 @@ class EmailSmtpSettingsController < ApplicationController
|
||||
|
||||
redirect_to settings_email_index_path, notice: I18n.t('changes_have_been_saved')
|
||||
else
|
||||
render :index, status: :unprocessable_entity
|
||||
render :index, status: :unprocessable_content
|
||||
end
|
||||
rescue StandardError => e
|
||||
flash[:alert] = e.message
|
||||
|
||||
render :index, status: :unprocessable_entity
|
||||
render :index, status: :unprocessable_content
|
||||
end
|
||||
|
||||
def destroy
|
||||
@encrypted_config.destroy!
|
||||
|
||||
redirect_to settings_email_index_path, notice: I18n.t('smtp_settings_have_been_reset')
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
@@ -1,22 +0,0 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class EnquiriesController < ApplicationController
|
||||
skip_before_action :authenticate_user!
|
||||
skip_authorization_check
|
||||
|
||||
def create
|
||||
if params[:talk_to_sales] == 'on'
|
||||
Faraday.post(Docuseal::ENQUIRIES_URL,
|
||||
enquiry_params.merge(type: :talk_to_sales).to_json,
|
||||
'Content-Type' => 'application/json')
|
||||
end
|
||||
|
||||
head :ok
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def enquiry_params
|
||||
params.require(:user).permit(:email)
|
||||
end
|
||||
end
|
||||
@@ -5,8 +5,12 @@ class ErrorsController < ActionController::Base
|
||||
'This feature is available in Pro Edition: https://www.docuseal.com/pricing'
|
||||
|
||||
ENTERPRISE_PATHS = [
|
||||
'/submissions/html',
|
||||
'/api/submissions/html',
|
||||
'/templates/html',
|
||||
'/api/templates/html',
|
||||
'/submissions/pdf',
|
||||
'/api/submissions/pdf',
|
||||
'/templates/pdf',
|
||||
'/api/templates/pdf',
|
||||
'/templates/doc',
|
||||
@@ -36,7 +40,7 @@ class ErrorsController < ActionController::Base
|
||||
render json: { status: error_status_code, error: }.compact, status: error_status_code
|
||||
end
|
||||
|
||||
f.html { render error_status_code.to_s, status: error_status_code }
|
||||
f.any { render error_status_code.to_s, status: error_status_code }
|
||||
end
|
||||
end
|
||||
|
||||
@@ -47,7 +51,7 @@ class ErrorsController < ActionController::Base
|
||||
headers['Access-Control-Allow-Methods'] = 'POST, GET, PUT, PATCH, DELETE, OPTIONS'
|
||||
headers['Access-Control-Allow-Headers'] = '*'
|
||||
headers['Access-Control-Max-Age'] = '1728000'
|
||||
headers['Access-Control-Allow-Credentials'] = true
|
||||
headers['Access-Control-Allow-Credentials'] = 'true'
|
||||
end
|
||||
|
||||
def error_status_code
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
class EsignSettingsController < ApplicationController
|
||||
DEFAULT_CERT_NAME = 'DocuSeal Self-Host Autogenerated'
|
||||
|
||||
CertFormRecord = Struct.new(:name, :file, :password, keyword_init: true) do
|
||||
CertFormRecord = Struct.new(:name, :file, :password) do
|
||||
include ActiveModel::Validations
|
||||
|
||||
def to_key
|
||||
@@ -14,8 +14,7 @@ class EsignSettingsController < ApplicationController
|
||||
prepend_before_action :maybe_redirect_com, only: %i[show]
|
||||
|
||||
before_action :load_encrypted_config
|
||||
authorize_resource :encrypted_config, parent: false, only: %i[new create]
|
||||
authorize_resource :encrypted_config, only: %i[update destroy show]
|
||||
authorize_resource :encrypted_config, parent: false
|
||||
|
||||
def show
|
||||
cert_data = @encrypted_config.value || {}
|
||||
@@ -53,7 +52,7 @@ class EsignSettingsController < ApplicationController
|
||||
@cert_record.errors.add(:name, I18n.t('already_exists'))
|
||||
|
||||
return render turbo_stream: turbo_stream.replace(:modal, template: 'esign_settings/new'),
|
||||
status: :unprocessable_entity
|
||||
status: :unprocessable_content
|
||||
end
|
||||
|
||||
save_new_cert!(@encrypted_config, @cert_record)
|
||||
@@ -64,7 +63,7 @@ class EsignSettingsController < ApplicationController
|
||||
|
||||
@cert_record.errors.add(:password, e.message)
|
||||
|
||||
render turbo_stream: turbo_stream.replace(:modal, template: 'esign_settings/new'), status: :unprocessable_entity
|
||||
render turbo_stream: turbo_stream.replace(:modal, template: 'esign_settings/new'), status: :unprocessable_content
|
||||
end
|
||||
|
||||
def update
|
||||
|
||||
@@ -1,4 +1,11 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class InvitationsController < Devise::PasswordsController
|
||||
def update
|
||||
super do |resource|
|
||||
resource.confirmed_at ||= Time.current if resource.errors.empty?
|
||||
|
||||
PasswordsController::Current.user = resource
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,92 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Mcp
|
||||
class CreateTemplateController < McpBaseController
|
||||
SCHEMA = {
|
||||
name: 'create_template',
|
||||
title: 'Create Template',
|
||||
description: 'Create a document template. Provide a URL to upload a PDF/DOCX file, or provide only a name ' \
|
||||
'to create an empty template and receive an edit URL where the file can be uploaded via the UI.',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
name: {
|
||||
type: 'string',
|
||||
description: 'Template name (used as the template name and required when url is not provided)'
|
||||
},
|
||||
url: {
|
||||
type: 'string',
|
||||
description: 'Optional URL of a PDF or DOCX file to upload. If omitted, an empty template is ' \
|
||||
'created and the returned edit_url can be used to upload a file via the UI.'
|
||||
}
|
||||
},
|
||||
required: %w[name]
|
||||
},
|
||||
annotations: {
|
||||
readOnlyHint: false,
|
||||
destructiveHint: false,
|
||||
idempotentHint: false,
|
||||
openWorldHint: true
|
||||
}
|
||||
}.freeze
|
||||
|
||||
# rubocop:disable Metrics/AbcSize, Metrics/MethodLength
|
||||
def call
|
||||
account = current_user.account
|
||||
|
||||
@template = Template.new(
|
||||
account:,
|
||||
author: current_user,
|
||||
folder: account.default_template_folder,
|
||||
source: :mcp,
|
||||
name: mcp_params['name'].to_s.presence || 'New Template',
|
||||
fields: [],
|
||||
schema: []
|
||||
)
|
||||
|
||||
authorize!(:create, @template)
|
||||
|
||||
Templates.maybe_assign_access(@template)
|
||||
|
||||
if mcp_params['url'].present?
|
||||
tempfile = Tempfile.new
|
||||
tempfile.binmode
|
||||
tempfile.write(DownloadUtils.call(mcp_params['url'], validate: true).body)
|
||||
tempfile.rewind
|
||||
|
||||
filename = File.basename(URI.decode_www_form_component(mcp_params['url']))
|
||||
|
||||
file = ActionDispatch::Http::UploadedFile.new(
|
||||
tempfile:,
|
||||
filename:,
|
||||
type: Marcel::MimeType.for(tempfile)
|
||||
)
|
||||
|
||||
@template.name = mcp_params['name'].presence || File.basename(filename, '.*')
|
||||
@template.save!
|
||||
|
||||
documents, = Templates::CreateAttachments.call(@template, { files: [file] }, extract_fields: true)
|
||||
schema = documents.map { |doc| { attachment_uuid: doc.uuid, name: doc.filename.base } }
|
||||
|
||||
if @template.fields.blank?
|
||||
@template.fields = Templates::ProcessDocument.normalize_attachment_fields(@template, documents)
|
||||
end
|
||||
|
||||
@template.update!(schema:)
|
||||
else
|
||||
@template.save!
|
||||
end
|
||||
|
||||
WebhookUrls.enqueue_events(@template, 'template.created')
|
||||
|
||||
SearchEntries.enqueue_reindex(@template)
|
||||
|
||||
render_tool_result(
|
||||
id: @template.id,
|
||||
name: @template.name,
|
||||
edit_url: edit_template_url(@template)
|
||||
)
|
||||
end
|
||||
# rubocop:enable Metrics/AbcSize, Metrics/MethodLength
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,54 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Mcp
|
||||
class LoadTemplateController < McpBaseController
|
||||
SCHEMA = {
|
||||
name: 'load_template',
|
||||
title: 'Load Template',
|
||||
description: 'Load a template with its fields. Each field includes name, type, and the signing role name.',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
template_id: {
|
||||
type: 'integer',
|
||||
description: 'Template identifier'
|
||||
}
|
||||
},
|
||||
required: %w[template_id]
|
||||
},
|
||||
annotations: {
|
||||
readOnlyHint: true,
|
||||
destructiveHint: false,
|
||||
idempotentHint: true,
|
||||
openWorldHint: false
|
||||
}
|
||||
}.freeze
|
||||
|
||||
def call
|
||||
@template = Template.accessible_by(current_ability).find(mcp_params['template_id'])
|
||||
|
||||
authorize!(:read, @template)
|
||||
|
||||
submitters_index = @template.submitters.index_by { |s| s['uuid'] }
|
||||
|
||||
roles = @template.submitters.pluck('name')
|
||||
|
||||
fields = @template.fields.filter_map do |field|
|
||||
next if field['name'].blank?
|
||||
|
||||
{
|
||||
name: field['name'],
|
||||
type: field['type'],
|
||||
role: submitters_index[field['submitter_uuid']]&.dig('name')
|
||||
}
|
||||
end
|
||||
|
||||
render_tool_result(
|
||||
id: @template.id,
|
||||
name: @template.name,
|
||||
roles: roles,
|
||||
fields: fields
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,81 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Mcp
|
||||
class McpBaseController < ActionController::API
|
||||
wrap_parameters false
|
||||
|
||||
before_action :authenticate_user!
|
||||
before_action :verify_mcp_enabled!
|
||||
check_authorization
|
||||
|
||||
before_action do
|
||||
raise CanCan::AccessDenied unless can?(:manage, :mcp)
|
||||
end
|
||||
|
||||
rescue_from CanCan::AccessDenied do
|
||||
render_error(-32_603, 'Forbidden', status: :forbidden)
|
||||
end
|
||||
|
||||
rescue_from ActiveRecord::RecordNotFound do
|
||||
render_tool_error('Not found')
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def default_url_options
|
||||
Docuseal.default_url_options
|
||||
end
|
||||
|
||||
def mcp_body
|
||||
request.request_parameters
|
||||
end
|
||||
|
||||
def mcp_params
|
||||
mcp_body.dig('params', 'arguments') || {}
|
||||
end
|
||||
|
||||
def render_result(result)
|
||||
render json: { jsonrpc: '2.0', id: mcp_body['id'], result: }
|
||||
end
|
||||
|
||||
def render_error(code, message, id: nil, status: :ok)
|
||||
render json: { jsonrpc: '2.0', id:, error: { code:, message: } }, status:
|
||||
end
|
||||
|
||||
def render_tool_result(data)
|
||||
render_result(content: [{ type: 'text', text: data.to_json }])
|
||||
end
|
||||
|
||||
def render_tool_error(message)
|
||||
render_result(content: [{ type: 'text', text: message }], isError: true)
|
||||
end
|
||||
|
||||
def authenticate_user!
|
||||
render json: { error: 'Not authenticated' }, status: :unauthorized unless current_user
|
||||
end
|
||||
|
||||
def verify_mcp_enabled!
|
||||
return if Docuseal.multitenant?
|
||||
|
||||
return if AccountConfig.exists?(account_id: current_user.account_id,
|
||||
key: AccountConfig::ENABLE_MCP_KEY,
|
||||
value: true)
|
||||
|
||||
render json: { error: 'MCP is disabled' }, status: :forbidden
|
||||
end
|
||||
|
||||
def current_user
|
||||
@current_user ||= user_from_api_key
|
||||
end
|
||||
|
||||
def user_from_api_key
|
||||
token = request.headers['Authorization'].to_s[/\ABearer\s+(.+)\z/, 1]
|
||||
|
||||
return if token.blank?
|
||||
|
||||
sha256 = Digest::SHA256.hexdigest(token)
|
||||
|
||||
User.joins(:mcp_tokens).active.find_by(mcp_tokens: { sha256:, archived_at: nil })
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,50 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Mcp
|
||||
class ProtocolController < McpBaseController
|
||||
skip_authorization_check
|
||||
|
||||
def ok
|
||||
head :ok
|
||||
end
|
||||
|
||||
def initialize_request
|
||||
render_result(
|
||||
protocolVersion: '2025-11-25',
|
||||
serverInfo: {
|
||||
name: 'DocuSeal',
|
||||
version: Docuseal.version.to_s
|
||||
},
|
||||
capabilities: {
|
||||
tools: {
|
||||
listChanged: false
|
||||
}
|
||||
}
|
||||
)
|
||||
end
|
||||
|
||||
def initialized_notification
|
||||
head :accepted
|
||||
end
|
||||
|
||||
def ping
|
||||
render_result({})
|
||||
end
|
||||
|
||||
def tools_list
|
||||
render_result(tools: McpController::TOOLS)
|
||||
end
|
||||
|
||||
def method_not_found
|
||||
render_error(-32_601, "Method not found: #{mcp_body['method']}", id: mcp_body['id'])
|
||||
end
|
||||
|
||||
def tool_not_found
|
||||
render_error(-32_602, "Unknown tool: #{mcp_body.dig('params', 'name')}", id: mcp_body['id'])
|
||||
end
|
||||
|
||||
def parse_error
|
||||
render_error(-32_700, 'Parse error', status: :bad_request)
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,59 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Mcp
|
||||
class SearchDocumentsController < McpBaseController
|
||||
SCHEMA = {
|
||||
name: 'search_documents',
|
||||
title: 'Search Documents',
|
||||
description: 'Search signed or pending documents by submitter name, email, phone, or template name',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
q: {
|
||||
type: 'string',
|
||||
description: 'Search by submitter name, email, phone, or template name'
|
||||
},
|
||||
limit: {
|
||||
type: 'integer',
|
||||
description: 'The number of results to return (default 10)'
|
||||
}
|
||||
},
|
||||
required: %w[q]
|
||||
},
|
||||
annotations: {
|
||||
readOnlyHint: true,
|
||||
destructiveHint: false,
|
||||
idempotentHint: true,
|
||||
openWorldHint: false
|
||||
}
|
||||
}.freeze
|
||||
|
||||
def call
|
||||
authorize!(:read, Submission)
|
||||
|
||||
submissions = Submissions.search(current_user, Submission.accessible_by(current_ability).active,
|
||||
mcp_params['q'], search_template: true)
|
||||
|
||||
limit = mcp_params.fetch('limit', 10).to_i
|
||||
limit = 10 if limit <= 0
|
||||
limit = [limit, 100].min
|
||||
submissions = submissions.preload(:submitters, :template)
|
||||
.order(id: :desc)
|
||||
.limit(limit)
|
||||
|
||||
data = submissions.map do |submission|
|
||||
{
|
||||
id: submission.id,
|
||||
template_name: submission.template&.name,
|
||||
status: Submissions::SerializeForApi.build_status(submission, submission.submitters),
|
||||
submitters: submission.submitters.map do |s|
|
||||
{ email: s.email, name: s.name, phone: s.phone, status: s.status }
|
||||
end,
|
||||
documents_url: submission_url(submission.id)
|
||||
}
|
||||
end
|
||||
|
||||
render_tool_result(data)
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,44 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Mcp
|
||||
class SearchTemplatesController < McpBaseController
|
||||
SCHEMA = {
|
||||
name: 'search_templates',
|
||||
title: 'Search Templates',
|
||||
description: 'Search document templates by name',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
q: {
|
||||
type: 'string',
|
||||
description: 'Search query to filter templates by name'
|
||||
},
|
||||
limit: {
|
||||
type: 'integer',
|
||||
description: 'The number of templates to return (default 10)'
|
||||
}
|
||||
},
|
||||
required: %w[q]
|
||||
},
|
||||
annotations: {
|
||||
readOnlyHint: true,
|
||||
destructiveHint: false,
|
||||
idempotentHint: true,
|
||||
openWorldHint: false
|
||||
}
|
||||
}.freeze
|
||||
|
||||
def call
|
||||
authorize!(:read, Template)
|
||||
|
||||
templates = Templates.search(current_user, Template.accessible_by(current_ability).active, mcp_params['q'])
|
||||
|
||||
limit = mcp_params.fetch('limit', 10).to_i
|
||||
limit = 10 if limit <= 0
|
||||
limit = [limit, 100].min
|
||||
templates = templates.order(id: :desc).limit(limit)
|
||||
|
||||
render_tool_result(templates.map { |t| { id: t.id, name: t.name } })
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,120 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Mcp
|
||||
class SendDocumentsController < McpBaseController
|
||||
SCHEMA = {
|
||||
name: 'send_documents',
|
||||
title: 'Send Documents',
|
||||
description: 'Send a document template for signing to specified submitters',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
template_id: {
|
||||
type: 'integer',
|
||||
description: 'Template identifier'
|
||||
},
|
||||
submitters: {
|
||||
type: 'array',
|
||||
description: 'The list of submitters (signers)',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
email: {
|
||||
type: 'string',
|
||||
description: 'Submitter email address'
|
||||
},
|
||||
name: {
|
||||
type: 'string',
|
||||
description: 'Submitter name'
|
||||
},
|
||||
phone: {
|
||||
type: 'string',
|
||||
description: 'Submitter phone number in E.164 format'
|
||||
},
|
||||
role: {
|
||||
type: 'string',
|
||||
description: 'Signing role name from the template'
|
||||
},
|
||||
fields: {
|
||||
type: 'array',
|
||||
description: 'Prefill field values for this submitter (fields become readonly)',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
name: {
|
||||
type: 'string',
|
||||
description: 'Field name'
|
||||
},
|
||||
value: {
|
||||
description: 'Prefilled value for the field'
|
||||
}
|
||||
},
|
||||
required: %w[name value]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
required: %w[template_id submitters]
|
||||
},
|
||||
annotations: {
|
||||
readOnlyHint: false,
|
||||
destructiveHint: true,
|
||||
idempotentHint: false,
|
||||
openWorldHint: true
|
||||
}
|
||||
}.freeze
|
||||
|
||||
# rubocop:disable Metrics
|
||||
def call
|
||||
@template = Template.accessible_by(current_ability).find(mcp_params['template_id'])
|
||||
|
||||
authorize!(:read, @template)
|
||||
|
||||
return render_tool_error('Template has been archived') if @template.archived_at?
|
||||
|
||||
authorize!(:create, Submission.new(template: @template, account_id: current_user.account_id))
|
||||
|
||||
return render_tool_error('Template has no fields') if @template.fields.blank?
|
||||
|
||||
submitters = (mcp_params['submitters'] || []).map do |s|
|
||||
attrs = s.slice('email', 'name', 'role', 'phone').compact_blank
|
||||
|
||||
fields = Array.wrap(s['fields']).filter_map do |f|
|
||||
next if f['name'].blank?
|
||||
|
||||
{ 'name' => f['name'], 'default_value' => f['value'], 'readonly' => true }
|
||||
end
|
||||
|
||||
attrs['fields'] = fields if fields.present?
|
||||
|
||||
attrs.with_indifferent_access
|
||||
end
|
||||
|
||||
submissions = Submissions.create_from_submitters(
|
||||
template: @template,
|
||||
user: current_user,
|
||||
source: :mcp,
|
||||
submitters_order: @template.preferences['submitters_order'].presence || 'random',
|
||||
submissions_attrs: { submitters: },
|
||||
params: { 'send_email' => true, 'submitters' => submitters }
|
||||
)
|
||||
|
||||
return render_tool_error('No valid submitters provided') if submissions.blank?
|
||||
|
||||
WebhookUrls.enqueue_events(submissions, 'submission.created')
|
||||
|
||||
Submissions.send_signature_requests(submissions)
|
||||
|
||||
SearchEntries.enqueue_reindex(submissions)
|
||||
|
||||
submission = submissions.first
|
||||
|
||||
render_tool_result(id: submission.id, status: 'pending')
|
||||
rescue Submissions::CreateFromSubmitters::BaseError => e
|
||||
render_tool_error(e.message)
|
||||
end
|
||||
# rubocop:enable Metrics
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,44 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class McpController < ActionController::Metal
|
||||
TOOL_CONTROLLERS = {
|
||||
'search_templates' => Mcp::SearchTemplatesController,
|
||||
'load_template' => Mcp::LoadTemplateController,
|
||||
'create_template' => Mcp::CreateTemplateController,
|
||||
'send_documents' => Mcp::SendDocumentsController,
|
||||
'search_documents' => Mcp::SearchDocumentsController
|
||||
}.freeze
|
||||
|
||||
TOOLS = TOOL_CONTROLLERS.map { |_, controller| controller::SCHEMA }.freeze
|
||||
|
||||
def call
|
||||
return Mcp::ProtocolController.dispatch(:ok, request, response) if request.raw_post.blank?
|
||||
|
||||
body = JSON.parse(request.raw_post)
|
||||
body = nil unless body.is_a?(Hash)
|
||||
|
||||
request.request_parameters = body || {}
|
||||
|
||||
action =
|
||||
case body&.dig('method')
|
||||
when 'initialize' then :initialize_request
|
||||
when 'notifications/initialized' then :initialized_notification
|
||||
when 'ping' then :ping
|
||||
when 'tools/list' then :tools_list
|
||||
when 'tools/call'
|
||||
tool = TOOL_CONTROLLERS[body.dig('params', 'name')]
|
||||
|
||||
return tool.dispatch(:call, request, response) if tool
|
||||
|
||||
:tool_not_found
|
||||
else
|
||||
:method_not_found
|
||||
end
|
||||
|
||||
Mcp::ProtocolController.dispatch(action, request, response)
|
||||
rescue JSON::ParserError
|
||||
request.request_parameters = {}
|
||||
|
||||
Mcp::ProtocolController.dispatch(:parse_error, request, response)
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,37 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class McpSettingsController < ApplicationController
|
||||
load_and_authorize_resource :mcp_token, parent: false
|
||||
|
||||
before_action do
|
||||
authorize!(:manage, :mcp)
|
||||
end
|
||||
|
||||
def index
|
||||
@mcp_tokens = @mcp_tokens.active.order(id: :desc)
|
||||
end
|
||||
|
||||
def create
|
||||
@mcp_token = current_user.mcp_tokens.new(mcp_token_params)
|
||||
|
||||
if @mcp_token.save
|
||||
@mcp_tokens = [@mcp_token]
|
||||
|
||||
render :index, status: :created
|
||||
else
|
||||
render turbo_stream: turbo_stream.replace(:modal, template: 'mcp_settings/new'), status: :unprocessable_content
|
||||
end
|
||||
end
|
||||
|
||||
def destroy
|
||||
@mcp_token.update!(archived_at: Time.current)
|
||||
|
||||
redirect_back fallback_location: settings_mcp_index_path, notice: I18n.t('mcp_token_has_been_removed')
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def mcp_token_params
|
||||
params.require(:mcp_token).permit(:name)
|
||||
end
|
||||
end
|
||||
@@ -14,6 +14,8 @@ class MfaSetupController < ApplicationController
|
||||
def edit; end
|
||||
|
||||
def create
|
||||
RateLimit.call("mfa-setup-otp-#{current_user.id}", limit: 5, ttl: 5.minutes, enabled: true)
|
||||
|
||||
if current_user.validate_and_consume_otp!(params[:otp_attempt])
|
||||
current_user.otp_required_for_login = true
|
||||
current_user.save!
|
||||
@@ -24,19 +26,21 @@ class MfaSetupController < ApplicationController
|
||||
|
||||
@error_message = I18n.t('code_is_invalid')
|
||||
|
||||
render turbo_stream: turbo_stream.replace(:mfa_form, partial: 'mfa_setup/form'), status: :unprocessable_entity
|
||||
render turbo_stream: turbo_stream.replace(:mfa_form, partial: 'mfa_setup/form'), status: :unprocessable_content
|
||||
end
|
||||
end
|
||||
|
||||
def destroy
|
||||
if current_user.validate_and_consume_otp!(params[:otp_attempt])
|
||||
RateLimit.call("mfa-setup-otp-#{current_user.id}", limit: 5, ttl: 5.minutes, enabled: true)
|
||||
|
||||
if current_user.valid_for_authentication? { current_user.validate_and_consume_otp!(params[:otp_attempt]) }
|
||||
current_user.update!(otp_required_for_login: false, otp_secret: nil)
|
||||
|
||||
redirect_to settings_profile_index_path, notice: I18n.t('2fa_has_been_removed')
|
||||
else
|
||||
@error_message = I18n.t('code_is_invalid')
|
||||
|
||||
render turbo_stream: turbo_stream.replace(:modal, template: 'mfa_setup/edit'), status: :unprocessable_entity
|
||||
render turbo_stream: turbo_stream.replace(:modal, template: 'mfa_setup/edit'), status: :unprocessable_content
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ class NotificationsSettingsController < ApplicationController
|
||||
|
||||
before_action :build_account_config, only: :create
|
||||
authorize_resource :account_config, only: :create
|
||||
before_action :authorize_email_reminders!, only: :create
|
||||
|
||||
def index; end
|
||||
|
||||
@@ -21,6 +22,14 @@ class NotificationsSettingsController < ApplicationController
|
||||
|
||||
private
|
||||
|
||||
def authorize_email_reminders!
|
||||
return unless Docuseal.multitenant?
|
||||
return if @account_config.key != AccountConfig::SUBMITTER_REMINDERS
|
||||
return if can?(:manage, :email_reminders)
|
||||
|
||||
redirect_back fallback_location: settings_notifications_path, alert: I18n.t('unlock_with_docuseal_pro')
|
||||
end
|
||||
|
||||
def build_account_config
|
||||
@account_config =
|
||||
AccountConfig.find_or_initialize_by(account: current_account, key: email_config_params[:key])
|
||||
|
||||
@@ -1,6 +1,12 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class PasswordsController < Devise::PasswordsController
|
||||
# rubocop:disable Rails/LexicallyScopedActionFilter
|
||||
skip_before_action :require_no_authentication, only: %i[edit update]
|
||||
# rubocop:enable Rails/LexicallyScopedActionFilter
|
||||
|
||||
around_action :with_browser_locale
|
||||
|
||||
class Current < ActiveSupport::CurrentAttributes
|
||||
attribute :user
|
||||
end
|
||||
@@ -16,4 +22,10 @@ class PasswordsController < Devise::PasswordsController
|
||||
Current.user = resource
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def after_resetting_password_path_for(_)
|
||||
new_session_path(resource_name)
|
||||
end
|
||||
end
|
||||
@@ -4,6 +4,7 @@ class PersonalizationSettingsController < ApplicationController
|
||||
ALLOWED_KEYS = [
|
||||
AccountConfig::FORM_COMPLETED_BUTTON_KEY,
|
||||
AccountConfig::SUBMITTER_INVITATION_EMAIL_KEY,
|
||||
AccountConfig::SUBMITTER_INVITATION_REMINDER_EMAIL_KEY,
|
||||
AccountConfig::SUBMITTER_DOCUMENTS_COPY_EMAIL_KEY,
|
||||
AccountConfig::SUBMITTER_COMPLETED_EMAIL_KEY,
|
||||
AccountConfig::FORM_COMPLETED_MESSAGE_KEY,
|
||||
|
||||
@@ -5,48 +5,93 @@ class PreviewDocumentPageController < ActionController::API
|
||||
|
||||
FORMAT = Templates::ProcessDocument::FORMAT
|
||||
|
||||
def show
|
||||
attachment_uuid = ApplicationRecord.signed_id_verifier.verified(params[:signed_uuid], purpose: :attachment)
|
||||
TMPFILE_PREFIX = 'attachment-'
|
||||
TMPFILE_TTL = 5.minutes
|
||||
TMPFILE_MAX_TOTAL_SIZE = Docuseal.multitenant? ? 400.megabytes : 1.gigabyte
|
||||
|
||||
attachment = ActiveStorage::Attachment.find_by(uuid: attachment_uuid) if attachment_uuid
|
||||
def show
|
||||
result_data =
|
||||
ApplicationRecord.signed_id_verifier.verified(params[:signed_key], purpose: :attachment)
|
||||
|
||||
attachment =
|
||||
if result_data.is_a?(Array) && result_data.compact_blank.size == 2
|
||||
attachment_id, attachment_uuid = result_data
|
||||
|
||||
ActiveStorage::Attachment.find_by(id: attachment_id, uuid: attachment_uuid)
|
||||
elsif result_data
|
||||
ActiveStorage::Attachment.find_by(uuid: result_data)
|
||||
end
|
||||
|
||||
return head :not_found unless attachment
|
||||
|
||||
@template = attachment.record
|
||||
|
||||
preview_image = attachment.preview_images.joins(:blob)
|
||||
.find_by(blob: { filename: ["#{params[:id]}.png", "#{params[:id]}.jpg"] })
|
||||
|
||||
return redirect_to preview_image.url, allow_other_host: true if preview_image
|
||||
|
||||
file_path =
|
||||
if attachment.service.name == :disk
|
||||
ActiveStorage::Blob.service.path_for(attachment.key)
|
||||
else
|
||||
find_or_create_document_tempfile_path(attachment)
|
||||
end
|
||||
|
||||
preview_image =
|
||||
Templates::ProcessDocument.generate_pdf_preview_from_file(attachment, file_path, params[:id].to_i)
|
||||
attachment.preview_images.joins(:blob)
|
||||
.find_by(blob: { filename: ["#{params[:id].to_i}.png", "#{params[:id].to_i}.jpg"] })
|
||||
|
||||
redirect_to preview_image.url, allow_other_host: true
|
||||
end
|
||||
|
||||
def find_or_create_document_tempfile_path(attachment)
|
||||
file_path = "#{Dir.tmpdir}/#{attachment.uuid}"
|
||||
|
||||
File.open(file_path, File::RDWR | File::CREAT, 0o644) do |f|
|
||||
f.flock(File::LOCK_EX)
|
||||
|
||||
# rubocop:disable Style/ZeroLengthPredicate
|
||||
if f.size.zero?
|
||||
f.binmode
|
||||
|
||||
f.write(attachment.download)
|
||||
end
|
||||
# rubocop:enable Style/ZeroLengthPredicate
|
||||
if preview_image
|
||||
return redirect_to preview_image.url(time: ActiveStorage::Attachment.service_url_time),
|
||||
allow_other_host: true
|
||||
end
|
||||
|
||||
file_path
|
||||
preview_image =
|
||||
open_attachment_io(attachment) do |io|
|
||||
Templates::ProcessDocument.generate_pdf_preview_from_io(attachment, io, params[:id].to_i)
|
||||
end
|
||||
|
||||
redirect_to preview_image.url(time: ActiveStorage::Attachment.service_url_time), allow_other_host: true
|
||||
end
|
||||
|
||||
def open_attachment_io(attachment, &)
|
||||
return File.open(ActiveStorage::Blob.service.path_for(attachment.key), 'rb', &) if attachment.service.name == :disk
|
||||
|
||||
file_name = "#{TMPFILE_PREFIX}#{Digest::SHA1.hexdigest("#{attachment.id}-#{attachment.uuid}")}"
|
||||
|
||||
file_path = File.join(Dir.tmpdir, file_name)
|
||||
|
||||
File.open(file_path, File::RDWR | File::CREAT, 0o644) do |file|
|
||||
file.flock(File::LOCK_EX)
|
||||
|
||||
# rubocop:disable Style/ZeroLengthPredicate
|
||||
if file.size.zero?
|
||||
cleanup_stale_tempfiles
|
||||
|
||||
file.binmode
|
||||
|
||||
file.write(attachment.download)
|
||||
else
|
||||
FileUtils.touch(file_path)
|
||||
end
|
||||
# rubocop:enable Style/ZeroLengthPredicate
|
||||
|
||||
file.flock(File::LOCK_UN)
|
||||
|
||||
yield file
|
||||
end
|
||||
end
|
||||
|
||||
def cleanup_stale_tempfiles
|
||||
entries =
|
||||
Dir.glob(File.join(Dir.tmpdir, "#{TMPFILE_PREFIX}*")).filter_map do |path|
|
||||
stat = File.stat(path)
|
||||
|
||||
[path, stat.mtime, stat.size]
|
||||
rescue Errno::ENOENT
|
||||
nil
|
||||
end
|
||||
|
||||
total_size = entries.sum(&:last)
|
||||
stale_time = TMPFILE_TTL.ago
|
||||
|
||||
entries.sort_by(&:second).each do |path, mtime, size|
|
||||
break if mtime > stale_time && total_size <= TMPFILE_MAX_TOTAL_SIZE
|
||||
|
||||
File.unlink(path)
|
||||
|
||||
total_size -= size
|
||||
rescue Errno::ENOENT
|
||||
nil
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -9,18 +9,25 @@ class ProfileController < ApplicationController
|
||||
|
||||
def update_contact
|
||||
if current_user.update(contact_params)
|
||||
redirect_to settings_profile_index_path, notice: I18n.t('contact_information_has_been_update')
|
||||
if current_user.try(:pending_reconfirmation?) && current_user.previous_changes.key?(:unconfirmed_email)
|
||||
SendConfirmationInstructionsJob.perform_async('user_id' => current_user.id)
|
||||
|
||||
redirect_to settings_profile_index_path,
|
||||
notice: I18n.t('a_confirmation_email_has_been_sent_to_the_new_email_address')
|
||||
else
|
||||
redirect_to settings_profile_index_path, notice: I18n.t('contact_information_has_been_update')
|
||||
end
|
||||
else
|
||||
render :index, status: :unprocessable_entity
|
||||
render :index, status: :unprocessable_content
|
||||
end
|
||||
end
|
||||
|
||||
def update_password
|
||||
if current_user.update(password_params)
|
||||
if current_user.update_with_password(password_params)
|
||||
bypass_sign_in(current_user)
|
||||
redirect_to settings_profile_index_path, notice: I18n.t('password_has_been_changed')
|
||||
else
|
||||
render :index, status: :unprocessable_entity
|
||||
render :index, status: :unprocessable_content
|
||||
end
|
||||
end
|
||||
|
||||
@@ -31,6 +38,6 @@ class ProfileController < ApplicationController
|
||||
end
|
||||
|
||||
def password_params
|
||||
params.require(:user).permit(:password, :password_confirmation)
|
||||
params.require(:user).permit(:password, :password_confirmation, :current_password)
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,29 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class RevealAccessTokenController < ApplicationController
|
||||
rate_limit to: 4, within: 1.minute, only: %i[create], by: -> { current_user.id }, with: lambda {
|
||||
Rollbar.error('Rate limit api key') if defined?(Rollbar)
|
||||
|
||||
render turbo_stream: turbo_stream.replace(:modal, template: 'reveal_access_token/show',
|
||||
locals: { error_message: I18n.t(:too_many_attempts) }),
|
||||
status: :unprocessable_content
|
||||
}
|
||||
|
||||
def show
|
||||
authorize!(:manage, current_user.access_token)
|
||||
end
|
||||
|
||||
def create
|
||||
authorize!(:manage, current_user.access_token)
|
||||
|
||||
if current_user.valid_password?(params[:password])
|
||||
render turbo_stream: turbo_stream.replace(:access_token_container,
|
||||
partial: 'reveal_access_token/access_token',
|
||||
locals: { token: current_user.access_token.token })
|
||||
else
|
||||
render turbo_stream: turbo_stream.replace(:modal, template: 'reveal_access_token/show',
|
||||
locals: { error_message: I18n.t('wrong_password') }),
|
||||
status: :unprocessable_content
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,132 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class SearchController < ApplicationController
|
||||
load_and_authorize_resource :template_folder, parent: false
|
||||
load_and_authorize_resource :template, parent: false
|
||||
load_and_authorize_resource :submission, parent: false
|
||||
load_and_authorize_resource :template, instance_name: :filter_template, id_param: :template_id, parent: true
|
||||
|
||||
FOLDERS_LIMIT = 4
|
||||
LIMIT = 12
|
||||
|
||||
def index
|
||||
@query = params[:q].to_s.squish
|
||||
|
||||
return render_infinite_scroll(@template_folders, @templates, @submissions, @query) if turbo_infinite_scroll?
|
||||
|
||||
if @query.blank?
|
||||
@template_folders = @templates = @submissions = []
|
||||
elsif params[:scope] == 'folders'
|
||||
@folders_pagy, @template_folders =
|
||||
pagy(:countless, filter_folders(@template_folders, @templates, @query), limit: LIMIT)
|
||||
|
||||
@templates = @submissions = []
|
||||
else
|
||||
folders = filter_folders(@template_folders, @templates, @query)
|
||||
|
||||
@pagy, @template_folders, @templates, @submissions =
|
||||
if params[:scope] == 'submissions'
|
||||
load_submissions_page(folders, @templates, @submissions, @query)
|
||||
else
|
||||
load_templates_page(folders, @templates, @submissions, @query)
|
||||
end
|
||||
|
||||
if @templates.blank? && @submissions.blank? && params[:template_id].blank?
|
||||
@folders_pagy, @template_folders = pagy(:countless, folders, limit: LIMIT)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def load_submissions_page(folders, templates, submissions, query)
|
||||
pagy, submissions = pagy(:countless, filter_submissions(submissions, query), limit: LIMIT)
|
||||
|
||||
return [pagy, [], [], submissions] if pagy.next || params[:template_id].present?
|
||||
|
||||
[pagy, folders.limit(FOLDERS_LIMIT).to_a, filter_templates(templates, query).limit(LIMIT).to_a, submissions]
|
||||
end
|
||||
|
||||
def load_templates_page(folders, templates, submissions, query)
|
||||
pagy, templates = pagy(:countless, filter_templates(templates, query), limit: LIMIT)
|
||||
|
||||
submissions = pagy.next ? [] : filter_submissions(submissions, query).limit(LIMIT).to_a
|
||||
|
||||
[pagy, folders.limit(FOLDERS_LIMIT).to_a, templates, submissions]
|
||||
end
|
||||
|
||||
def render_infinite_scroll(folders, templates, submissions, query)
|
||||
target, partial, collection =
|
||||
case params[:scope]
|
||||
when 'folders'
|
||||
['search_folders', 'search/folder', filter_folders(folders, templates, query)]
|
||||
when 'submissions'
|
||||
['search_submissions', 'search/submission', filter_submissions(submissions, query)]
|
||||
else
|
||||
['search_templates', 'search/template', filter_templates(templates, query)]
|
||||
end
|
||||
|
||||
pagy, records = pagy(:countless, collection, limit: LIMIT)
|
||||
|
||||
render turbo_stream: [
|
||||
turbo_stream.append(target, partial:, collection: records),
|
||||
turbo_stream.replace('infinite_scroll', partial: 'shared/infinite_scroll', locals: { pagy: })
|
||||
]
|
||||
end
|
||||
|
||||
def filter_folders(folders, templates, query)
|
||||
return folders.none if params[:archived] == 'true'
|
||||
|
||||
folders = folders.where.not(id: current_account.default_template_folder.id)
|
||||
|
||||
folders = folders.where(parent_folder_id: filter_folder_ids(params[:folder])) if params[:folder].present?
|
||||
folders = folders.where(author_id: filter_author_id(params[:author])) if params[:author].present?
|
||||
folders = TemplateFolders.filter_active_folders(folders, templates)
|
||||
folders = TemplateFolders.search(folders, query)
|
||||
|
||||
folders.preload(:parent_folder).order(:name)
|
||||
end
|
||||
|
||||
def filter_templates(templates, query)
|
||||
templates = params[:archived] == 'true' ? templates.where.not(archived_at: nil) : templates.active
|
||||
templates = templates.select_for_list.preload(:author, :template_accesses, folder: :parent_folder)
|
||||
|
||||
templates = templates.where(folder_id: filter_folder_ids(params[:folder])) if params[:folder].present?
|
||||
templates = templates.where(author_id: filter_author_id(params[:author])) if params[:author].present?
|
||||
|
||||
Templates.search(current_user, templates, query).order(id: :desc)
|
||||
end
|
||||
|
||||
def filter_folder_ids(folder_name)
|
||||
TemplateFolders.filter_by_full_name(current_account.template_folders, folder_name)
|
||||
.preload(:subfolders)
|
||||
.flat_map { |folder| [folder.id, *folder.subfolders.map(&:id)] }
|
||||
end
|
||||
|
||||
def filter_author_id(email)
|
||||
current_account.users.find_by(email:)&.id || -1
|
||||
end
|
||||
|
||||
def filter_submissions(submissions, query)
|
||||
submissions = submissions.left_joins(:template)
|
||||
|
||||
submissions =
|
||||
if params[:archived] == 'true'
|
||||
submissions.where.not(archived_at: nil).or(submissions.where.not(templates: { archived_at: nil }))
|
||||
else
|
||||
submissions.where(archived_at: nil).where(templates: { archived_at: nil })
|
||||
end
|
||||
|
||||
submissions = Submissions.search(current_user, submissions, query, search_template: true)
|
||||
submissions = Submissions::Filter.call(submissions, current_user, params)
|
||||
|
||||
submissions =
|
||||
if params[:status] == 'completed' || params[:completed_at_from].present? || params[:completed_at_to].present?
|
||||
submissions.order(completed_at: :desc)
|
||||
else
|
||||
submissions.order(id: :desc)
|
||||
end
|
||||
|
||||
submissions.select_for_list.preload(:submitters, :template)
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,17 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class SearchEntriesReindexController < ApplicationController
|
||||
def create
|
||||
authorize!(:manage, EncryptedConfig)
|
||||
|
||||
ReindexAllSearchEntriesJob.perform_async
|
||||
|
||||
AccountConfig.find_or_initialize_by(account_id: Account.minimum(:id), key: :fulltext_search)
|
||||
.update!(value: true)
|
||||
|
||||
Docuseal.instance_variable_set(:@fulltext_search, nil)
|
||||
|
||||
redirect_back(fallback_location: settings_account_path,
|
||||
notice: "Started building search index. Visit #{root_url}jobs/busy to check progress.")
|
||||
end
|
||||
end
|
||||
@@ -11,22 +11,24 @@ class SendSubmissionEmailController < ApplicationController
|
||||
|
||||
def create
|
||||
if params[:template_slug]
|
||||
@submitter = Submitter.completed.joins(submission: :template).find_by!(email: params[:email].to_s.downcase,
|
||||
template: { slug: params[:template_slug] })
|
||||
template = Template.find_by!(slug: params[:template_slug])
|
||||
|
||||
@submitter =
|
||||
Submitter.completed.where(submission: template.submissions).find_by(email: params[:email].to_s.downcase)
|
||||
elsif params[:submission_slug]
|
||||
@submitter = Submitter.completed.joins(:submission).find_by(email: params[:email].to_s.downcase,
|
||||
submission: { slug: params[:submission_slug] })
|
||||
submission = Submission.find_by!(slug: params[:submission_slug])
|
||||
|
||||
@submitter = submission.submitters.order(:completed_at).find_by(email: params[:email].to_s.downcase)
|
||||
|
||||
return redirect_to submissions_preview_completed_path(params[:submission_slug], status: :error) unless @submitter
|
||||
else
|
||||
@submitter = Submitter.completed.find_by!(slug: params[:submitter_slug])
|
||||
@submitter = Submitter.find_by!(slug: params[:submitter_slug])
|
||||
end
|
||||
|
||||
RateLimit.call("send-email-#{@submitter.id}", limit: 2, ttl: 5.minutes)
|
||||
if @submitter && completed_submitter?(@submitter)
|
||||
RateLimit.call("send-email-#{@submitter.id}", limit: 2, ttl: 5.minutes)
|
||||
|
||||
unless EmailEvent.exists?(tag: :submitter_documents_copy, email: @submitter.email, emailable: @submitter,
|
||||
event_type: :send, created_at: SEND_DURATION.ago..Time.current)
|
||||
SubmitterMailer.documents_copy_email(@submitter, sig: true).deliver_later!
|
||||
SubmitterMailer.documents_copy_email(@submitter, sig: true).deliver_later! if can_send?(@submitter)
|
||||
end
|
||||
|
||||
respond_to do |f|
|
||||
@@ -34,4 +36,18 @@ class SendSubmissionEmailController < ApplicationController
|
||||
f.json { head :ok }
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def completed_submitter?(submitter)
|
||||
submitter.completed_at? || (submitter.viewer? && submitter.submission.completed_at?)
|
||||
end
|
||||
|
||||
def can_send?(submitter)
|
||||
return false if submitter.account.archived_at?
|
||||
return false if EmailEvent.exists?(tag: :submitter_documents_copy, email: submitter.email, emailable: submitter,
|
||||
event_type: :send, created_at: SEND_DURATION.ago..Time.current)
|
||||
|
||||
true
|
||||
end
|
||||
end
|
||||
@@ -3,6 +3,10 @@
|
||||
class SessionsController < Devise::SessionsController
|
||||
before_action :configure_permitted_parameters
|
||||
|
||||
skip_before_action :verify_authenticity_token, if: -> { request.xhr? && request.format.json? }
|
||||
|
||||
respond_to :json, only: :create
|
||||
|
||||
around_action :with_browser_locale
|
||||
|
||||
def create
|
||||
@@ -11,12 +15,26 @@ class SessionsController < Devise::SessionsController
|
||||
if Docuseal.multitenant? && !User.exists?(email:)
|
||||
Rollbar.warning('Sign in new user') if defined?(Rollbar)
|
||||
|
||||
return redirect_to new_registration_path(sign_up: true, user: sign_in_params.slice(:email)),
|
||||
notice: I18n.t('create_a_new_account')
|
||||
respond_to do |format|
|
||||
format.html do
|
||||
redirect_to new_registration_path(sign_up: true, user: sign_in_params.slice(:email)),
|
||||
notice: t('create_a_new_account')
|
||||
end
|
||||
format.json do
|
||||
render json: { sign_up: true, notice: t('create_a_new_account') }, status: :unprocessable_content
|
||||
end
|
||||
end
|
||||
|
||||
return
|
||||
end
|
||||
|
||||
if User.exists?(email:, otp_required_for_login: true) && sign_in_params[:otp_attempt].blank?
|
||||
return render :otp, locals: { resource: User.new(sign_in_params) }, status: :unprocessable_entity
|
||||
respond_to do |format|
|
||||
format.html { render :otp, locals: { resource: User.new(sign_in_params) }, status: :unprocessable_content }
|
||||
format.json { render json: { otp_required: true, email_otp: false }, status: :unprocessable_content }
|
||||
end
|
||||
|
||||
return
|
||||
end
|
||||
|
||||
super
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class SettingsController < ApplicationController
|
||||
skip_authorization_check
|
||||
|
||||
def index; end
|
||||
end
|
||||
@@ -23,10 +23,10 @@ class SetupController < ApplicationController
|
||||
unless URI.parse(encrypted_config_params[:value].to_s).class.in?([URI::HTTP, URI::HTTPS])
|
||||
@encrypted_config.errors.add(:value, I18n.t('should_be_a_valid_url'))
|
||||
|
||||
return render :index, status: :unprocessable_entity
|
||||
return render :index, status: :unprocessable_content
|
||||
end
|
||||
|
||||
return render :index, status: :unprocessable_entity unless @account.valid?
|
||||
return render :index, status: :unprocessable_content unless @account.valid?
|
||||
|
||||
if @user.save
|
||||
encrypted_configs = [
|
||||
@@ -34,6 +34,7 @@ class SetupController < ApplicationController
|
||||
{ key: EncryptedConfig::ESIGN_CERTS_KEY, value: GenerateCertificate.call.transform_values(&:to_pem) }
|
||||
]
|
||||
@account.encrypted_configs.create!(encrypted_configs)
|
||||
@account.account_configs.create!(key: :fulltext_search, value: true) if SearchEntry.table_exists?
|
||||
|
||||
Docuseal.refresh_default_url_options!
|
||||
|
||||
@@ -41,7 +42,7 @@ class SetupController < ApplicationController
|
||||
|
||||
redirect_to newsletter_path
|
||||
else
|
||||
render :index, status: :unprocessable_entity
|
||||
render :index, status: :unprocessable_content
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@@ -6,20 +6,21 @@ class StartFormController < ApplicationController
|
||||
skip_before_action :authenticate_user!
|
||||
skip_authorization_check
|
||||
|
||||
around_action :with_browser_locale, only: %i[show completed]
|
||||
around_action :with_browser_locale, only: %i[show update completed]
|
||||
before_action :maybe_redirect_com, only: %i[show completed]
|
||||
before_action :load_resubmit_submitter, only: :update
|
||||
before_action :load_template
|
||||
before_action :authorize_start!, only: :update
|
||||
|
||||
def show
|
||||
raise ActionController::RoutingError, I18n.t('not_found') if @template.preferences['require_phone_2fa']
|
||||
if @template.preferences['require_phone_2fa'] || @template.preferences['require_email_2fa']
|
||||
raise ActionController::RoutingError, I18n.t('not_found')
|
||||
end
|
||||
|
||||
if @template.shared_link?
|
||||
@submitter = @template.submissions.new(account_id: @template.account_id)
|
||||
.submitters.new(account_id: @template.account_id,
|
||||
uuid: (filter_undefined_submitters(@template).first ||
|
||||
@template.submitters.first)['uuid'])
|
||||
uuid: Submitters::StartForm.first_submitter_uuid(@template))
|
||||
render :email_verification if params[:email_verification]
|
||||
else
|
||||
Rollbar.warning("Not shared template: #{@template.id}") if defined?(Rollbar)
|
||||
|
||||
@@ -33,37 +34,34 @@ class StartFormController < ApplicationController
|
||||
@submitter = find_or_initialize_submitter(@template, submitter_params)
|
||||
|
||||
if @submitter.completed_at?
|
||||
redirect_to start_form_completed_path(@template.slug, email: submitter_params[:email])
|
||||
redirect_to start_form_completed_path(@template.slug, submitter_params.compact_blank)
|
||||
else
|
||||
if filter_undefined_submitters(@template).size > 1 && @submitter.new_record?
|
||||
if Templates.filter_undefined_submitters(@template.submitters).size > 1 && @submitter.new_record?
|
||||
@error_message = multiple_submitters_error_message
|
||||
|
||||
return render :show
|
||||
return render :show, status: :unprocessable_content
|
||||
end
|
||||
|
||||
if (is_new_record = @submitter.new_record?)
|
||||
assign_submission_attributes(@submitter, @template)
|
||||
Submitters::StartForm.assign_submission_attributes(
|
||||
@submitter, @template, ip: request.remote_ip, user_agent: request.user_agent
|
||||
)
|
||||
|
||||
Submissions::AssignDefinedSubmitters.call(@submitter.submission)
|
||||
else
|
||||
@submitter.assign_attributes(ip: request.remote_ip, ua: request.user_agent)
|
||||
end
|
||||
|
||||
if @submitter.save
|
||||
if is_new_record
|
||||
enqueue_submission_create_webhooks(@submitter)
|
||||
if require_link_2fa?(@template, @submitter)
|
||||
handle_require_2fa(@template, @submitter)
|
||||
elsif @submitter.errors.blank? && @submitter.save
|
||||
Submitters::StartForm.enqueue_new_submitter_jobs(@submitter) if is_new_record
|
||||
|
||||
SearchEntries.enqueue_reindex(@submitter)
|
||||
|
||||
if @submitter.submission.expire_at?
|
||||
ProcessSubmissionExpiredJob.perform_at(@submitter.submission.expire_at,
|
||||
'submission_id' => @submitter.submission_id)
|
||||
end
|
||||
end
|
||||
Submitters::StartForm.assign_start_form_cookie(@submitter, request)
|
||||
|
||||
redirect_to submit_form_path(@submitter.slug)
|
||||
else
|
||||
render :show
|
||||
render :show, status: :unprocessable_content
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -71,24 +69,58 @@ class StartFormController < ApplicationController
|
||||
def completed
|
||||
return redirect_to start_form_path(@template.slug) if !@template.shared_link? || @template.archived_at?
|
||||
|
||||
submitter_params = params.permit(:name, :email, :phone).tap do |attrs|
|
||||
attrs[:email] = Submissions.normalize_email(attrs[:email])
|
||||
end
|
||||
|
||||
required_fields = @template.preferences.fetch('link_form_fields', ['email'])
|
||||
|
||||
required_params = required_fields.index_with { |key| submitter_params[key] }
|
||||
|
||||
raise ActionController::RoutingError, I18n.t('not_found') if required_params.any? { |_, v| v.blank? } ||
|
||||
required_params.except('name').compact_blank.blank?
|
||||
|
||||
@submitter = Submitter.where(submission: @template.submissions)
|
||||
.where.not(completed_at: nil)
|
||||
.find_by!(email: params[:email])
|
||||
.find_by!(required_params.except('name'))
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def load_resubmit_submitter
|
||||
@resubmit_submitter =
|
||||
if params[:resubmit].present? && !params[:resubmit].in?([true, 'true'])
|
||||
Submitter.find_by(slug: params[:resubmit])
|
||||
def find_or_initialize_submitter(template, submitter_params)
|
||||
required_fields = template.preferences.fetch('link_form_fields', ['email'])
|
||||
|
||||
blank_fields = required_fields.select { |key| submitter_params[key].blank? }
|
||||
|
||||
submitter =
|
||||
if blank_fields.present?
|
||||
Submitter.new(submitter_params)
|
||||
else
|
||||
Submitters::StartForm.find_or_initialize_submitter(
|
||||
template, submitter_params, exclude_completed: params[:resubmit].present?,
|
||||
request:, current_user:
|
||||
)
|
||||
end
|
||||
|
||||
blank_fields.each { |key| submitter.errors.add(key.to_sym, :blank) }
|
||||
|
||||
submitter
|
||||
end
|
||||
|
||||
def require_link_2fa?(template, submitter)
|
||||
return true if template.preferences['shared_link_2fa'] == true
|
||||
return false if cookies.encrypted[:start_form_slug] == submitter.slug
|
||||
return false if current_user && submitter.email == current_user.email &&
|
||||
current_user.account_id == submitter.account_id
|
||||
|
||||
!submitter.new_record?
|
||||
end
|
||||
|
||||
def authorize_start!
|
||||
return redirect_to start_form_path(@template.slug) if @template.archived_at?
|
||||
is_archived = @template.archived_at? || @template.account.archived_at?
|
||||
|
||||
return redirect_to start_form_path(@template.slug) if is_archived
|
||||
|
||||
return if @resubmit_submitter
|
||||
return if @template.shared_link? || (current_user && current_ability.can?(:read, @template))
|
||||
|
||||
Rollbar.warning("Not shared template: #{@template.id}") if defined?(Rollbar)
|
||||
@@ -96,73 +128,14 @@ class StartFormController < ApplicationController
|
||||
redirect_to start_form_path(@template.slug)
|
||||
end
|
||||
|
||||
def enqueue_submission_create_webhooks(submitter)
|
||||
WebhookUrls.for_account_id(submitter.account_id, 'submission.created').each do |webhook_url|
|
||||
SendSubmissionCreatedWebhookRequestJob.perform_async('submission_id' => submitter.submission_id,
|
||||
'webhook_url_id' => webhook_url.id)
|
||||
end
|
||||
end
|
||||
|
||||
def find_or_initialize_submitter(template, submitter_params)
|
||||
Submitter
|
||||
.where(submission: template.submissions.where(expire_at: Time.current..)
|
||||
.or(template.submissions.where(expire_at: nil)).where(archived_at: nil))
|
||||
.order(id: :desc)
|
||||
.where(declined_at: nil)
|
||||
.where(external_id: nil)
|
||||
.where(ip: [nil, request.remote_ip])
|
||||
.then { |rel| params[:resubmit].present? || params[:selfsign].present? ? rel.where(completed_at: nil) : rel }
|
||||
.find_or_initialize_by(email: submitter_params[:email], **submitter_params.compact_blank)
|
||||
end
|
||||
|
||||
def assign_submission_attributes(submitter, template)
|
||||
submitter.assign_attributes(
|
||||
uuid: (filter_undefined_submitters(template).first || @template.submitters.first)['uuid'],
|
||||
ip: request.remote_ip,
|
||||
ua: request.user_agent,
|
||||
values: @resubmit_submitter&.preferences&.fetch('default_values', nil) || {},
|
||||
preferences: @resubmit_submitter&.preferences.presence || { 'send_email' => true },
|
||||
metadata: @resubmit_submitter&.metadata.presence || {}
|
||||
)
|
||||
|
||||
if submitter.values.present?
|
||||
@resubmit_submitter.attachments.each do |attachment|
|
||||
submitter.attachments << attachment.dup if submitter.values.value?(attachment.uuid)
|
||||
end
|
||||
end
|
||||
|
||||
submitter.submission ||= Submission.new(template:,
|
||||
account_id: template.account_id,
|
||||
template_submitters: template.submitters,
|
||||
expire_at: Templates.build_default_expire_at(template),
|
||||
submitters: [submitter],
|
||||
source: :link)
|
||||
|
||||
submitter.account_id = submitter.submission.account_id
|
||||
|
||||
submitter
|
||||
end
|
||||
|
||||
def filter_undefined_submitters(template)
|
||||
Templates.filter_undefined_submitters(template.submitters)
|
||||
end
|
||||
|
||||
def submitter_params
|
||||
return current_user.slice(:email) if params[:selfsign]
|
||||
return @resubmit_submitter.slice(:name, :phone, :email) if @resubmit_submitter.present?
|
||||
|
||||
params.require(:submitter).permit(:email, :phone, :name).tap do |attrs|
|
||||
attrs[:email] = Submissions.normalize_email(attrs[:email])
|
||||
end
|
||||
end
|
||||
|
||||
def load_template
|
||||
@template =
|
||||
if @resubmit_submitter
|
||||
@resubmit_submitter.template
|
||||
else
|
||||
Template.find_by!(slug: params[:slug] || params[:start_form_slug])
|
||||
end
|
||||
@template = Template.find_by!(slug: params[:slug] || params[:start_form_slug])
|
||||
end
|
||||
|
||||
def multiple_submitters_error_message
|
||||
@@ -172,4 +145,32 @@ class StartFormController < ApplicationController
|
||||
I18n.t('not_found')
|
||||
end
|
||||
end
|
||||
|
||||
def handle_require_2fa(template, submitter)
|
||||
return render :show, status: :unprocessable_content if submitter.errors.present?
|
||||
|
||||
if Submitters::StartForm.verify_2fa_and_save_submitter(submitter, request, is_new_record: submitter.new_record?)
|
||||
redirect_to submit_form_path(submitter.slug)
|
||||
else
|
||||
if defined?(Rollbar) && template.preferences['shared_link_2fa'] != true
|
||||
Rollbar.info("2FA link requested: #{submitter.id}")
|
||||
end
|
||||
|
||||
Submitters.send_shared_link_email_verification_code(submitter, request:)
|
||||
|
||||
render :email_verification
|
||||
end
|
||||
rescue Submitters::StartForm::NotSaved
|
||||
render :show, status: :unprocessable_content
|
||||
rescue Submitters::BouncedEmail => e
|
||||
@error_message = e.message
|
||||
|
||||
render :error, status: :unprocessable_content
|
||||
rescue Submitters::UnableToSendCode, Submitters::InvalidOtp => e
|
||||
redirect_to start_form_path(template.slug, params: submitter_params.merge(email_verification: true)),
|
||||
alert: e.message
|
||||
rescue RateLimit::LimitApproached
|
||||
redirect_to start_form_path(template.slug, params: submitter_params.merge(email_verification: true)),
|
||||
alert: I18n.t(:too_many_attempts)
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,69 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class StartFormEmail2faSendController < ApplicationController
|
||||
layout 'form'
|
||||
|
||||
around_action :with_browser_locale
|
||||
|
||||
skip_before_action :authenticate_user!
|
||||
skip_authorization_check
|
||||
|
||||
before_action :load_template
|
||||
before_action :authorize_start!
|
||||
|
||||
def create
|
||||
@submitter = @template.submissions.new(account_id: @template.account_id)
|
||||
.submitters.new(**submitter_params, account_id: @template.account_id)
|
||||
|
||||
if @submitter.email.blank?
|
||||
return redirect_to start_form_path(@template.slug), alert: I18n.t(:provide_your_email_to_start)
|
||||
end
|
||||
|
||||
Submitters.send_shared_link_email_verification_code(@submitter, request:)
|
||||
|
||||
redir_params = { notice: I18n.t(:code_has_been_resent) } if params[:resend]
|
||||
|
||||
redirect_to start_form_path(@template.slug, params: submitter_params.merge(email_verification: true)),
|
||||
**redir_params
|
||||
rescue Submitters::BouncedEmail => e
|
||||
@error_message = e.message
|
||||
|
||||
render 'start_form/error', status: :unprocessable_content
|
||||
rescue Submitters::UnableToSendCode => e
|
||||
redirect_to start_form_path(@template.slug, params: submitter_params.merge(email_verification: true)),
|
||||
alert: e.message
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def load_template
|
||||
@template = Template.find_by!(slug: params[:slug])
|
||||
end
|
||||
|
||||
def authorize_start!
|
||||
is_archived = @template.archived_at? || @template.account.archived_at?
|
||||
|
||||
return redirect_to start_form_path(@template.slug) if is_archived
|
||||
|
||||
return if (@template.shared_link? || (current_user && current_ability.can?(:read, @template))) &&
|
||||
(@template.preferences['shared_link_2fa'] == true || submitter_exists?(@template, params))
|
||||
|
||||
Rollbar.warning("Not 2FA shared template: #{@template.id}") if defined?(Rollbar)
|
||||
|
||||
redirect_to start_form_path(@template.slug)
|
||||
end
|
||||
|
||||
def submitter_exists?(template, params)
|
||||
email = Submissions.normalize_email(params.dig(:submitter, :email))
|
||||
|
||||
return false if email.blank?
|
||||
|
||||
Submitters::StartForm.build_submitters_scope(template, exclude_completed: true).exists?(email:)
|
||||
end
|
||||
|
||||
def submitter_params
|
||||
params.require(:submitter).permit(:name, :email, :phone).tap do |attrs|
|
||||
attrs[:email] = Submissions.normalize_email(attrs[:email])
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,103 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class StartFormResubmitController < ApplicationController
|
||||
layout 'form'
|
||||
|
||||
skip_before_action :authenticate_user!
|
||||
skip_authorization_check
|
||||
|
||||
around_action :with_browser_locale
|
||||
before_action :load_resubmit_submitter
|
||||
before_action :load_template
|
||||
before_action :authorize_start!
|
||||
|
||||
def update
|
||||
@submitter = find_or_initialize_submitter(@template, @resubmit_submitter)
|
||||
|
||||
if Templates.filter_undefined_submitters(@template.submitters).size > 1 && @submitter.new_record?
|
||||
@error_message = multiple_submitters_error_message
|
||||
|
||||
return render 'start_form/show', status: :unprocessable_content
|
||||
end
|
||||
|
||||
if (is_new_record = @submitter.new_record?)
|
||||
Submitters::StartForm.assign_submission_attributes(
|
||||
@submitter, @template,
|
||||
ip: request.remote_ip, user_agent: request.user_agent, resubmit_submitter: @resubmit_submitter,
|
||||
source: @resubmit_submitter.submission.source_self? ? :self : :link
|
||||
)
|
||||
|
||||
Submissions::AssignDefinedSubmitters.call(@submitter.submission)
|
||||
else
|
||||
@submitter.assign_attributes(ip: request.remote_ip, ua: request.user_agent)
|
||||
end
|
||||
|
||||
if @template.preferences['shared_link_2fa'] == true
|
||||
handle_require_2fa(@submitter)
|
||||
elsif @submitter.save
|
||||
Submitters::StartForm.enqueue_new_submitter_jobs(@submitter) if is_new_record
|
||||
|
||||
Submitters::StartForm.assign_start_form_cookie(@submitter, request)
|
||||
|
||||
redirect_to submit_form_path(@submitter.slug)
|
||||
else
|
||||
render 'start_form/show', status: :unprocessable_content
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def find_or_initialize_submitter(template, resubmit_submitter)
|
||||
Submitters::StartForm.build_submitters_scope(template, exclude_completed: true)
|
||||
.find_by(slug: cookies.encrypted[:start_form_slug],
|
||||
email: resubmit_submitter.email) || Submitter.new
|
||||
end
|
||||
|
||||
def load_resubmit_submitter
|
||||
@resubmit_submitter = Submitter.find_by(slug: params[:resubmit])
|
||||
|
||||
raise ActiveRecord::RecordNotFound if @resubmit_submitter.blank? ||
|
||||
!Submitters::StartForm.can_resubmit?(@resubmit_submitter)
|
||||
end
|
||||
|
||||
def load_template
|
||||
@template = @resubmit_submitter.template
|
||||
end
|
||||
|
||||
def authorize_start!
|
||||
redirect_to submit_form_path(@resubmit_submitter.slug) if @template.archived_at? || @template.account.archived_at?
|
||||
end
|
||||
|
||||
def multiple_submitters_error_message
|
||||
if current_user&.account_id == @template.account_id
|
||||
helpers.t('this_submission_has_multiple_signers_which_prevents_the_use_of_a_sharing_link_html')
|
||||
else
|
||||
I18n.t('not_found')
|
||||
end
|
||||
end
|
||||
|
||||
def handle_require_2fa(submitter)
|
||||
if Submitters::StartForm.verify_2fa_and_save_submitter(submitter, request,
|
||||
is_new_record: submitter.new_record?)
|
||||
redirect_to submit_form_path(submitter.slug)
|
||||
else
|
||||
Submitters.send_shared_link_email_verification_code(submitter, request:)
|
||||
|
||||
render 'start_form/email_verification'
|
||||
end
|
||||
rescue Submitters::StartForm::NotSaved
|
||||
render 'start_form/show', status: :unprocessable_content
|
||||
rescue Submitters::BouncedEmail => e
|
||||
@error_message = e.message
|
||||
|
||||
render 'start_form/error', status: :unprocessable_content
|
||||
rescue Submitters::UnableToSendCode, Submitters::InvalidOtp => e
|
||||
flash.now[:alert] = e.message
|
||||
|
||||
render 'start_form/email_verification'
|
||||
rescue RateLimit::LimitApproached
|
||||
flash.now[:alert] = I18n.t(:too_many_attempts)
|
||||
|
||||
render 'start_form/email_verification'
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,50 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class StartFormSelfController < ApplicationController
|
||||
load_resource :template, parent: false
|
||||
|
||||
around_action :with_browser_locale
|
||||
before_action :authorize_start!
|
||||
|
||||
def update
|
||||
@submitter = Submitters::StartForm.build_submitters_scope(@template, exclude_completed: true, source: :self)
|
||||
.find_or_initialize_by(email: current_user.email)
|
||||
|
||||
@submitter = Submitter.new(email: current_user.email) if @submitter.persisted? &&
|
||||
outdated_template_fields?(@submitter, @template)
|
||||
|
||||
if (is_new_record = @submitter.new_record?)
|
||||
@submitter.name = current_user.full_name
|
||||
|
||||
Submitters::StartForm.assign_submission_attributes(
|
||||
@submitter, @template, ip: request.remote_ip, user_agent: request.user_agent, source: :self
|
||||
)
|
||||
|
||||
Submissions::AssignDefinedSubmitters.call(@submitter.submission)
|
||||
else
|
||||
@submitter.assign_attributes(ip: request.remote_ip, ua: request.user_agent)
|
||||
end
|
||||
|
||||
@submitter.save!
|
||||
|
||||
Submitters::StartForm.enqueue_new_submitter_jobs(@submitter) if is_new_record
|
||||
|
||||
Submitters::StartForm.assign_start_form_cookie(@submitter, request)
|
||||
|
||||
redirect_to submit_form_path(@submitter.slug)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def outdated_template_fields?(submitter, template)
|
||||
template_fields = submitter.submission.template_fields
|
||||
|
||||
template_fields.present? && template_fields != template.fields
|
||||
end
|
||||
|
||||
def authorize_start!
|
||||
authorize!(:read, @template)
|
||||
|
||||
redirect_to template_path(@template) if @template.archived_at?
|
||||
end
|
||||
end
|
||||
@@ -13,7 +13,7 @@ class StorageSettingsController < ApplicationController
|
||||
|
||||
redirect_to settings_storage_index_path, notice: I18n.t('changes_have_been_saved')
|
||||
else
|
||||
render :index, status: :unprocessable_entity
|
||||
render :index, status: :unprocessable_content
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@@ -10,10 +10,13 @@ class SubmissionEventsController < ApplicationController
|
||||
'api_complete_form' => 'check',
|
||||
'send_reminder_email' => 'mail_forward',
|
||||
'send_2fa_sms' => '2fa',
|
||||
'send_2fa_email' => '2fa',
|
||||
'send_sms' => 'send',
|
||||
'phone_verified' => 'phone_check',
|
||||
'email_verified' => 'email_check',
|
||||
'click_sms' => 'hand_click',
|
||||
'decline_form' => 'x',
|
||||
'delegate_form' => 'user_share',
|
||||
'start_verification' => 'player_play',
|
||||
'complete_verification' => 'check',
|
||||
'invite_party' => 'user_plus'
|
||||
@@ -21,5 +24,7 @@ class SubmissionEventsController < ApplicationController
|
||||
|
||||
load_and_authorize_resource :submission
|
||||
|
||||
def index; end
|
||||
def index
|
||||
render :index, layout: 'plain'
|
||||
end
|
||||
end
|
||||
@@ -7,17 +7,39 @@ class SubmissionsArchivedController < ApplicationController
|
||||
@submissions = @submissions.left_joins(:template)
|
||||
@submissions = @submissions.where.not(archived_at: nil)
|
||||
.or(@submissions.where.not(templates: { archived_at: nil }))
|
||||
.preload(:template_accesses, :created_by_user, template: :author)
|
||||
.preload(:template_accesses, :created_by_user)
|
||||
|
||||
@submissions = Submissions.search(current_user, @submissions, params[:q], search_template: true)
|
||||
@submissions = Submissions::Filter.call(@submissions, current_user, params)
|
||||
|
||||
@submissions = if params[:completed_at_from].present? || params[:completed_at_to].present?
|
||||
@submissions.order(Submitter.arel_table[:completed_at].maximum.desc)
|
||||
else
|
||||
@submissions.order(id: :desc)
|
||||
end
|
||||
@submissions =
|
||||
if params[:status] == 'completed' || params[:completed_at_from].present? || params[:completed_at_to].present?
|
||||
@submissions.order(completed_at: :desc)
|
||||
else
|
||||
@submissions.order(id: :desc)
|
||||
end
|
||||
|
||||
@pagy, @submissions = pagy_auto(@submissions.preload(submitters: :start_form_submission_events))
|
||||
@pagy, @submissions = pagy_auto(@submissions.select_for_list.preload(submitters: :start_form_submission_events))
|
||||
|
||||
template_scope = @submissions.all?(&:template_submitters) ? Template.select_for_list : nil
|
||||
|
||||
ActiveRecord::Associations::Preloader.new(records: @submissions,
|
||||
associations: :template,
|
||||
scope: template_scope).call
|
||||
|
||||
ActiveRecord::Associations::Preloader.new(records: @submissions.filter_map(&:template),
|
||||
associations: :author).call
|
||||
|
||||
render_infinite_scroll(@pagy, @submissions) if turbo_infinite_scroll?
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def render_infinite_scroll(pagy, submissions)
|
||||
render turbo_stream: [
|
||||
turbo_stream.before('infinite_scroll', partial: 'templates/submission', collection: submissions,
|
||||
locals: { with_template: true, archived: true }),
|
||||
turbo_stream.replace('infinite_scroll', partial: 'shared/infinite_scroll', locals: { pagy: })
|
||||
]
|
||||
end
|
||||
end
|
||||
@@ -1,9 +1,7 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class SubmissionsController < ApplicationController
|
||||
before_action :load_template, only: %i[new create]
|
||||
authorize_resource :template, only: %i[new create]
|
||||
|
||||
load_and_authorize_resource :template, only: %i[new create]
|
||||
load_and_authorize_resource :submission, only: %i[show destroy]
|
||||
|
||||
prepend_before_action :maybe_redirect_com, only: %i[show]
|
||||
@@ -12,13 +10,31 @@ class SubmissionsController < ApplicationController
|
||||
authorize!(:create, Submission)
|
||||
end
|
||||
|
||||
FIELD_ICONS = {
|
||||
'text' => 'text_size', 'signature' => 'writing_sign', 'date' => 'calendar_event',
|
||||
'number' => 'square_number_1', 'image' => 'photo', 'initials' => 'letter_case_upper',
|
||||
'file' => 'paperclip', 'select' => 'select', 'checkbox' => 'checkbox', 'radio' => 'circle_dot',
|
||||
'stamp' => 'rubber_stamp', 'cells' => 'columns_3', 'multiple' => 'checks', 'phone' => 'phone_check',
|
||||
'payment' => 'credit_card', 'verification' => 'id', 'kba' => 'user_scan'
|
||||
}.freeze
|
||||
|
||||
STATUS_BADGES = {
|
||||
'awaiting' => 'badge-info', 'sent' => 'badge-info', 'completed' => 'badge-success',
|
||||
'opened' => 'badge-warning', 'declined' => 'badge-error'
|
||||
}.freeze
|
||||
|
||||
STATUS_ICONS = {
|
||||
'awaiting' => 'clock', 'sent' => 'send', 'completed' => 'circle_check',
|
||||
'opened' => 'mail_opened', 'declined' => 'x_circle'
|
||||
}.freeze
|
||||
|
||||
def show
|
||||
@submission = Submissions.preload_with_pages(@submission)
|
||||
|
||||
unless @submission.submitters.all?(&:completed_at?)
|
||||
unless @submission.completed_at?
|
||||
ActiveRecord::Associations::Preloader.new(
|
||||
records: [@submission],
|
||||
associations: [submitters: :start_form_submission_events]
|
||||
associations: [{ submitters: :start_form_submission_events }]
|
||||
).call
|
||||
end
|
||||
|
||||
@@ -27,15 +43,16 @@ class SubmissionsController < ApplicationController
|
||||
|
||||
def new
|
||||
authorize!(:new, Submission)
|
||||
|
||||
render :new, layout: 'plain'
|
||||
end
|
||||
|
||||
def create
|
||||
save_template_message(@template, params) if params[:save_message] == '1'
|
||||
return redirect_to template_path(@template), alert: I18n.t('template_has_been_archived') if @template.archived_at?
|
||||
|
||||
if params[:is_custom_message] != '1'
|
||||
params.delete(:subject)
|
||||
params.delete(:body)
|
||||
end
|
||||
save_template_message(@template, params) if params[:save_message] == '1' && can?(:update, @template)
|
||||
|
||||
[params.delete(:subject), params.delete(:body)] if params[:is_custom_message] != '1'
|
||||
|
||||
submissions =
|
||||
if params[:emails].present?
|
||||
@@ -46,15 +63,10 @@ class SubmissionsController < ApplicationController
|
||||
emails: params[:emails],
|
||||
params: params.merge('send_completed_email' => true))
|
||||
else
|
||||
Submissions.create_from_submitters(template: @template,
|
||||
user: current_user,
|
||||
source: :invite,
|
||||
submitters_order: params[:preserve_order] == '1' ? 'preserved' : 'random',
|
||||
submissions_attrs: submissions_params[:submission].to_h.values,
|
||||
params: params.merge('send_completed_email' => true))
|
||||
create_submissions(@template, submissions_params, params)
|
||||
end
|
||||
|
||||
enqueue_submission_created_webhooks(@template, submissions)
|
||||
WebhookUrls.enqueue_events(submissions, 'submission.created')
|
||||
|
||||
Submissions.send_signature_requests(submissions)
|
||||
|
||||
@@ -62,10 +74,9 @@ class SubmissionsController < ApplicationController
|
||||
|
||||
redirect_to template_path(@template), notice: I18n.t('new_recipients_have_been_added')
|
||||
rescue Submissions::CreateFromSubmitters::BaseError => e
|
||||
render turbo_stream: turbo_stream.replace(:submitters_error,
|
||||
partial: 'submissions/error',
|
||||
locals: { error: e.message }),
|
||||
status: :unprocessable_entity
|
||||
render turbo_stream: turbo_stream.replace(:submitters_error, partial: 'submissions/error',
|
||||
locals: { error: e.message }),
|
||||
status: :unprocessable_content
|
||||
end
|
||||
|
||||
def destroy
|
||||
@@ -77,19 +88,40 @@ class SubmissionsController < ApplicationController
|
||||
else
|
||||
@submission.update!(archived_at: Time.current)
|
||||
|
||||
WebhookUrls.for_account_id(@submission.account_id, 'submission.archived').each do |webhook_url|
|
||||
SendSubmissionArchivedWebhookRequestJob.perform_async('submission_id' => @submission.id,
|
||||
'webhook_url_id' => webhook_url.id)
|
||||
end
|
||||
WebhookUrls.enqueue_events(@submission, 'submission.archived')
|
||||
|
||||
I18n.t('submission_has_been_archived')
|
||||
end
|
||||
|
||||
redirect_back(fallback_location: @submission.template_id ? template_path(@submission.template) : root_path, notice:)
|
||||
if turbo_native_app? && request.format.turbo_stream?
|
||||
flash.now[:notice] = notice
|
||||
|
||||
render turbo_stream: [turbo_stream.remove(@submission), turbo_stream.append_all('body', partial: 'shared/flash')]
|
||||
else
|
||||
redirect_back(fallback_location: @submission.template_id ? template_path(@submission.template) : root_path,
|
||||
notice:)
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def create_submissions(template, submissions_params, params)
|
||||
normalize_message_submitter_uuids!(params)
|
||||
|
||||
submissions_attrs = submissions_params[:submission].to_h.values
|
||||
|
||||
submissions_attrs, _, new_fields =
|
||||
Submissions::NormalizeParamUtils.normalize_submissions_params!(submissions_attrs, template, add_fields: true)
|
||||
|
||||
Submissions.create_from_submitters(template: template,
|
||||
user: current_user,
|
||||
source: :invite,
|
||||
submitters_order: params[:preserve_order] == '1' ? 'preserved' : 'random',
|
||||
submissions_attrs:,
|
||||
new_fields:,
|
||||
params: params.merge('send_completed_email' => true))
|
||||
end
|
||||
|
||||
def save_template_message(template, params)
|
||||
template.preferences['request_email_subject'] = params[:subject] if params[:subject].present?
|
||||
template.preferences['request_email_body'] = params[:body] if params[:body].present?
|
||||
@@ -97,20 +129,26 @@ class SubmissionsController < ApplicationController
|
||||
template.save!
|
||||
end
|
||||
|
||||
def enqueue_submission_created_webhooks(template, submissions)
|
||||
WebhookUrls.for_account_id(template.account_id, 'submission.created').each do |webhook_url|
|
||||
submissions.each do |submission|
|
||||
SendSubmissionCreatedWebhookRequestJob.perform_async('submission_id' => submission.id,
|
||||
'webhook_url_id' => webhook_url.id)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def submissions_params
|
||||
params.permit(submission: { submitters: [%i[uuid email phone name]] })
|
||||
params.permit(submission: { submitters: [:uuid, :email, :phone, :name, { values: {} }] })
|
||||
end
|
||||
|
||||
def load_template
|
||||
@template = Template.accessible_by(current_ability).find(params[:template_id])
|
||||
def normalize_message_submitter_uuids!(params)
|
||||
return if params[:request_email_per_submitter] == '1'
|
||||
|
||||
uuids = params[:email_message_submitter_uuids]
|
||||
|
||||
return if uuids.blank?
|
||||
return if params[:subject].blank? && params[:body].blank?
|
||||
|
||||
params[:submitter_preferences] =
|
||||
Array.wrap(uuids).index_with { { 'subject' => params[:subject], 'body' => params[:body] } }
|
||||
|
||||
params[:request_email_per_submitter] = '1'
|
||||
|
||||
params.delete(:subject)
|
||||
params.delete(:body)
|
||||
|
||||
params
|
||||
end
|
||||
end
|
||||
@@ -8,17 +8,39 @@ class SubmissionsDashboardController < ApplicationController
|
||||
|
||||
@submissions = @submissions.where(archived_at: nil)
|
||||
.where(templates: { archived_at: nil })
|
||||
.preload(:template_accesses, :created_by_user, template: :author)
|
||||
.preload(:template_accesses, :created_by_user)
|
||||
|
||||
@submissions = Submissions.search(current_user, @submissions, params[:q], search_template: true)
|
||||
@submissions = Submissions::Filter.call(@submissions, current_user, params)
|
||||
|
||||
@submissions = if params[:completed_at_from].present? || params[:completed_at_to].present?
|
||||
@submissions.order(Submitter.arel_table[:completed_at].maximum.desc)
|
||||
else
|
||||
@submissions.order(id: :desc)
|
||||
end
|
||||
@submissions =
|
||||
if params[:status] == 'completed' || params[:completed_at_from].present? || params[:completed_at_to].present?
|
||||
@submissions.order(completed_at: :desc)
|
||||
else
|
||||
@submissions.order(id: :desc)
|
||||
end
|
||||
|
||||
@pagy, @submissions = pagy_auto(@submissions.preload(submitters: :start_form_submission_events))
|
||||
@pagy, @submissions = pagy_auto(@submissions.select_for_list.preload(submitters: :start_form_submission_events))
|
||||
|
||||
template_scope = @submissions.all?(&:template_submitters) ? Template.select_for_list : nil
|
||||
|
||||
ActiveRecord::Associations::Preloader.new(records: @submissions,
|
||||
associations: :template,
|
||||
scope: template_scope).call
|
||||
|
||||
ActiveRecord::Associations::Preloader.new(records: @submissions.filter_map(&:template),
|
||||
associations: :author).call
|
||||
|
||||
render_infinite_scroll(@pagy, @submissions) if turbo_infinite_scroll?
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def render_infinite_scroll(pagy, submissions)
|
||||
render turbo_stream: [
|
||||
turbo_stream.before('infinite_scroll', partial: 'templates/submission', collection: submissions,
|
||||
locals: { with_template: true }),
|
||||
turbo_stream.replace('infinite_scroll', partial: 'shared/infinite_scroll', locals: { pagy: })
|
||||
]
|
||||
end
|
||||
end
|
||||
@@ -9,7 +9,7 @@ class SubmissionsDebugController < ApplicationController
|
||||
def index
|
||||
@submitter = Submitter.preload({ attachments_attachments: :blob },
|
||||
submission: { template: { documents_attachments: :blob } })
|
||||
.find_by(slug: params[:submitter_slug])
|
||||
.find_by(slug: params[:submit_form_slug])
|
||||
|
||||
respond_to do |f|
|
||||
f.html do
|
||||
|
||||
@@ -1,40 +1,17 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class SubmissionsDownloadController < ApplicationController
|
||||
skip_before_action :authenticate_user!
|
||||
skip_authorization_check
|
||||
|
||||
TTL = 40.minutes
|
||||
FILES_TTL = 5.minutes
|
||||
load_and_authorize_resource :submission
|
||||
|
||||
def index
|
||||
@submitter = Submitter.find_signed(params[:sig], purpose: :download_completed) if params[:sig].present?
|
||||
|
||||
signature_valid =
|
||||
if @submitter&.slug == params[:submitter_slug]
|
||||
true
|
||||
else
|
||||
@submitter = nil
|
||||
end
|
||||
|
||||
@submitter ||= Submitter.find_by!(slug: params[:submitter_slug])
|
||||
|
||||
Submissions::EnsureResultGenerated.call(@submitter)
|
||||
|
||||
last_submitter = @submitter.submission.submitters.where.not(completed_at: nil).order(:completed_at).last
|
||||
last_submitter = @submission.submitters.where.not(completed_at: nil).order(:completed_at).last
|
||||
|
||||
return head :not_found unless last_submitter
|
||||
|
||||
Submissions::EnsureResultGenerated.call(last_submitter)
|
||||
|
||||
if last_submitter.completed_at < TTL.ago && !signature_valid && !current_user_submitter?(last_submitter)
|
||||
Rollbar.info("TTL: #{last_submitter.id}") if defined?(Rollbar)
|
||||
|
||||
return head :not_found
|
||||
end
|
||||
|
||||
if params[:combined] == 'true'
|
||||
url = build_combined_url(@submitter)
|
||||
url = Submitters.build_combined_url(last_submitter)
|
||||
|
||||
if url
|
||||
render json: [url]
|
||||
@@ -42,43 +19,7 @@ class SubmissionsDownloadController < ApplicationController
|
||||
head :not_found
|
||||
end
|
||||
else
|
||||
render json: build_urls(last_submitter)
|
||||
render json: Submitters.build_document_urls(last_submitter)
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def current_user_submitter?(submitter)
|
||||
current_user && current_user.account.submitters.exists?(id: submitter.id)
|
||||
end
|
||||
|
||||
def build_urls(submitter)
|
||||
filename_format = AccountConfig.find_or_initialize_by(account_id: submitter.account_id,
|
||||
key: AccountConfig::DOCUMENT_FILENAME_FORMAT_KEY)&.value
|
||||
|
||||
Submitters.select_attachments_for_download(submitter).map do |attachment|
|
||||
ActiveStorage::Blob.proxy_url(
|
||||
attachment.blob,
|
||||
expires_at: FILES_TTL.from_now.to_i,
|
||||
filename: Submitters.build_document_filename(submitter, attachment.blob, filename_format)
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
def build_combined_url(submitter)
|
||||
return if submitter.submission.submitters.exists?(completed_at: nil)
|
||||
return if submitter.submission.submitters.order(:completed_at).last != submitter
|
||||
|
||||
attachment = submitter.submission.combined_document_attachment
|
||||
attachment ||= Submissions::GenerateCombinedAttachment.call(submitter)
|
||||
|
||||
filename_format = AccountConfig.find_or_initialize_by(account_id: submitter.account_id,
|
||||
key: AccountConfig::DOCUMENT_FILENAME_FORMAT_KEY)&.value
|
||||
|
||||
ActiveStorage::Blob.proxy_url(
|
||||
attachment.blob,
|
||||
expires_at: FILES_TTL.from_now.to_i,
|
||||
filename: Submitters.build_document_filename(submitter, attachment.blob, filename_format)
|
||||
)
|
||||
end
|
||||
end
|
||||
@@ -5,16 +5,22 @@ class SubmissionsExportController < ApplicationController
|
||||
load_and_authorize_resource :submission, through: :template, parent: false, only: :index
|
||||
|
||||
def index
|
||||
submissions = @submissions.active
|
||||
.preload(submitters: { documents_attachments: :blob,
|
||||
attachments_attachments: :blob })
|
||||
.order(id: :asc)
|
||||
submissions = params[:archived] == 'true' ? @submissions.archived : @submissions.active
|
||||
|
||||
submissions = submissions.preload(submitters: { documents_attachments: :blob,
|
||||
attachments_attachments: :blob })
|
||||
.order(id: :asc)
|
||||
|
||||
submissions = Submissions.search(current_user, submissions, params[:q], search_values: true)
|
||||
submissions = Submissions::Filter.call(submissions, current_user, params)
|
||||
|
||||
expires_at = Accounts.link_expires_at(current_account)
|
||||
|
||||
if params[:format] == 'csv'
|
||||
send_data Submissions::GenerateExportFiles.call(submissions, format: params[:format]),
|
||||
send_data Submissions::GenerateExportFiles.call(submissions, format: params[:format], expires_at:),
|
||||
filename: "#{@template.name}.csv"
|
||||
elsif params[:format] == 'xlsx'
|
||||
send_data Submissions::GenerateExportFiles.call(submissions, format: params[:format]),
|
||||
send_data Submissions::GenerateExportFiles.call(submissions, format: params[:format], expires_at:),
|
||||
filename: "#{@template.name}.xlsx"
|
||||
end
|
||||
end
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
class SubmissionsFiltersController < ApplicationController
|
||||
ALLOWED_NAMES = %w[
|
||||
author
|
||||
folder
|
||||
completed_at
|
||||
status
|
||||
created_at
|
||||
|
||||
@@ -10,25 +10,27 @@ class SubmissionsPreviewController < ApplicationController
|
||||
TTL = 40.minutes
|
||||
|
||||
def show
|
||||
submitter = Submitter.find_signed(params[:sig], purpose: :download_completed) if params[:sig].present?
|
||||
@sig_submitter = Submitter.find_signed(params[:sig], purpose: :download_completed) if params[:sig].present?
|
||||
|
||||
signature_valid =
|
||||
if submitter && submitter.submission.slug == params[:slug]
|
||||
@submission = submitter.submission
|
||||
if @sig_submitter && @sig_submitter.submission.slug == params[:slug]
|
||||
@submission = @sig_submitter.submission
|
||||
|
||||
true
|
||||
else
|
||||
@sig_submitter = nil
|
||||
end
|
||||
|
||||
@submission ||= Submission.find_by!(slug: params[:slug])
|
||||
|
||||
raise ActionController::RoutingError, I18n.t('not_found') if @submission.account.archived_at?
|
||||
|
||||
if !@submission.submitters.all?(&:completed_at?) && !signature_valid &&
|
||||
if !@submission.completed_at? && !signature_valid &&
|
||||
(!current_user || !current_ability.can?(:read, @submission))
|
||||
raise ActionController::RoutingError, I18n.t('not_found')
|
||||
end
|
||||
|
||||
if !submission_valid_ttl?(@submission) && !signature_valid
|
||||
if use_signature?(@submission) && !signature_valid
|
||||
Rollbar.info("TTL: #{@submission.id}") if defined?(Rollbar)
|
||||
|
||||
return redirect_to submissions_preview_completed_path(@submission.slug)
|
||||
@@ -36,11 +38,14 @@ class SubmissionsPreviewController < ApplicationController
|
||||
|
||||
@submission = Submissions.preload_with_pages(@submission)
|
||||
|
||||
render 'submissions/show', layout: 'plain'
|
||||
render 'submissions/show', layout: 'plain', locals: { is_preview: true }
|
||||
end
|
||||
|
||||
def completed
|
||||
@submission = Submission.find_by!(slug: params[:submissions_preview_slug])
|
||||
|
||||
raise ActionController::RoutingError, I18n.t('not_found') if @submission.account.archived_at?
|
||||
|
||||
@template = @submission.template
|
||||
|
||||
render :completed, layout: 'form'
|
||||
@@ -48,9 +53,18 @@ class SubmissionsPreviewController < ApplicationController
|
||||
|
||||
private
|
||||
|
||||
def submission_valid_ttl?(submission)
|
||||
return true if current_user && current_user.account.submissions.exists?(id: submission.id)
|
||||
def use_signature?(submission)
|
||||
return false if current_user && can?(:read, submission)
|
||||
return true if submission.submitters.any? do |e|
|
||||
e.preferences['require_phone_2fa'] || e.preferences['require_email_2fa']
|
||||
end
|
||||
return true if submission.template&.preferences&.dig('require_phone_2fa')
|
||||
return true if submission.template&.preferences&.dig('require_email_2fa')
|
||||
|
||||
!submission_valid_ttl?(submission)
|
||||
end
|
||||
|
||||
def submission_valid_ttl?(submission)
|
||||
last_submitter = submission.submitters.select(&:completed_at?).max_by(&:completed_at)
|
||||
|
||||
last_submitter && last_submitter.completed_at > TTL.ago
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class SubmissionsPreviewDownloadController < ApplicationController
|
||||
skip_before_action :authenticate_user!
|
||||
skip_authorization_check
|
||||
|
||||
TTL = 40.minutes
|
||||
|
||||
def index
|
||||
@submission = Submission.find_by!(slug: params[:submission_slug] || params[:submissions_preview_slug])
|
||||
|
||||
last_submitter = @submission.submitters.where.not(completed_at: nil).order(:completed_at).last
|
||||
|
||||
return head :not_found unless last_submitter
|
||||
|
||||
Submissions::EnsureResultGenerated.call(last_submitter)
|
||||
|
||||
unless current_user_submission?(@submission)
|
||||
if use_2fa?(@submission)
|
||||
Rollbar.info("2FA download error: #{last_submitter.id}") if defined?(Rollbar)
|
||||
|
||||
return head :not_found
|
||||
end
|
||||
|
||||
if last_submitter.completed_at < TTL.ago
|
||||
Rollbar.info("TTL: #{last_submitter.id}") if defined?(Rollbar)
|
||||
|
||||
return head :not_found
|
||||
end
|
||||
end
|
||||
|
||||
if params[:combined] == 'true'
|
||||
respond_with_combined(last_submitter)
|
||||
else
|
||||
render json: Submitters.build_document_urls(last_submitter)
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def respond_with_combined(submitter)
|
||||
url = Submitters.build_combined_url(submitter)
|
||||
|
||||
if url
|
||||
render json: [url]
|
||||
else
|
||||
head :not_found
|
||||
end
|
||||
end
|
||||
|
||||
def current_user_submission?(submission)
|
||||
current_user && current_ability.can?(:read, submission)
|
||||
end
|
||||
|
||||
def use_2fa?(submission)
|
||||
return true if submission.submitters.any? do |e|
|
||||
e.preferences['require_phone_2fa'] || e.preferences['require_email_2fa']
|
||||
end
|
||||
return true if submission.template&.preferences&.dig('require_phone_2fa')
|
||||
return true if submission.template&.preferences&.dig('require_email_2fa')
|
||||
|
||||
false
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,39 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class SubmissionsResendEmailController < ApplicationController
|
||||
load_and_authorize_resource :submission
|
||||
|
||||
before_action do
|
||||
authorize!(:manage, :resend_all)
|
||||
authorize!(:update, @submission)
|
||||
end
|
||||
|
||||
def create
|
||||
submitters = @submission.submitters.reject(&:completed_at?).select { |s| s.email.present? && !s.declined_at? }
|
||||
|
||||
if Docuseal.multitenant?
|
||||
recent_submitter_ids =
|
||||
SubmissionEvent.where(submitter_id: submitters.map(&:id),
|
||||
event_type: 'send_email',
|
||||
created_at: 10.hours.ago..Time.current).pluck(:submitter_id).to_set
|
||||
|
||||
submitters = submitters.reject { |s| recent_submitter_ids.include?(s.id) }
|
||||
end
|
||||
|
||||
submitters.each do |submitter|
|
||||
SendSubmitterInvitationEmailJob.perform_async('submitter_id' => submitter.id)
|
||||
|
||||
submitter.sent_at ||= Time.current
|
||||
submitter.save!
|
||||
end
|
||||
|
||||
notice =
|
||||
if submitters.empty?
|
||||
I18n.t('email_has_been_sent_already')
|
||||
else
|
||||
I18n.t('emails_have_been_sent_to_n_recipients', count: submitters.size)
|
||||
end
|
||||
|
||||
redirect_back(fallback_location: submission_path(@submission), notice:)
|
||||
end
|
||||
end
|
||||
@@ -4,6 +4,8 @@ class SubmissionsUnarchiveController < ApplicationController
|
||||
load_and_authorize_resource :submission
|
||||
|
||||
def create
|
||||
authorize!(:destroy, @submission)
|
||||
|
||||
@submission.update!(archived_at: nil)
|
||||
|
||||
redirect_to submission_path(@submission), notice: I18n.t('submission_has_been_unarchived')
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class SubmitFormClickEmailController < ActionController::API
|
||||
def create
|
||||
@submitter = Submitter.find_by!(slug: params[:submit_form_slug])
|
||||
|
||||
if params[:t].present? && params[:t] == SubmissionEvents.build_tracking_param(@submitter, 'click_email')
|
||||
SubmissionEvents.create_with_tracking_data(@submitter, 'click_email', request)
|
||||
end
|
||||
|
||||
render json: {}
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,73 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class SubmitFormCompletedDownloadController < ApplicationController
|
||||
skip_before_action :authenticate_user!
|
||||
skip_authorization_check
|
||||
|
||||
TTL = 40.minutes
|
||||
FILES_TTL = 5.minutes
|
||||
|
||||
def index
|
||||
@submitter = Submitter.find_signed(params[:sig], purpose: :download_completed) if params[:sig].present?
|
||||
|
||||
signature_valid = @submitter&.slug == submitter_slug
|
||||
|
||||
@submitter = Submitter.find_by!(slug: submitter_slug) unless signature_valid
|
||||
|
||||
unless completed_submitter?(@submitter)
|
||||
Rollbar.error("Not completed: #{@submitter.id}") if defined?(Rollbar)
|
||||
|
||||
return head :not_found
|
||||
end
|
||||
|
||||
Submissions::EnsureResultGenerated.call(@submitter) if @submitter.completed_at?
|
||||
|
||||
last_submitter = @submitter.submission.submitters.completed.order(:completed_at).last
|
||||
|
||||
return head :not_found unless last_submitter
|
||||
|
||||
Submissions::EnsureResultGenerated.call(last_submitter)
|
||||
|
||||
if !signature_valid && !current_user_submitter?(last_submitter)
|
||||
unless Submitters::AuthorizedForForm.call(@submitter, current_user, request, with_order: true)
|
||||
return head :not_found
|
||||
end
|
||||
|
||||
if last_submitter.completed_at < TTL.ago
|
||||
Rollbar.info("TTL: #{last_submitter.id}") if defined?(Rollbar)
|
||||
|
||||
return head :not_found
|
||||
end
|
||||
end
|
||||
|
||||
if params[:combined] == 'true'
|
||||
respond_with_combined(last_submitter)
|
||||
else
|
||||
render json: Submitters.build_document_urls(last_submitter)
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def submitter_slug
|
||||
params[:submit_form_slug] || params[:submitter_slug]
|
||||
end
|
||||
|
||||
def respond_with_combined(submitter)
|
||||
url = Submitters.build_combined_url(submitter)
|
||||
|
||||
if url
|
||||
render json: [url]
|
||||
else
|
||||
head :not_found
|
||||
end
|
||||
end
|
||||
|
||||
def completed_submitter?(submitter)
|
||||
submitter.completed_at? || (submitter.viewer? && submitter.submission.completed_at?)
|
||||
end
|
||||
|
||||
def current_user_submitter?(submitter)
|
||||
current_user && current_ability.can?(:read, submitter)
|
||||
end
|
||||
end
|
||||
@@ -3,19 +3,25 @@
|
||||
class SubmitFormController < ApplicationController
|
||||
layout 'form'
|
||||
|
||||
around_action :with_browser_locale, only: %i[show completed success]
|
||||
around_action :with_browser_locale, only: %i[show completed success delegated]
|
||||
skip_before_action :authenticate_user!
|
||||
skip_authorization_check
|
||||
|
||||
before_action :load_submitter, only: %i[show update completed]
|
||||
before_action :maybe_redirect_delegated, only: %i[show completed]
|
||||
before_action :maybe_render_locked_page, only: :show
|
||||
before_action :maybe_require_link_2fa, only: %i[show]
|
||||
|
||||
CONFIG_KEYS = [].freeze
|
||||
|
||||
def show
|
||||
submission = @submitter.submission
|
||||
|
||||
return redirect_to submit_form_completed_path(@submitter.slug) if @submitter.completed_at?
|
||||
return render :email_2fa unless Submitters::AuthorizedForForm.pass_email_2fa?(@submitter, request)
|
||||
|
||||
if @submitter.completed_at? || submission.completed_at?
|
||||
return redirect_to submit_form_completed_path(@submitter.slug)
|
||||
end
|
||||
|
||||
@form_configs = Submitters::FormConfigs.call(@submitter, CONFIG_KEYS)
|
||||
|
||||
@@ -25,42 +31,51 @@ class SubmitFormController < ApplicationController
|
||||
|
||||
Submissions.preload_with_pages(submission)
|
||||
|
||||
Submitters::MaybeUpdateDefaultValues.call(@submitter, current_user)
|
||||
|
||||
@attachments_index = build_attachments_index(submission)
|
||||
|
||||
if current_user && current_user.email == @submitter.email
|
||||
@current_user_data = current_user.as_json(only: %i[first_name last_name], methods: %i[full_name])
|
||||
|
||||
initials = UserConfigs.load_initials(current_user)
|
||||
|
||||
@prefill_initials = Submitters.build_prefill_attachment_data(@submitter, initials) if initials
|
||||
end
|
||||
|
||||
return unless @form_configs[:prefill_signature]
|
||||
|
||||
if (user_signature = UserConfigs.load_signature(current_user))
|
||||
@signature_attachment = ActiveStorage::Attachment.find_or_create_by!(
|
||||
blob_id: user_signature.blob_id,
|
||||
name: 'attachments',
|
||||
record: @submitter
|
||||
)
|
||||
end
|
||||
signature = UserConfigs.load_signature(current_user) ||
|
||||
Submitters::FindRememberedSignature.call(@submitter, params, cookies)
|
||||
|
||||
@signature_attachment ||=
|
||||
Submitters::MaybeAssignDefaultBrowserSignature.call(@submitter, params, cookies, @attachments_index.values)
|
||||
|
||||
@attachments_index[@signature_attachment.uuid] = @signature_attachment if @signature_attachment
|
||||
@prefill_signature = Submitters.build_prefill_attachment_data(@submitter, signature) if signature
|
||||
end
|
||||
|
||||
def update
|
||||
if @submitter.completed_at?
|
||||
return render json: { error: I18n.t('form_has_been_completed_already') }, status: :unprocessable_entity
|
||||
unless Submitters::AuthorizedForForm.call(@submitter, current_user, request)
|
||||
return render json: { error: I18n.t('verification_required_refresh_the_page_and_pass_2fa') },
|
||||
status: :unprocessable_content
|
||||
end
|
||||
|
||||
if @submitter.template&.archived_at? || @submitter.submission.archived_at?
|
||||
return render json: { error: I18n.t('form_has_been_archived') }, status: :unprocessable_entity
|
||||
if @submitter.completed_at?
|
||||
return render json: { error: I18n.t('form_has_been_completed_already') }, status: :unprocessable_content
|
||||
end
|
||||
|
||||
if @submitter.submission.template&.archived_at? || @submitter.submission.archived_at?
|
||||
return render json: { error: I18n.t('form_has_been_archived') }, status: :unprocessable_content
|
||||
end
|
||||
|
||||
if @submitter.submission.expired?
|
||||
return render json: { error: I18n.t('form_has_been_expired') }, status: :unprocessable_entity
|
||||
return render json: { error: I18n.t('form_has_been_expired') }, status: :unprocessable_content
|
||||
end
|
||||
|
||||
if @submitter.declined_at?
|
||||
return render json: { error: I18n.t('form_has_been_declined') },
|
||||
status: :unprocessable_entity
|
||||
status: :unprocessable_content
|
||||
end
|
||||
|
||||
if @submitter.viewer?
|
||||
Rollbar.warning("Submit viewer: #{@submitter.id}") if defined?(Rollbar)
|
||||
|
||||
return render json: { error: I18n.t('form_is_view_only') }, status: :unprocessable_content
|
||||
end
|
||||
|
||||
Submitters::SubmitValues.call(@submitter, params, request)
|
||||
@@ -69,17 +84,39 @@ class SubmitFormController < ApplicationController
|
||||
rescue Submitters::SubmitValues::RequiredFieldError => e
|
||||
Rollbar.warning("Required field #{@submitter.id}: #{e.message}") if defined?(Rollbar)
|
||||
|
||||
render json: { field_uuid: e.message }, status: :unprocessable_entity
|
||||
render json: { field_uuid: e.message }, status: :unprocessable_content
|
||||
rescue Submitters::SubmitValues::ValidationError => e
|
||||
render json: { error: e.message }, status: :unprocessable_entity
|
||||
Rollbar.warning("Validation error #{@submitter.id}: #{e.message}") if defined?(Rollbar)
|
||||
|
||||
render json: { error: e.message }, status: :unprocessable_content
|
||||
end
|
||||
|
||||
def completed; end
|
||||
def completed
|
||||
raise ActionController::RoutingError, I18n.t('not_found') if @submitter.account.archived_at?
|
||||
|
||||
return if Submitters::AuthorizedForForm.call(@submitter, current_user, request)
|
||||
|
||||
redirect_to submit_form_path(params[:submit_form_slug])
|
||||
end
|
||||
|
||||
def success; end
|
||||
|
||||
def delegated
|
||||
submitter_version = SubmitterVersion.find_by!(slug: params[:slug] || params[:submit_form_slug])
|
||||
|
||||
@submitter = submitter_version.submitter
|
||||
|
||||
maybe_render_locked_page
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def maybe_require_link_2fa
|
||||
return if Submitters::AuthorizedForForm.pass_link_2fa?(@submitter, current_user, request)
|
||||
|
||||
redirect_to start_form_path(@submitter.submission.template.slug)
|
||||
end
|
||||
|
||||
def maybe_render_locked_page
|
||||
return render :archived if @submitter.submission.template&.archived_at? ||
|
||||
@submitter.submission.archived_at? ||
|
||||
@@ -89,8 +126,18 @@ class SubmitFormController < ApplicationController
|
||||
render :declined if @submitter.declined_at?
|
||||
end
|
||||
|
||||
def maybe_redirect_delegated
|
||||
return if @submitter
|
||||
|
||||
submitter_version = SubmitterVersion.find_by!(slug: params[:slug] || params[:submit_form_slug])
|
||||
|
||||
submitter_version.submitter.submission_events.find_by!(event_type: :delegate_form)
|
||||
|
||||
redirect_to submit_form_delegated_path(submitter_version.slug)
|
||||
end
|
||||
|
||||
def load_submitter
|
||||
@submitter = Submitter.find_by!(slug: params[:slug] || params[:submit_form_slug])
|
||||
@submitter = Submitter.find_by(slug: params[:slug] || params[:submit_form_slug])
|
||||
end
|
||||
|
||||
def build_attachments_index(submission)
|
||||
|
||||
@@ -4,32 +4,45 @@ class SubmitFormDeclineController < ApplicationController
|
||||
skip_before_action :authenticate_user!
|
||||
skip_authorization_check
|
||||
|
||||
def create
|
||||
submitter = Submitter.find_by!(slug: params[:submit_form_slug])
|
||||
before_action :load_submitter
|
||||
|
||||
return redirect_to submit_form_path(submitter.slug) if submitter.declined_at? ||
|
||||
submitter.completed_at? ||
|
||||
submitter.submission.archived_at? ||
|
||||
submitter.submission.expired? ||
|
||||
submitter.submission.template&.archived_at?
|
||||
def create
|
||||
return redirect_to submit_form_path(@submitter.slug) if declining_disabled? ||
|
||||
@submitter.declined_at? ||
|
||||
@submitter.completed_at? ||
|
||||
@submitter.submission.archived_at? ||
|
||||
@submitter.submission.expired? ||
|
||||
@submitter.submission.template&.archived_at? ||
|
||||
@submitter.viewer? ||
|
||||
!Submitters::AuthorizedForForm.call(@submitter,
|
||||
current_user,
|
||||
request)
|
||||
|
||||
ApplicationRecord.transaction do
|
||||
submitter.update!(declined_at: Time.current)
|
||||
@submitter.update!(declined_at: Time.current, values: @submitter.preferences['default_values'] || {})
|
||||
|
||||
SubmissionEvents.create_with_tracking_data(submitter, 'decline_form', request, { reason: params[:reason] })
|
||||
SubmissionEvents.create_with_tracking_data(@submitter, 'decline_form', request, { reason: params[:reason] })
|
||||
end
|
||||
|
||||
user = submitter.submission.created_by_user || submitter.template.author
|
||||
user = @submitter.submission.created_by_user || @submitter.template.author
|
||||
|
||||
if user.user_configs.find_by(key: UserConfig::RECEIVE_DECLINED_EMAIL)&.value != false
|
||||
SubmitterMailer.declined_email(submitter, user).deliver_later!
|
||||
if Users.send_emails?(user) &&
|
||||
user.user_configs.find_by(key: UserConfig::RECEIVE_DECLINED_EMAIL)&.value != false
|
||||
SubmitterMailer.declined_email(@submitter, user).deliver_later!
|
||||
end
|
||||
|
||||
WebhookUrls.for_account_id(submitter.account_id, 'form.declined').each do |webhook_url|
|
||||
SendFormDeclinedWebhookRequestJob.perform_async('submitter_id' => submitter.id,
|
||||
'webhook_url_id' => webhook_url.id)
|
||||
end
|
||||
WebhookUrls.enqueue_events(@submitter, 'form.declined')
|
||||
|
||||
redirect_to submit_form_path(submitter.slug)
|
||||
redirect_to submit_form_path(@submitter.slug)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def declining_disabled?
|
||||
@submitter.account.account_configs.find_by(key: AccountConfig::ALLOW_TO_DECLINE_KEY)&.value == false
|
||||
end
|
||||
|
||||
def load_submitter
|
||||
@submitter = Submitter.find_by!(slug: params[:submit_form_slug])
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,50 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class SubmitFormDelegateController < ApplicationController
|
||||
skip_before_action :authenticate_user!
|
||||
skip_authorization_check
|
||||
|
||||
before_action :load_submitter
|
||||
|
||||
def create
|
||||
return redirect_to submit_form_path(@submitter.slug) if @submitter.declined_at? ||
|
||||
@submitter.completed_at? ||
|
||||
@submitter.submission.archived_at? ||
|
||||
@submitter.submission.expired? ||
|
||||
@submitter.submission.template&.archived_at? ||
|
||||
@submitter.viewer? ||
|
||||
!Submitters::AuthorizedForForm.call(@submitter,
|
||||
current_user,
|
||||
request,
|
||||
with_order: true)
|
||||
|
||||
@submitter.account.account_configs.find_by!(key: AccountConfig::ALLOW_TO_DELEGATE_KEY, value: true)
|
||||
|
||||
email = Submissions.normalize_email(params[:email])
|
||||
|
||||
return redirect_to submit_form_path(@submitter.slug) if email.blank?
|
||||
|
||||
old_slug = @submitter.slug
|
||||
|
||||
ApplicationRecord.transaction do
|
||||
@submitter.submitter_versions.create!(slug: old_slug, email: @submitter.email,
|
||||
name: @submitter.name, phone: @submitter.phone)
|
||||
|
||||
SubmissionEvents.create_with_tracking_data(@submitter, 'delegate_form', request,
|
||||
{ old_email: @submitter.email, email: })
|
||||
|
||||
@submitter.update!(email:, phone: nil, name: nil, slug: SecureRandom.base58(14),
|
||||
values: Submitters.fetch_values_for_delegate(@submitter))
|
||||
end
|
||||
|
||||
SendSubmitterInvitationEmailJob.perform_async('submitter_id' => @submitter.id)
|
||||
|
||||
redirect_to submit_form_delegated_path(old_slug)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def load_submitter
|
||||
@submitter = Submitter.find_by!(slug: params[:submit_form_slug])
|
||||
end
|
||||
end
|
||||
@@ -9,15 +9,18 @@ class SubmitFormDownloadController < ApplicationController
|
||||
def index
|
||||
@submitter = Submitter.find_by!(slug: params[:submit_form_slug])
|
||||
|
||||
return redirect_to submitter_download_index_path(@submitter.slug) if @submitter.completed_at?
|
||||
return redirect_to submit_form_documents_path(@submitter.slug) if @submitter.completed_at?
|
||||
|
||||
return head :unprocessable_entity if @submitter.declined_at? ||
|
||||
@submitter.submission.archived_at? ||
|
||||
@submitter.submission.expired? ||
|
||||
@submitter.submission.template&.archived_at? ||
|
||||
AccountConfig.exists?(account_id: @submitter.account_id,
|
||||
key: AccountConfig::ALLOW_TO_PARTIAL_DOWNLOAD_KEY,
|
||||
value: false)
|
||||
return head :unprocessable_content if @submitter.declined_at? ||
|
||||
@submitter.submission.archived_at? ||
|
||||
@submitter.submission.expired? ||
|
||||
@submitter.submission.completed_at? ||
|
||||
@submitter.submission.template&.archived_at? ||
|
||||
AccountConfig.exists?(account_id: @submitter.account_id,
|
||||
key: AccountConfig::ALLOW_TO_PARTIAL_DOWNLOAD_KEY,
|
||||
value: false) ||
|
||||
!Submitters::AuthorizedForForm.call(@submitter, current_user, request,
|
||||
with_order: true)
|
||||
|
||||
last_completed_submitter = @submitter.submission.submitters
|
||||
.where.not(id: @submitter.id)
|
||||
@@ -32,7 +35,7 @@ class SubmitFormDownloadController < ApplicationController
|
||||
end
|
||||
|
||||
urls = attachments.map do |attachment|
|
||||
ActiveStorage::Blob.proxy_url(attachment.blob, expires_at: FILES_TTL.from_now.to_i)
|
||||
ActiveStorage::Blob.proxy_path(attachment.blob, expires_at: FILES_TTL.from_now.to_i)
|
||||
end
|
||||
|
||||
render json: urls
|
||||
|
||||
@@ -12,7 +12,10 @@ class SubmitFormDrawSignatureController < ApplicationController
|
||||
|
||||
return redirect_to submit_form_completed_path(@submitter.slug) if @submitter.completed_at?
|
||||
|
||||
if @submitter.submission.template.archived_at? || @submitter.submission.archived_at?
|
||||
return redirect_to submit_form_path(@submitter.slug) if @submitter.viewer?
|
||||
|
||||
if @submitter.submission.template&.archived_at? || @submitter.submission.archived_at? ||
|
||||
!Submitters::AuthorizedForForm.call(@submitter, current_user, request, with_order: true)
|
||||
return redirect_to submit_form_path(@submitter.slug)
|
||||
end
|
||||
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class SubmitFormEmail2fasController < ApplicationController
|
||||
around_action :with_browser_locale
|
||||
|
||||
skip_before_action :authenticate_user!
|
||||
skip_authorization_check
|
||||
|
||||
before_action :load_submitter
|
||||
|
||||
def create
|
||||
RateLimit.call("verify-2fa-code-#{@submitter.id}", limit: 2, ttl: 45.seconds, enabled: true)
|
||||
RateLimit.call("verify-2fa-code-1h-#{@submitter.id}", limit: 10, ttl: 1.hour, enabled: true)
|
||||
RateLimit.call("verify-2fa-code-1d-#{@submitter.id}", limit: 20, ttl: 1.day, enabled: true)
|
||||
|
||||
value = [@submitter.email.downcase.strip, @submitter.slug].join(':')
|
||||
|
||||
if EmailVerificationCodes.verify(params[:one_time_code].to_s.gsub(/\D/, ''), value)
|
||||
SubmissionEvents.create_with_tracking_data(@submitter, 'email_verified', request, { email: @submitter.email })
|
||||
|
||||
cookies.encrypted[:email_2fa_slug] =
|
||||
{ value: @submitter.slug, expires: Submitters::StartForm::COOKIES_TTL.from_now,
|
||||
**Submitters::StartForm::COOKIES_DEFAULTS }
|
||||
|
||||
redirect_to submit_form_path(@submitter.slug)
|
||||
else
|
||||
redirect_to submit_form_path(@submitter.slug, status: :error), alert: I18n.t(:invalid_code)
|
||||
end
|
||||
rescue RateLimit::LimitApproached
|
||||
redirect_to submit_form_path(@submitter.slug, status: :error), alert: I18n.t(:too_many_attempts)
|
||||
end
|
||||
|
||||
def update
|
||||
if @submitter.submission_events.where(event_type: 'send_2fa_email').exists?(created_at: 15.seconds.ago..)
|
||||
return redirect_to submit_form_path(@submitter.slug, status: :error), alert: I18n.t(:rate_limit_exceeded)
|
||||
end
|
||||
|
||||
RateLimit.call("send-email-code-#{@submitter.id}", limit: 2, ttl: 45.seconds, enabled: true)
|
||||
|
||||
if Docuseal.multitenant? && Submitters.email_bounced_recently?(@submitter.email)
|
||||
Rollbar.warning("Bounced OTP email for submitter: #{@submitter.id}") if defined?(Rollbar)
|
||||
|
||||
return redirect_to submit_form_path(@submitter.slug, status: :error), alert: I18n.t(:verification_email_bounced)
|
||||
end
|
||||
|
||||
SendSubmitterVerificationEmailJob.perform_async('submitter_id' => @submitter.id, 'locale' => I18n.locale.to_s)
|
||||
|
||||
redir_params = params[:resend] ? { alert: I18n.t(:code_has_been_resent) } : {}
|
||||
|
||||
redirect_to submit_form_path(@submitter.slug, status: :sent), **redir_params
|
||||
rescue RateLimit::LimitApproached
|
||||
redirect_to submit_form_path(@submitter.slug, status: :error), alert: I18n.t(:too_many_attempts)
|
||||
end
|
||||
|
||||
def load_submitter
|
||||
@submitter = Submitter.find_by!(slug: params[:submitter_slug])
|
||||
end
|
||||
end
|
||||
@@ -5,12 +5,12 @@ class SubmitFormInviteController < ApplicationController
|
||||
skip_authorization_check
|
||||
|
||||
def create
|
||||
submitter = Submitter.find_by!(slug: params[:submit_form_slug])
|
||||
@submitter = Submitter.find_by!(slug: params[:submit_form_slug])
|
||||
|
||||
return head :unprocessable_entity unless can_invite?(submitter)
|
||||
return head :unprocessable_content unless can_invite?(@submitter)
|
||||
|
||||
invite_submitters = filter_invite_submitters(submitter, 'invite_by_uuid')
|
||||
optional_invite_submitters = filter_invite_submitters(submitter, 'optional_invite_by_uuid')
|
||||
invite_submitters = filter_invite_submitters(@submitter, 'invite_by_uuid')
|
||||
optional_invite_submitters = filter_invite_submitters(@submitter, 'optional_invite_by_uuid')
|
||||
|
||||
ApplicationRecord.transaction do
|
||||
(invite_submitters + optional_invite_submitters).each do |item|
|
||||
@@ -19,22 +19,24 @@ class SubmitFormInviteController < ApplicationController
|
||||
next unless attrs
|
||||
next if attrs[:email].blank?
|
||||
|
||||
submitter.submission.submitters.create!(**attrs, account_id: submitter.account_id)
|
||||
email = Submissions.normalize_email(attrs[:email])
|
||||
|
||||
SubmissionEvents.create_with_tracking_data(submitter, 'invite_party', request, { uuid: submitter.uuid })
|
||||
@submitter.submission.submitters.create!(uuid: attrs[:uuid], email:, account_id: @submitter.account_id)
|
||||
|
||||
SubmissionEvents.create_with_tracking_data(@submitter, 'invite_party', request, { uuid: attrs[:uuid] })
|
||||
end
|
||||
|
||||
submitter.submission.update!(submitters_order: :preserved)
|
||||
@submitter.submission.update!(submitters_order: :preserved)
|
||||
end
|
||||
|
||||
submitter.submission.submitters.reload
|
||||
@submitter.submission.submitters.reload
|
||||
|
||||
if invite_submitters.all? { |s| submitter.submission.submitters.any? { |e| e.uuid == s['uuid'] } }
|
||||
Submitters::SubmitValues.call(submitter, ActionController::Parameters.new(completed: 'true'), request)
|
||||
if invite_submitters.all? { |s| @submitter.submission.submitters.any? { |e| e.uuid == s['uuid'] } }
|
||||
Submitters::SubmitValues.call(@submitter, ActionController::Parameters.new(completed: 'true'), request)
|
||||
|
||||
head :ok
|
||||
else
|
||||
head :unprocessable_entity
|
||||
head :unprocessable_content
|
||||
end
|
||||
end
|
||||
|
||||
@@ -45,7 +47,9 @@ class SubmitFormInviteController < ApplicationController
|
||||
!submitter.completed_at? &&
|
||||
!submitter.submission.archived_at? &&
|
||||
!submitter.submission.expired? &&
|
||||
!submitter.submission.template.archived_at?
|
||||
!submitter.submission.template&.archived_at? &&
|
||||
!submitter.viewer? &&
|
||||
Submitters::AuthorizedForForm.call(submitter, current_user, request)
|
||||
end
|
||||
|
||||
def filter_invite_submitters(submitter, key = 'invite_by_uuid')
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class SubmitFormMetadataController < ApplicationController
|
||||
skip_before_action :authenticate_user!
|
||||
skip_authorization_check
|
||||
|
||||
def index
|
||||
@submitter = Submitter.find_by!(slug: params[:submit_form_slug])
|
||||
|
||||
return head :not_found unless authorized_submitter?(@submitter)
|
||||
|
||||
submission = @submitter.submission
|
||||
values = submission.submitters.reduce({}) { |acc, sub| acc.merge(sub.values) }
|
||||
schema = Submissions.filtered_conditions_schema(submission, values:, include_submitter_uuid: @submitter.uuid)
|
||||
|
||||
documents = schema.filter_map do |item|
|
||||
submission.schema_documents.find { |a| a.uuid == item['attachment_uuid'] }
|
||||
end
|
||||
|
||||
ActiveRecord::Associations::Preloader.new(records: documents, associations: %i[blob record]).call
|
||||
|
||||
text_runs = documents.to_h do |document|
|
||||
[
|
||||
document.uuid,
|
||||
DocumentMetadatas.find_or_create_for_document(document, account_id: document.record.account_id).text_runs
|
||||
]
|
||||
end
|
||||
|
||||
render json: { text_runs: }
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def authorized_submitter?(submitter)
|
||||
!submitter.declined_at? &&
|
||||
!submitter.completed_at? &&
|
||||
!submitter.submission.archived_at? &&
|
||||
!submitter.submission.completed_at? &&
|
||||
!submitter.submission.expired? &&
|
||||
!submitter.submission.template&.archived_at? &&
|
||||
!submitter.account.archived_at? &&
|
||||
Submitters::AuthorizedForForm.call(submitter, current_user, request, with_order: true)
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,33 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class SubmitFormPrefillAttachmentsController < ActionController::API
|
||||
include ActiveStorage::SetCurrent
|
||||
|
||||
def create
|
||||
submitter = Submitter.find_by!(slug: params[:submit_form_slug])
|
||||
|
||||
unless can_upload?(submitter)
|
||||
return render json: { error: I18n.t('form_has_been_archived') }, status: :unprocessable_content
|
||||
end
|
||||
|
||||
uuid, slug = ApplicationRecord.signed_id_verifier.verify(params[:prefill_token], purpose: :prefill_attachment)
|
||||
|
||||
raise ActiveRecord::RecordNotFound if slug != submitter.slug
|
||||
|
||||
source_attachment = ActiveStorage::Attachment.find_by!(uuid:)
|
||||
|
||||
attachment = submitter.attachments_attachments.find_or_create_by!(blob_id: source_attachment.blob_id)
|
||||
|
||||
render json: attachment.as_json(only: %i[uuid created_at], methods: %i[url filename content_type])
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def can_upload?(submitter)
|
||||
!submitter.declined_at? &&
|
||||
!submitter.completed_at? &&
|
||||
!submitter.submission.archived_at? &&
|
||||
!submitter.submission.expired? &&
|
||||
!submitter.submission.template&.archived_at?
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,86 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class SubmitFormUploadController < ActionController::API
|
||||
include ActionController::Cookies
|
||||
include ActiveStorage::SetCurrent
|
||||
|
||||
before_action :load_submitter
|
||||
before_action :authorize_upload!
|
||||
|
||||
COOKIE_STORE_LIMIT = 10
|
||||
|
||||
def create
|
||||
file = params[:file]
|
||||
|
||||
if params[:type].in?(%w[initials signature])
|
||||
image = ImageUtils.load_vips(file.read, content_type: file.content_type)
|
||||
|
||||
if ImageUtils.blank?(image)
|
||||
Rollbar.error("Empty signature: #{@submitter.id}") if defined?(Rollbar)
|
||||
|
||||
return render json: { error: "#{params[:type]} is empty" }, status: :unprocessable_content
|
||||
end
|
||||
|
||||
if ImageUtils.error?(image)
|
||||
Rollbar.error("Error signature: #{@submitter.id}") if defined?(Rollbar)
|
||||
|
||||
return render json: { error: "#{params[:type]} error, try to sign on another device" },
|
||||
status: :unprocessable_content
|
||||
end
|
||||
|
||||
metadata = { analyzed: true, identified: true, width: image.width, height: image.height }
|
||||
end
|
||||
|
||||
attachment = Submitters.create_attachment!(@submitter, file, metadata:)
|
||||
|
||||
if params[:remember_signature] == 'true' && @submitter.email.present?
|
||||
cookies.encrypted[:signature_uuids] = build_new_cookie_signatures_json(@submitter, attachment)
|
||||
end
|
||||
|
||||
render json: attachment.as_json(only: %i[uuid created_at], methods: %i[url filename content_type])
|
||||
rescue Submitters::MaliciousFileExtension => e
|
||||
Rollbar.error(e) if defined?(Rollbar)
|
||||
|
||||
render json: { error: e.message }, status: :unprocessable_content
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def load_submitter
|
||||
@submitter = Submitter.find_by!(slug: params[:submit_form_slug])
|
||||
end
|
||||
|
||||
def authorize_upload!
|
||||
unless can_upload?(@submitter)
|
||||
return render json: { error: I18n.t('form_has_been_archived') }, status: :unprocessable_content
|
||||
end
|
||||
|
||||
return if Submitters::AuthorizedForForm.call(@submitter, current_user, request)
|
||||
|
||||
render json: { error: I18n.t('verification_required_refresh_the_page_and_pass_2fa') },
|
||||
status: :unprocessable_content
|
||||
end
|
||||
|
||||
def can_upload?(submitter)
|
||||
!submitter.declined_at? &&
|
||||
!submitter.completed_at? &&
|
||||
!submitter.submission.archived_at? &&
|
||||
!submitter.submission.expired? &&
|
||||
!submitter.submission.template&.archived_at?
|
||||
end
|
||||
|
||||
def build_new_cookie_signatures_json(submitter, attachment)
|
||||
values =
|
||||
begin
|
||||
JSON.parse(cookies.encrypted[:signature_uuids].presence || '{}')
|
||||
rescue JSON::ParserError
|
||||
{}
|
||||
end
|
||||
|
||||
values[submitter.email] = attachment.uuid
|
||||
|
||||
values = values.to_a.last(COOKIE_STORE_LIMIT).to_h if values.size > COOKIE_STORE_LIMIT
|
||||
|
||||
values.to_json
|
||||
end
|
||||
end
|
||||
@@ -5,19 +5,21 @@ class SubmitFormValuesController < ApplicationController
|
||||
skip_authorization_check
|
||||
|
||||
def index
|
||||
submitter = Submitter.find_by!(slug: params[:submit_form_slug])
|
||||
@submitter = Submitter.find_by!(slug: params[:submit_form_slug])
|
||||
|
||||
return render json: {} if submitter.completed_at? || submitter.declined_at?
|
||||
return render json: {} if submitter.submission.template.archived_at? ||
|
||||
submitter.submission.archived_at? ||
|
||||
submitter.submission.expired?
|
||||
return render json: {} if @submitter.completed_at? ||
|
||||
@submitter.declined_at? ||
|
||||
@submitter.submission.template&.archived_at? ||
|
||||
@submitter.submission.archived_at? ||
|
||||
@submitter.submission.expired? ||
|
||||
!Submitters::AuthorizedForForm.call(@submitter, current_user, request)
|
||||
|
||||
value = submitter.values[params['field_uuid']]
|
||||
attachment = submitter.attachments.where(created_at: params[:after]..).find_by(uuid: value) if value.present?
|
||||
value = @submitter.values[params['field_uuid']]
|
||||
attachment = @submitter.attachments.where(created_at: params[:after]..).find_by(uuid: value) if value.present?
|
||||
|
||||
render json: {
|
||||
value:,
|
||||
attachment: attachment&.as_json(only: %i[uuid created_at], methods: %i[url filename content_type])
|
||||
}, head: :ok
|
||||
}
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,18 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class SubmitFormViewController < ActionController::API
|
||||
def create
|
||||
@submitter = Submitter.find_by!(slug: params[:submit_form_slug])
|
||||
|
||||
return head :forbidden if Submitter.signed_id_verifier.verified(params[:v], purpose: :view_form) != @submitter.slug
|
||||
|
||||
@submitter.opened_at = Time.current
|
||||
@submitter.save
|
||||
|
||||
SubmissionEvents.create_with_tracking_data(@submitter, 'view_form', request)
|
||||
|
||||
WebhookUrls.enqueue_events(@submitter, 'form.viewed')
|
||||
|
||||
render json: {}
|
||||
end
|
||||
end
|
||||
@@ -7,27 +7,36 @@ class SubmittersAutocompleteController < ApplicationController
|
||||
LIMIT = 100
|
||||
|
||||
def index
|
||||
submitters = search_submitters(@submitters)
|
||||
field = SELECT_COLUMNS.find { |c| c == params[:field] }
|
||||
|
||||
submitters = search_submitters(@submitters, field)
|
||||
|
||||
arel_columns = SELECT_COLUMNS.map { |col| Submitter.arel_table[col] }
|
||||
values = submitters.limit(LIMIT).group(arel_columns).pluck(arel_columns)
|
||||
|
||||
values =
|
||||
if field
|
||||
max_ids = submitters.group(field).limit(LIMIT).select(Submitter.arel_table[:id].maximum)
|
||||
|
||||
submitters.where(id: max_ids).order(id: :desc).pluck(arel_columns)
|
||||
else
|
||||
submitters.limit(LIMIT).group(arel_columns).pluck(arel_columns)
|
||||
end
|
||||
|
||||
attrs = values.map { |row| SELECT_COLUMNS.zip(row).to_h }
|
||||
attrs = attrs.uniq { |e| e[params[:field]] } if params[:field].present?
|
||||
|
||||
render json: attrs
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def search_submitters(submitters)
|
||||
if SELECT_COLUMNS.include?(params[:field])
|
||||
if Docuseal.fulltext_search?(current_user)
|
||||
Submitters.fulltext_search_field(current_user, submitters, params[:q], params[:field])
|
||||
def search_submitters(submitters, field)
|
||||
if field
|
||||
if Docuseal.fulltext_search?
|
||||
Submitters.fulltext_search_field(current_user, submitters, params[:q], field)
|
||||
else
|
||||
column = Submitter.arel_table[params[:field].to_sym]
|
||||
column = Submitter.arel_table[field.to_sym]
|
||||
|
||||
term = "#{params[:q].downcase}%"
|
||||
term = "#{ActiveRecord::Base.sanitize_sql_like(params[:q].downcase)}%"
|
||||
|
||||
submitters.where(column.matches(term))
|
||||
end
|
||||
|
||||
@@ -8,12 +8,14 @@ class SubmittersController < ApplicationController
|
||||
if @submitter.preferences['email_message_uuid'].present?
|
||||
@submitter.account.email_messages.find_by(uuid: @submitter.preferences['email_message_uuid'])
|
||||
end
|
||||
|
||||
render :edit, layout: 'plain'
|
||||
end
|
||||
|
||||
def update
|
||||
submission = @submitter.submission
|
||||
|
||||
if @submitter.submission_events.exists?(event_type: 'start_form') || submission.archived_at? || submission.expired?
|
||||
unless submitter_editable?(submission)
|
||||
return redirect_back fallback_location: submission_path(submission), alert: I18n.t('submitter_cannot_be_updated')
|
||||
end
|
||||
|
||||
@@ -48,11 +50,17 @@ class SubmittersController < ApplicationController
|
||||
|
||||
private
|
||||
|
||||
def submitter_editable?(submission)
|
||||
!@submitter.submission_events.exists?(event_type: 'start_form') &&
|
||||
!@submitter.completed_at? && !@submitter.declined_at? && !submission.completed_at? &&
|
||||
!submission.archived_at? && !submission.expired? && !submission.template&.archived_at?
|
||||
end
|
||||
|
||||
def maybe_resend_email_sms(submitter, params)
|
||||
if params[:send_email] == '1' && submitter.email.present?
|
||||
is_sent_recently = Docuseal.multitenant? &&
|
||||
EmailEvent.exists?(email: submitter.email,
|
||||
tag: 'submitter_invitation',
|
||||
tag: %w[submitter_invitation submitter_view_invitation],
|
||||
emailable: submitter,
|
||||
event_type: 'send',
|
||||
created_at: 4.hours.ago..Time.current)
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class SubmittersDownloadController < ApplicationController
|
||||
load_and_authorize_resource :submitter
|
||||
|
||||
def index
|
||||
Submissions::EnsureResultGenerated.call(@submitter)
|
||||
|
||||
render json: Submitters.build_document_urls(@submitter)
|
||||
end
|
||||
end
|
||||
@@ -5,6 +5,8 @@ class SubmittersResubmitController < ApplicationController
|
||||
|
||||
def update
|
||||
return redirect_to submit_form_path(slug: @submitter.slug) if @submitter.email != current_user.email
|
||||
return redirect_to submit_form_path(slug: @submitter.slug) if @submitter.completed_at.blank? ||
|
||||
@submitter.completed_at < 1.month.ago
|
||||
|
||||
submission = @submitter.account.submissions.new(created_by_user: current_user,
|
||||
submitters_order: :preserved,
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class SubmittersSendEmailController < ApplicationController
|
||||
load_and_authorize_resource :submitter, id_param: :submitter_slug, find_by: :slug
|
||||
load_and_authorize_resource :submitter
|
||||
|
||||
def create
|
||||
authorize!(:update, @submitter)
|
||||
|
||||
if Docuseal.multitenant? && SubmissionEvent.exists?(submitter: @submitter,
|
||||
event_type: 'send_email',
|
||||
created_at: 10.hours.ago..Time.current)
|
||||
|
||||
@@ -3,14 +3,22 @@
|
||||
class TemplateDocumentsController < ApplicationController
|
||||
load_and_authorize_resource :template
|
||||
|
||||
FILES_TTL = 5.minutes
|
||||
|
||||
def index
|
||||
render json: @template.schema_documents.map { |d| ActiveStorage::Blob.proxy_path(d.blob, expires_at: FILES_TTL.from_now.to_i) }
|
||||
end
|
||||
|
||||
def create
|
||||
authorize!(:update, @template)
|
||||
|
||||
if params[:blobs].blank? && params[:files].blank?
|
||||
return render json: { error: I18n.t('file_is_missing') }, status: :unprocessable_entity
|
||||
return render json: { error: I18n.t('file_is_missing') }, status: :unprocessable_content
|
||||
end
|
||||
|
||||
old_fields_hash = @template.fields.hash
|
||||
|
||||
documents = Templates::CreateAttachments.call(@template, params, extract_fields: true)
|
||||
documents, = Templates::CreateAttachments.call(@template, params, extract_fields: true)
|
||||
|
||||
schema = documents.map do |doc|
|
||||
{ attachment_uuid: doc.uuid, name: doc.filename.base }
|
||||
@@ -21,13 +29,13 @@ class TemplateDocumentsController < ApplicationController
|
||||
fields: old_fields_hash == @template.fields.hash ? nil : @template.fields,
|
||||
submitters: old_fields_hash == @template.fields.hash ? nil : @template.submitters,
|
||||
documents: documents.as_json(
|
||||
methods: %i[metadata signed_uuid],
|
||||
methods: %i[metadata signed_key],
|
||||
include: {
|
||||
preview_images: { methods: %i[url metadata filename] }
|
||||
}
|
||||
)
|
||||
}
|
||||
rescue Templates::CreateAttachments::PdfEncrypted
|
||||
render json: { error: 'PDF encrypted', status: 'pdf_encrypted' }, status: :unprocessable_entity
|
||||
render json: { error: 'PDF encrypted', status: 'pdf_encrypted' }, status: :unprocessable_content
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,39 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class TemplateDocumentsCropController < ApplicationController
|
||||
load_and_authorize_resource :template
|
||||
before_action :load_attachment
|
||||
|
||||
rescue_from Leptonica::LeptonicaError do
|
||||
render json: { error: I18n.t(:unable_to_save) }, status: :unprocessable_content
|
||||
end
|
||||
|
||||
def index
|
||||
render json: { corners: Leptonica.detect_document_corners(@attachment.download) }
|
||||
end
|
||||
|
||||
def create
|
||||
authorize!(:update, @template)
|
||||
|
||||
document = Templates::CreateDocumentCrop.call(@template, @attachment, crop_params)
|
||||
|
||||
render json: {
|
||||
document: document.as_json(
|
||||
methods: %i[metadata signed_key],
|
||||
include: {
|
||||
preview_images: { methods: %i[url metadata filename] }
|
||||
}
|
||||
)
|
||||
}
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def load_attachment
|
||||
@attachment = @template.documents_attachments.find_by!(uuid: params[:attachment_uuid])
|
||||
end
|
||||
|
||||
def crop_params
|
||||
params.permit(:scan, :rotate, :flip_h, :flip_v, corners: [%i[x y]])
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,32 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class TemplateDocumentsModifyController < ApplicationController
|
||||
load_and_authorize_resource :template
|
||||
|
||||
def create
|
||||
authorize!(:update, @template)
|
||||
|
||||
documents_layout =
|
||||
params.require(:documents).map do |item|
|
||||
item.permit(:attachment_uuid,
|
||||
pages: [:attachment_uuid, :page, :rotate,
|
||||
{ redact: [%i[x y w h color]], replaced_page: %i[attachment_uuid page] }]).to_h
|
||||
end
|
||||
|
||||
Templates::ModifyDocuments.call(@template, documents_layout)
|
||||
|
||||
render json: {
|
||||
schema: @template.schema,
|
||||
fields: @template.fields,
|
||||
submitters: @template.submitters,
|
||||
documents: @template.schema_documents.reload.preload(:blob, preview_images_attachments: :blob).as_json(
|
||||
methods: %i[metadata signed_key],
|
||||
include: {
|
||||
preview_images: { methods: %i[url metadata filename] }
|
||||
}
|
||||
)
|
||||
}
|
||||
rescue Templates::ModifyDocuments::InvalidLayout
|
||||
render json: { error: I18n.t(:unable_to_save) }, status: :unprocessable_content
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,11 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class TemplateDocumentsPageObjectsController < ApplicationController
|
||||
load_and_authorize_resource :template
|
||||
|
||||
def index
|
||||
attachment = @template.documents_attachments.find_by!(uuid: params[:attachment_uuid])
|
||||
|
||||
render json: Templates::ModifyDocuments.page_objects(attachment, params[:page].to_i)
|
||||
end
|
||||
end
|
||||
@@ -3,12 +3,31 @@
|
||||
class TemplateFoldersAutocompleteController < ApplicationController
|
||||
load_and_authorize_resource :template_folder, parent: false
|
||||
|
||||
LIMIT = 100
|
||||
LIMIT = 30
|
||||
|
||||
def index
|
||||
template_folders = @template_folders.joins(:templates).where(templates: { archived_at: nil }).distinct
|
||||
template_folders = TemplateFolders.search(template_folders, params[:q]).limit(LIMIT)
|
||||
parent_name, name =
|
||||
if params[:parent_name].present?
|
||||
[params[:parent_name], params[:q]]
|
||||
else
|
||||
params[:q].to_s.split(' /', 2).map(&:squish)
|
||||
end
|
||||
|
||||
render json: template_folders.as_json(only: %i[name archived_at])
|
||||
if name
|
||||
parent_folder = @template_folders.find_by(name: parent_name, parent_folder_id: nil)
|
||||
else
|
||||
name = parent_name
|
||||
end
|
||||
|
||||
template_folders = TemplateFolders.filter_active_folders(@template_folders.where(parent_folder:),
|
||||
Template.accessible_by(current_ability))
|
||||
|
||||
name = name.to_s.downcase
|
||||
|
||||
template_folders = TemplateFolders.search(template_folders, name).order(id: :desc).limit(LIMIT)
|
||||
|
||||
render json: template_folders.preload(:parent_folder)
|
||||
.sort_by { |e| e.name.downcase.index(name) || Float::MAX }
|
||||
.as_json(only: %i[name archived_at], methods: %i[full_name])
|
||||
end
|
||||
end
|
||||
@@ -3,13 +3,42 @@
|
||||
class TemplateFoldersController < ApplicationController
|
||||
load_and_authorize_resource :template_folder
|
||||
|
||||
def show
|
||||
@templates = @template_folder.templates.active.accessible_by(current_ability)
|
||||
.preload(:author, :template_accesses)
|
||||
@templates = Templates.search(current_user, @templates, params[:q])
|
||||
@templates = Templates::Order.call(@templates, current_user, cookies.permanent[:dashboard_templates_order])
|
||||
helper_method :selected_order
|
||||
|
||||
@pagy, @templates = pagy_auto(@templates, limit: 12)
|
||||
TEMPLATES_PER_PAGE = 12
|
||||
FOLDERS_PER_PAGE = 18
|
||||
|
||||
def show
|
||||
@templates = Template.active.accessible_by(current_ability)
|
||||
.where(folder: [@template_folder, *(params[:q].present? ? @template_folder.subfolders : [])])
|
||||
.preload(:author, :template_accesses)
|
||||
|
||||
@template_folders = filter_folders(@template_folder)
|
||||
@template_folders = TemplateFolders.sort(@template_folders, current_user, selected_order)
|
||||
|
||||
if @templates.exists?
|
||||
@templates = Templates.search(current_user, @templates, params[:q])
|
||||
@templates = Templates::Order.call(@templates, current_user, selected_order)
|
||||
|
||||
limit =
|
||||
if @template_folders.size < 4
|
||||
TEMPLATES_PER_PAGE
|
||||
else
|
||||
(@template_folders.size < 7 ? 9 : 6)
|
||||
end
|
||||
|
||||
@pagy, @templates = pagy_auto(@templates.select_for_list, limit:)
|
||||
|
||||
if params[:q].present? && @templates.blank?
|
||||
@related_submissions_pagy, @related_submissions = load_related_submissions(@template_folder)
|
||||
end
|
||||
else
|
||||
@pagy, @template_folders = pagy(@template_folders, limit: FOLDERS_PER_PAGE)
|
||||
|
||||
@templates = @templates.none
|
||||
end
|
||||
|
||||
render_infinite_scroll(@pagy, @templates, @template_folders) if turbo_infinite_scroll?
|
||||
end
|
||||
|
||||
def edit; end
|
||||
@@ -25,7 +54,54 @@ class TemplateFoldersController < ApplicationController
|
||||
|
||||
private
|
||||
|
||||
def filter_folders(template_folder)
|
||||
template_folders =
|
||||
template_folder.subfolders.where(id: Template.accessible_by(current_ability).active.select(:folder_id))
|
||||
|
||||
TemplateFolders.search(template_folders, params[:q])
|
||||
end
|
||||
|
||||
def render_infinite_scroll(pagy, templates, template_folders)
|
||||
render_params =
|
||||
if templates.present?
|
||||
{ partial: 'templates/template', collection: templates }
|
||||
else
|
||||
{ partial: 'template_folders/folder', collection: template_folders, as: :folder }
|
||||
end
|
||||
|
||||
render turbo_stream: [
|
||||
turbo_stream.before('infinite_scroll', **render_params),
|
||||
turbo_stream.replace('infinite_scroll', partial: 'shared/infinite_scroll', locals: { pagy: })
|
||||
]
|
||||
end
|
||||
|
||||
def selected_order
|
||||
@selected_order ||=
|
||||
if can?(:manage, :countless)
|
||||
'created_at'
|
||||
else
|
||||
cookies.permanent[:dashboard_templates_order].presence || 'created_at'
|
||||
end
|
||||
end
|
||||
|
||||
def template_folder_params
|
||||
params.require(:template_folder).permit(:name)
|
||||
end
|
||||
|
||||
def load_related_submissions(template_folder)
|
||||
related_submissions =
|
||||
Submission.accessible_by(current_ability)
|
||||
.where(archived_at: nil)
|
||||
.where(template_id: current_account.templates.active
|
||||
.where(folder: [template_folder, *template_folder.subfolders])
|
||||
.select(:id))
|
||||
.preload(:template_accesses, :created_by_user,
|
||||
template: :author,
|
||||
submitters: :start_form_submission_events)
|
||||
|
||||
related_submissions = Submissions.search(current_user, related_submissions, params[:q])
|
||||
.order(id: :desc)
|
||||
|
||||
pagy_auto(related_submissions.select_for_list, limit: 5)
|
||||
end
|
||||
end
|
||||
@@ -4,9 +4,43 @@ class TemplatesArchivedController < ApplicationController
|
||||
load_and_authorize_resource :template, parent: false
|
||||
|
||||
def index
|
||||
@templates = @templates.where.not(archived_at: nil).preload(:author, :folder, :template_accesses).order(id: :desc)
|
||||
@templates = @templates.where.not(archived_at: nil)
|
||||
.preload(:author, :template_accesses, folder: :parent_folder)
|
||||
.order(id: :desc)
|
||||
|
||||
@templates = Templates.search(current_user, @templates, params[:q])
|
||||
|
||||
@pagy, @templates = pagy_auto(@templates, limit: 12)
|
||||
@pagy, @templates = pagy_auto(@templates.select_for_list, limit: 12)
|
||||
|
||||
if params[:q].present? && @templates.blank?
|
||||
@related_submissions_pagy, @related_submissions = load_related_submissions
|
||||
end
|
||||
|
||||
render_infinite_scroll(@pagy, @templates) if turbo_infinite_scroll?
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def render_infinite_scroll(pagy, templates)
|
||||
render turbo_stream: [
|
||||
turbo_stream.before('infinite_scroll', partial: 'templates/template', collection: templates,
|
||||
locals: { with_folder: true }),
|
||||
turbo_stream.replace('infinite_scroll', partial: 'shared/infinite_scroll', locals: { pagy: })
|
||||
]
|
||||
end
|
||||
|
||||
def load_related_submissions
|
||||
related_submissions =
|
||||
Submission.accessible_by(current_ability)
|
||||
.joins(:template)
|
||||
.where.not(templates: { archived_at: nil })
|
||||
.preload(:template_accesses, :created_by_user,
|
||||
template: :author,
|
||||
submitters: :start_form_submission_events)
|
||||
|
||||
related_submissions = Submissions.search(current_user, related_submissions, params[:q])
|
||||
.order(id: :desc)
|
||||
|
||||
pagy_auto(related_submissions.select_for_list, limit: 5)
|
||||
end
|
||||
end
|
||||
@@ -10,13 +10,25 @@ class TemplatesArchivedSubmissionsController < ApplicationController
|
||||
@submissions = Submissions::Filter.call(@submissions, current_user, params)
|
||||
|
||||
@submissions = if params[:completed_at_from].present? || params[:completed_at_to].present?
|
||||
@submissions.order(Submitter.arel_table[:completed_at].maximum.desc)
|
||||
@submissions.order(completed_at: :desc)
|
||||
else
|
||||
@submissions.order(id: :desc)
|
||||
end
|
||||
|
||||
@pagy, @submissions = pagy_auto(@submissions.preload(submitters: :start_form_submission_events))
|
||||
@pagy, @submissions = pagy_auto(@submissions.select_for_list.preload(submitters: :start_form_submission_events))
|
||||
|
||||
render_infinite_scroll(@pagy, @submissions, @template) if turbo_infinite_scroll?
|
||||
rescue ActiveRecord::RecordNotFound
|
||||
redirect_to root_path
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def render_infinite_scroll(pagy, submissions, template)
|
||||
render turbo_stream: [
|
||||
turbo_stream.before('infinite_scroll', partial: 'templates/submission', collection: submissions,
|
||||
locals: { template:, archived: true }),
|
||||
turbo_stream.replace('infinite_scroll', partial: 'shared/infinite_scroll', locals: { pagy: })
|
||||
]
|
||||
end
|
||||
end
|
||||
Loaded 100 of 787 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user