mirror of
https://github.com/docusealco/docuseal.git
synced 2026-08-07 07:14:43 +00:00
Compare commits
1980 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 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 | |||
| 055f7e2bc1 | |||
| dae3b4b6b5 | |||
| f2888a08dc | |||
| 95627c5963 | |||
| 4bb3721db0 | |||
| b12111383b | |||
| 263c0ff939 | |||
| efc8ba6b0a | |||
| 4c530b3f88 | |||
| f6358c9d47 | |||
| b3baea2f6f | |||
| c0594a8b74 | |||
| 1403401b03 | |||
| 700184f776 | |||
| b6a1a3ee4d | |||
| 8cbf5802b5 | |||
| ab9706afef | |||
| f07f9d8cf9 | |||
| 591caecb92 | |||
| ef4c2b7213 | |||
| 18999255a9 | |||
| 0a2a1de6b9 | |||
| 1b857b6eb3 | |||
| e5e6b11c95 | |||
| 3ec3f58d55 | |||
| 3bfce862ec | |||
| c01b67f7ec | |||
| ea0efe9314 | |||
| 28b583691f | |||
| 33811945be | |||
| 9d1860f038 | |||
| f03166b4ea | |||
| c59d948d41 | |||
| 57d63dd2f2 | |||
| c88715daec | |||
| 0375dad7c9 | |||
| de3f83db96 | |||
| a7bb96464c | |||
| 1d48fe9621 | |||
| 9544bb3d76 | |||
| 4e94664008 | |||
| e41fe5fbca | |||
| 24d713dec6 | |||
| f464e1c46a | |||
| 85d52d1f28 | |||
| e77b5fa2c9 | |||
| c91b4a765b | |||
| 2a031c14c8 | |||
| f7dfc0b2ed | |||
| 3b894f5281 | |||
| 9645332e48 | |||
| 5670390b37 | |||
| 260bd6d5d5 | |||
| fa0733f480 | |||
| 8b9678a434 | |||
| 43669bf095 | |||
| 2ef8df3f0c | |||
| 1c9d15aa7e | |||
| 9fcd8955d3 | |||
| 5e44ba49b8 | |||
| ac1f90e711 | |||
| c6eae66127 | |||
| 08aa902e07 | |||
| 1690337699 | |||
| f78cf8cc6e | |||
| 343118d00d | |||
| a3bd37c9a2 | |||
| 8bc13a5900 | |||
| 21138c1d02 | |||
| 38674a9fc9 | |||
| a7ed0b86a4 | |||
| 91a96f06c8 | |||
| 23631b75f2 | |||
| 0fb9c2bbf6 | |||
| 14fa8fc8e7 | |||
| 047c192988 | |||
| bfa244c8fa | |||
| 019222ae40 | |||
| 8a10852fe2 | |||
| 01232aed5a | |||
| 82f3ff4824 | |||
| 7be3f5a931 | |||
| 9d013a7384 | |||
| 0c71d7bff8 | |||
| fdd030652e | |||
| f2bf1461f0 | |||
| fc6f796f30 | |||
| 5f44806287 | |||
| 318b65af18 | |||
| 91b182b04a | |||
| 15b886d4ff | |||
| 0122a237e3 | |||
| 2dca7d8a8e | |||
| 87265e2f22 | |||
| 3166defa1a | |||
| 93ba17f42e | |||
| 751be1dae5 | |||
| 3f8d2831e2 | |||
| 4d8942803e | |||
| 46d9bb2c87 | |||
| 34433c0575 | |||
| 544306eac4 | |||
| 0eeebfd3cf | |||
| 050224a10d | |||
| 7b6fc7d06f | |||
| 7fb148c85c | |||
| dffa59fecd | |||
| 24fe2a1192 | |||
| 97b8ac4444 | |||
| e34a763dd9 | |||
| 298b1d3e1e | |||
| d9978519cc | |||
| b4a68cf79e | |||
| 5663e60f92 | |||
| 19e8835809 | |||
| 330fae927f | |||
| c5a5212947 | |||
| 21d81e38bf | |||
| 5b1a4ebffd | |||
| 5f5dabd0e5 | |||
| ff46993de6 | |||
| 7676db2196 | |||
| 3ef0d28ffe | |||
| 231177f057 | |||
| e5f7739952 | |||
| 4a8e2a82e5 | |||
| 6efe8027f1 | |||
| 96bb4eb6e5 | |||
| 38e68814cf | |||
| 786c80bee7 | |||
| 98dcce299e | |||
| cbff242a5d | |||
| 1876b3efba | |||
| 51091f05f0 | |||
| d76eee2ae4 | |||
| d7bfa96d57 | |||
| be60608211 | |||
| 4e5abb3c09 | |||
| 7fee82ded2 | |||
| e1cd62ff40 | |||
| b498291b62 | |||
| def3009846 | |||
| 9d43fd9aa5 | |||
| 22a21fb037 | |||
| 2a9c375273 | |||
| 847e70ec1b | |||
| 7964d6e0f0 | |||
| d0259791bf | |||
| a9dcaeccbc | |||
| 501b91a603 | |||
| 2b5f772feb | |||
| 7cada0af8b | |||
| 67be785e69 | |||
| d1cfc64adc | |||
| 4e5157d1ee | |||
| 5e27cc36c6 | |||
| 6e81a3be3f | |||
| 858b2f33fd | |||
| 6adcc87b2b | |||
| e1d5a5b6a6 | |||
| f090e79002 | |||
| 56b32e35a2 | |||
| b40c27cdbf | |||
| c4505e67f0 | |||
| d0799b8cc1 | |||
| 889fcad859 | |||
| c133fef99d | |||
| cfdc978a2a | |||
| bff1651966 | |||
| b97854f2fe | |||
| 7a4629bf79 | |||
| 2573d8cb19 | |||
| dd559e90f1 | |||
| e5db61e358 | |||
| 69376c0faf | |||
| 1f48be135c | |||
| 1cae12b728 | |||
| 86d2cd93f8 | |||
| b0110d9340 | |||
| 7e70f4fb14 | |||
| 28e3d65e63 | |||
| f1a2b71b59 | |||
| 47c7270911 | |||
| 4e1fa336a2 | |||
| 244bc32136 | |||
| 42c30bdfd0 | |||
| 0abf48ddba | |||
| ef05f030de | |||
| b8c36ff09c | |||
| 1ba8a9a374 | |||
| 661e9b69fb | |||
| a1aeabccff | |||
| 0e5e0e7772 | |||
| 2a74fdd095 | |||
| ac426e55b2 | |||
| ad2b35673a | |||
| af1e8fa6f2 | |||
| 5800cb6c5a | |||
| 64d3360e82 | |||
| 3ae98bf03e | |||
| 65e81d90f4 | |||
| 4fb9637e2d | |||
| 6b3726dbd5 | |||
| c5b0f2c76c | |||
| ea18f9e28a | |||
| d229c75978 | |||
| ecbe3c3e4f | |||
| 38a5cb968b | |||
| a30ee9e961 | |||
| eba27004dc | |||
| 09849acd2f | |||
| 19e9047a39 | |||
| 82b6bdd9ad | |||
| 5764b4dac1 | |||
| 3affd67d24 | |||
| d5a98a0f9e | |||
| 9629cc7226 | |||
| 06415ad869 | |||
| 3f872da435 | |||
| bc1d5a9825 | |||
| 72c8ed9a00 | |||
| 3fec433846 | |||
| 3b572702d7 | |||
| febdc0b48d | |||
| 8773bb2f62 | |||
| 55ad70fabc | |||
| 23dcd62672 | |||
| 42371d7ce3 | |||
| 4116746be3 | |||
| b2b2856ac0 | |||
| b65bca213f | |||
| 7a72e2cbb7 | |||
| 3a73398a1e | |||
| cce20d9230 | |||
| 8b72c91951 | |||
| b0d8e47a75 | |||
| 85824faa2b | |||
| 8a507339af | |||
| 4636849118 | |||
| cc52cae9fc | |||
| b6bb4d2c8b | |||
| 5032676bd2 | |||
| 146b37f8cd | |||
| 47b3a2d09e | |||
| f2306b334b | |||
| 3affa65571 | |||
| 6f3be16826 | |||
| 07ce76537b | |||
| a1fb38b8cb | |||
| 1b52a99662 | |||
| f5facfa6b9 | |||
| 21c6144dc6 | |||
| 2c155f0e42 | |||
| 33cc0a8ab5 | |||
| 1e65a7e52b | |||
| 14e2dbe1aa | |||
| a823a6ac56 | |||
| cfc4e001ba | |||
| 496cc855f6 | |||
| bfb2db07a5 | |||
| 9f4f868246 | |||
| f91f02f786 | |||
| 1da72e3e7a | |||
| 2e2c0e087d | |||
| 0c72f58ab0 | |||
| d9d969cb79 | |||
| 8d685d1f73 | |||
| e406620044 | |||
| a315674421 | |||
| 8d953f210f | |||
| 7439ef07ad | |||
| fc4b4de6ed | |||
| 96e4733f87 | |||
| 8b6731a289 | |||
| f93ac68e9c | |||
| 439df4879c | |||
| 83b6ce8574 | |||
| 6ea07bc13b | |||
| a15624690b | |||
| 974bb85fb7 | |||
| 5dc80601b2 | |||
| df2e53bc40 | |||
| 6e685bbc32 | |||
| 0b3ca0de96 | |||
| aed3f1f141 | |||
| 1a7981e919 | |||
| 4add805371 | |||
| 66c18317b7 | |||
| 9edbb885b4 | |||
| c67c80d6bc | |||
| 0399531e0c | |||
| 54426556d9 | |||
| fa74ee1755 | |||
| eb53af12c4 | |||
| 38efcb99c0 | |||
| a92d914674 | |||
| 219a698aa5 | |||
| 5bed6448dc | |||
| 6d2a8ca3d3 | |||
| bfb2e0043d | |||
| 922101b12c | |||
| 9ff41a0c4a | |||
| 071f187605 | |||
| ab6cd0e89c | |||
| 97c0dad7ff | |||
| bc0917a190 | |||
| 5718acecff | |||
| 395d43dbeb | |||
| 37de5e0d0d | |||
| 44e848a1f0 | |||
| f6d59d731c | |||
| 87b039e709 | |||
| e0c6a4e08b | |||
| 80b06550c2 | |||
| ec8e35ed7a | |||
| 4dc325a6bb | |||
| d52c57df72 | |||
| 0cb2557447 | |||
| cd05c1fd19 | |||
| 5458e82b6e | |||
| c59c58624b | |||
| 2da059d440 | |||
| b0aabd9e79 | |||
| f1cd60dfad | |||
| a264a8522a | |||
| 794e339425 | |||
| 9aaf61e658 | |||
| 37faa7ece6 | |||
| 666e3fde4c | |||
| 995e040a4e | |||
| 185ac2cbee | |||
| a4e372ff5c | |||
| b4834c7674 | |||
| c24ae1751e | |||
| 9bd0ad70c4 | |||
| f210f1d8bf | |||
| 92b61992f2 | |||
| e3f3895aac | |||
| c696890cf5 | |||
| 288d204d8e | |||
| 5a96f4f1eb | |||
| 5ff4f9889e | |||
| 78fb93fd02 | |||
| 3c83d459bd | |||
| 6cdb8e7132 | |||
| e2b93cbcab | |||
| a59cc7c44e | |||
| 1984b99e04 | |||
| fe3e8fba3e | |||
| 97ecdf2ff0 | |||
| e4c62087d0 | |||
| 1f9faddfd0 | |||
| 6dff6483bf | |||
| 2e43eb189f | |||
| cee471bfa5 | |||
| 81521dee81 | |||
| 3cac62d0fd | |||
| 24ed3f2e7c | |||
| 02ad620c9f | |||
| 9051d5a835 | |||
| 0abd270f56 | |||
| fde9affd70 | |||
| 4edee2971a | |||
| 313d7f8408 | |||
| db60810272 | |||
| b3507c1a3a | |||
| 19b5a07bac | |||
| 616aced4e4 | |||
| cc218fc0b0 | |||
| c59f2a022a | |||
| 2a240cebeb | |||
| b950b1c63e | |||
| 4ad0b04b7f | |||
| 8edf6405b0 | |||
| 8e3caae849 | |||
| ac9a5c9556 | |||
| 20d09f6dff | |||
| 333a758688 | |||
| 2c8b4d5816 | |||
| 9ce822bce8 | |||
| 0a382a688c | |||
| bd3008a35a | |||
| e9bd11cb2e | |||
| ca429bc445 | |||
| 21a0c54843 | |||
| 2f41d13009 | |||
| f69dcf77de | |||
| 32596d738c | |||
| e7e5d0e776 | |||
| bb3b38e76f | |||
| bc4c21f804 | |||
| 2fb5de4fa9 | |||
| 2e58b2e37b | |||
| 780f5512f4 | |||
| 2cf4908d53 | |||
| 4a9b898713 | |||
| bb552d13d4 | |||
| 15d06f4fb0 | |||
| 35a2aba887 | |||
| 48a82b1d49 | |||
| b8453e14c4 | |||
| 29fbdfbe2f | |||
| 5340af0bc6 | |||
| 1881b6c50f | |||
| b6a2aae970 | |||
| b317094192 | |||
| ab1f4cf297 | |||
| d3033c072a | |||
| f380200e03 | |||
| a4928268ea | |||
| dd18c51384 | |||
| c2014a1549 | |||
| 46edc7dc37 | |||
| d62d83ef8e | |||
| 79350c8fba | |||
| 6702dd52b6 | |||
| c8b74a2c43 | |||
| 1746920eb5 | |||
| bacbc4309c | |||
| 4429d742b3 | |||
| 38b5eccb65 | |||
| d8126c4a47 | |||
| b1c31672ee | |||
| 975f3a95fe | |||
| e3bed5ae67 | |||
| 4737b103c0 | |||
| c5178b96d3 | |||
| 39a2159196 | |||
| 5724159b7b | |||
| 05b7cc9fa6 | |||
| 18b13b5b1e | |||
| d25555b9b0 | |||
| eec266ee83 | |||
| 75683631f0 | |||
| 36c64d1de4 | |||
| 2552b24f21 | |||
| e6ca6dfd85 | |||
| fa2d86b20a | |||
| 8e212e4586 | |||
| a59c18f04b | |||
| 1ab0f1c90c | |||
| 9beaaa0851 | |||
| b86aac86d1 | |||
| c2eb94b571 | |||
| a377e45a63 | |||
| 58574abc6b | |||
| cbae888971 | |||
| 944e1f31db | |||
| 711b3b3190 | |||
| e7abadcefb | |||
| 18d60f5522 | |||
| 6d5c6e086d | |||
| 5a48ab0360 | |||
| 599785b6d1 | |||
| f2fd5fb82b | |||
| f2782bd84e | |||
| e52a721fc9 | |||
| b035123ff9 | |||
| ca8de23082 | |||
| f514654c40 | |||
| 99bc528a6c | |||
| f3c77d0177 | |||
| db2b5f35bd | |||
| 5728381725 | |||
| a7affb1a49 | |||
| 21bf5ed9f7 | |||
| 8ddd4ee916 | |||
| 0e2fd8ee4f | |||
| 27901063ed | |||
| ce31ebde1e | |||
| 0c2abf211e | |||
| 8ecaa4fd7a | |||
| 6759c26d86 | |||
| 76f1f140ac | |||
| ca7ab5aab7 | |||
| fd3530ac62 | |||
| 4ea863592b | |||
| 3f87d0336d | |||
| e251482481 | |||
| c79fca75f3 | |||
| 0b0f0b497e | |||
| e9034d8da7 | |||
| 8fbb8b8f6e | |||
| 4635fdd32b | |||
| 6dd4382446 | |||
| 50acff2e81 | |||
| cb6d24dfca | |||
| ab4a22d8ba | |||
| bc40a15de1 | |||
| 2542f26d96 | |||
| 84edb6dbda | |||
| 88776e7316 | |||
| f669045362 | |||
| 68d3efbdba | |||
| 22d4833496 | |||
| ff14caa9f7 | |||
| 05647135bc | |||
| ae76c2a4a8 | |||
| fb2b3d44fe | |||
| 3868557efd | |||
| 9bef28c8ba | |||
| f25fd3b03d | |||
| d1d7571ee4 | |||
| 6a298a068c | |||
| d4aef79b68 | |||
| 05d578d88f | |||
| 8773b30b10 | |||
| 1b636c5878 | |||
| 46e948ab99 | |||
| 889103aa5e | |||
| 558a14a5f2 | |||
| 72bbdb38bf | |||
| 3975a03cb4 | |||
| e398327fb7 | |||
| 9266694d65 | |||
| 350c4b1c2a | |||
| c3a933d87d | |||
| 970e53a5a3 | |||
| 520bac2ec7 | |||
| e1eddd31c4 | |||
| 3c616a824c | |||
| 90d86b58c2 | |||
| 4332b4ebb8 | |||
| e2f930f6f7 | |||
| c303159c63 | |||
| c1ce2bec00 | |||
| 3d2046f227 | |||
| babaf1aa48 | |||
| 3f03d343ca | |||
| 110cdb5123 | |||
| 440d1114be | |||
| 9032517d4e | |||
| 7c71fefc47 | |||
| e8bd8ff76f | |||
| fed420cae1 | |||
| 910490528d | |||
| ab810be250 | |||
| 178ee47093 | |||
| 6f90147a11 | |||
| 3ebaacbb7a | |||
| 64b39b53a2 | |||
| 2948910c25 | |||
| 1d20c79cf4 | |||
| 48f3b13f3e | |||
| 1fc1e9af16 | |||
| ce70016603 | |||
| e0e79e343d | |||
| 6568f81ee9 | |||
| 386f130d7a | |||
| 3ee41975fd | |||
| d9f314898b | |||
| 4fc7412cac | |||
| 9adbfc52c2 | |||
| 1ad8d9196a | |||
| e9172392b5 | |||
| d6d94de328 | |||
| 77261f457c | |||
| c258915031 | |||
| c1db8bdd3b | |||
| c700484ce8 | |||
| b62bd200ed | |||
| a164fe8dd8 | |||
| bb40e872b3 | |||
| bab80e9488 | |||
| d090d79ebf | |||
| 2299ba4e29 | |||
| d6474f0e6e | |||
| e0a297cf81 | |||
| 40f5fa4217 | |||
| 94e552d170 | |||
| cf1b0a4c16 | |||
| 9aa0a8ecbf | |||
| fd4bef1706 | |||
| d32b0e6d20 | |||
| 4a846be214 | |||
| 3dd3877ea9 | |||
| b4dba0d809 | |||
| dea80a82d8 | |||
| eefa994724 | |||
| 35cce24539 | |||
| e6c8203959 | |||
| 9479771fc8 | |||
| 711427521c | |||
| 5dae742361 | |||
| d8ccd4beee | |||
| c532a0115d | |||
| c9c3364764 | |||
| 1ff23d7c40 | |||
| 95e9b34033 | |||
| 6b42dcf91b | |||
| 28962c949b | |||
| 4ecbbf8410 | |||
| 990b35be18 | |||
| 8002764855 | |||
| b80982658c | |||
| 443ac23c29 | |||
| 37bb987e3e | |||
| c7b08e8d30 | |||
| dd502e8cef | |||
| a554182e57 | |||
| 0c039ee64f | |||
| c6b004f107 | |||
| 2fcb08d27d | |||
| 75fe5ce381 | |||
| fd7d0eaf53 | |||
| 7e537e4232 | |||
| a892975010 | |||
| 05ec92186b | |||
| 8b035f80b3 | |||
| c717293306 | |||
| b3f3cf2aa6 | |||
| 7406334f77 | |||
| 3e1350a221 | |||
| 961e0a1c89 | |||
| 2ce9bbecb9 | |||
| 704da3a2ec | |||
| 2f530ccfca | |||
| e6c20f5ff6 | |||
| d267978437 | |||
| e9d728fa4a | |||
| 3c60fb4ee6 | |||
| f1e494b669 | |||
| f7a8e4102b | |||
| 311c2b0e34 | |||
| 5dd59f4f7c | |||
| 571f5d8221 | |||
| 136b0eb5ed | |||
| 2d813e184b | |||
| fd23674e87 | |||
| 3b9d35acf1 | |||
| 17fc6e2f1c | |||
| 2e132d8b5a | |||
| 53bef85a63 | |||
| 99ad6e36ac | |||
| bfa41ac35f | |||
| bc4414a368 | |||
| 26ae5f1d80 | |||
| be4962a02a | |||
| e72090c2e0 | |||
| ad8bca8bf4 | |||
| 22be3b37cf | |||
| cfc32ee522 | |||
| 10fbdf6612 | |||
| 3604fb6461 | |||
| 24392d0258 | |||
| 4ab1065773 | |||
| 50b0f7e5ed | |||
| be4a0505bc | |||
| bd853c6265 | |||
| 5a1efd0e27 | |||
| 71b8cc7172 | |||
| 126a36dbf3 | |||
| 90db64cc03 | |||
| 227a067c77 | |||
| dc24df0d4c | |||
| 2598962c5b | |||
| b4884e94f6 | |||
| 4741e64178 | |||
| 25fe35e212 | |||
| 92909cea09 | |||
| b16322dd5b | |||
| a04c04141c | |||
| 90f21d3ed9 | |||
| f31225e8c2 | |||
| eead1b2bc4 | |||
| f90a4e5576 | |||
| 50987ce38b | |||
| 26113ac3a6 | |||
| 0b7b72ce16 | |||
| 95d48b2808 | |||
| 8fecf94a28 | |||
| 66dd87b290 | |||
| d20e8cc37d | |||
| c189e2ed20 | |||
| 57f8f45320 | |||
| 5684baf1e0 | |||
| b6f307690d | |||
| 66c44f8ff5 | |||
| 8f3592c481 | |||
| dc8b1e0741 | |||
| b806a6afc5 | |||
| d43f8f4dd8 | |||
| 37c9bf5741 | |||
| a0d37f516f | |||
| 2764a18252 | |||
| 96cf228e74 | |||
| ff6ff3a51e | |||
| 9472a6b5b0 | |||
| 5f3a8bf391 | |||
| a0a0fdd1b5 | |||
| 7a6f901b09 | |||
| c4f05fbd9e | |||
| 50c342ec5c | |||
| a9cfd2bcfc | |||
| e2bdb2acee | |||
| b8828b503c | |||
| d63f62109b | |||
| c04483f0ea | |||
| 1efaf30fb7 | |||
| 2e462e9633 | |||
| 838df5f41d | |||
| 32445f0e19 | |||
| 5f9067baee | |||
| ad56d5ee14 | |||
| a091d121d9 | |||
| cd9c3ee3c1 | |||
| 534c3a2e10 | |||
| 7bc8e194df | |||
| de5c4d5bcd | |||
| 1e6b5c684f | |||
| 4651f6b652 | |||
| e5b0a2355f | |||
| db22fe7518 | |||
| 0dcd97f49b | |||
| de14330449 | |||
| 173658138c | |||
| 000f3c72fc | |||
| 561b16e9fb | |||
| 507ca49112 | |||
| 94d5ab4ace | |||
| 91d44a3e31 | |||
| 478167ea4f | |||
| 6b1c3bba19 | |||
| 73841f751c | |||
| 22dbe3291f | |||
| 1dac3fe434 | |||
| d0c49ac04c | |||
| 3877dbf83f | |||
| d15fd1fb84 | |||
| 0a105490fe | |||
| 0bd35b526c | |||
| 3a2910f717 | |||
| c4a91c2b34 | |||
| 5643380916 | |||
| cf6b378ccb | |||
| 66bc8cfb5f | |||
| 994bd1fe5c | |||
| 12528b6527 | |||
| d135c93939 | |||
| 637011d55a | |||
| b94e6bf54a | |||
| 8f975c2898 | |||
| d25c4fa766 | |||
| 43db15b350 | |||
| 09074e2ac0 | |||
| 04295f4540 | |||
| 11c48a495f | |||
| 942c961cd1 | |||
| 9193511d5f | |||
| e0ddf97771 | |||
| 2bd249bd5a | |||
| b72c99dbce | |||
| fc0504f91e | |||
| 1da2196b0e | |||
| 4bdb249a2b | |||
| cb38549480 | |||
| ee9ee6ee32 | |||
| 748eaaa98d | |||
| 9e4ec11e33 | |||
| 93d2b76eae | |||
| 2c8942e888 | |||
| 4424840c81 | |||
| 68392b4e74 | |||
| 0b4e345ad6 | |||
| 1cea5f5a99 | |||
| cdaadbab20 | |||
| d1ec8c6837 | |||
| cdffa66db1 | |||
| fca21ab476 | |||
| b1d2d92f54 | |||
| 10bb9112c7 | |||
| 58b337a3ee | |||
| de52f2f5e5 | |||
| f10e941529 | |||
| 78b1864d7d | |||
| ab49af9bcb | |||
| 286b8d7067 | |||
| 21c087cba7 | |||
| ddd881a52f | |||
| 12c69578af | |||
| e96cb5eb8a | |||
| c01cfe83be | |||
| b92d1ee38c | |||
| b90c67b184 | |||
| edb29977dd | |||
| e55c8f251d | |||
| f1d6bbdc67 | |||
| 8da4c2fa10 | |||
| e872fa2497 | |||
| 165bfef9b9 | |||
| 8a50c755d2 | |||
| 002bc315f6 | |||
| e87522eeab | |||
| 377244f948 | |||
| 73423a6f44 | |||
| d219e3cd7e | |||
| 09b0022195 | |||
| 8aac935641 | |||
| 16e5c13633 | |||
| fff1831768 | |||
| f91a318e94 | |||
| ed06e17b23 | |||
| 63e1be7dfe | |||
| 16044612bd | |||
| 27d8880414 | |||
| a0aae6a9de | |||
| 749043dfbb | |||
| 320fe02e35 | |||
| 6f4b9d4a4e | |||
| a589cf9471 | |||
| 7fc7283191 | |||
| af03560494 | |||
| 988d1b93a5 | |||
| ab0278198a | |||
| fbc649ab59 | |||
| 6b5adc2504 | |||
| 7c4307ef10 | |||
| de744e2f6c | |||
| c171597c7f | |||
| 3034c00bb3 | |||
| 9539c476c0 | |||
| 4552c72860 | |||
| 85f8a92721 | |||
| 049b9c78c2 | |||
| acf06084bf | |||
| 61da477733 | |||
| fdeec548a3 | |||
| ff0d486831 | |||
| d5bf54ee26 | |||
| 7616774613 | |||
| 27c4dc7eca | |||
| 4ae834d6f7 | |||
| b71a66a082 | |||
| 80cc38542c | |||
| 185aecec42 | |||
| 8c092eba78 | |||
| 781ccc701d | |||
| 009e05584a | |||
| f9d52e56a2 | |||
| 9e66f8412c | |||
| 181e42c4b3 | |||
| 15e60fc5e7 | |||
| ee357e58ac | |||
| 0d9a709fa3 | |||
| 7eae09bdb0 | |||
| e8e32e5d1f | |||
| a3d54db4c6 | |||
| 9686ae6250 | |||
| 044195f07b | |||
| 35f159266a | |||
| 3b717ae3de | |||
| 2389c3fc40 | |||
| 92da790e77 | |||
| 14418b93b1 | |||
| 1289aff493 | |||
| 7568f563cb | |||
| 5ae56bd50c | |||
| c2ded81959 | |||
| f2c307ef63 | |||
| af787887d8 | |||
| 54272d83eb | |||
| 3070fc2074 | |||
| 6a31d2c722 | |||
| f26bfe9acc | |||
| a729fd6234 | |||
| c2051c156e | |||
| 3852870c09 | |||
| a58253a6a1 | |||
| 30c98a970d | |||
| 8f2edef09e | |||
| 9e465ea47e | |||
| cdda55d528 | |||
| db302858c4 | |||
| b81cc83027 | |||
| 766d653349 | |||
| 98c04eac15 | |||
| c35dfea154 | |||
| 37a9a86a1d | |||
| e9a759055b | |||
| f6c061a57b | |||
| 91f3d8ed7e | |||
| 3b18502d3d | |||
| df8e79683a | |||
| 7080e8ea33 | |||
| c56f8cb38f | |||
| 1ceac968b0 | |||
| 938ee99cfc | |||
| 2367040e45 | |||
| df19aabfba | |||
| f7b5bcd52f | |||
| a249b0a678 | |||
| 99bc46b40f | |||
| c1e6f0970d | |||
| e8c2316bb2 | |||
| 9866e5468c | |||
| fc60df7974 | |||
| 08784d28c7 | |||
| 0440ea652f | |||
| e466562391 | |||
| b2c8667c53 | |||
| d6893079f7 | |||
| ac1d973605 | |||
| 1405fefc85 | |||
| 2a75055fb6 | |||
| f84fdd51de | |||
| b38073126e | |||
| c6e681195d | |||
| 53d924be0b | |||
| d6cf027d5d | |||
| ec332bab88 | |||
| 8b3b90a7e2 | |||
| 9047e3bed3 | |||
| 72b7391245 | |||
| 2df05f0327 | |||
| 95f3eb0e5d | |||
| ce8d964ce6 | |||
| f0d5fc4e2d | |||
| b4b17d847e | |||
| fd537e7ac0 | |||
| 8c271104e1 | |||
| 51edd60d96 | |||
| a265ca1716 | |||
| a773f63a65 | |||
| 576c8a2e24 | |||
| 5829de9ee4 | |||
| b202941b79 | |||
| 6f6fb74988 | |||
| 0137bdbe4c | |||
| 55d9511c01 | |||
| 929d29576d | |||
| 6f7128a51c | |||
| fc5c4ff2fc | |||
| a81595db2b | |||
| a5615d500d | |||
| da9266befc | |||
| 5b3cffeb73 | |||
| 8f7b8a5fdf | |||
| 6d93116993 | |||
| a0f4916d3b | |||
| ace897eaf9 | |||
| b8667c5408 | |||
| d87d66632e | |||
| 3015c2d156 | |||
| 52b9e9ee8e | |||
| 5bfad0271c | |||
| 58797891c7 | |||
| 897750ba1e | |||
| 99341e773f | |||
| 71319c1548 | |||
| c0a42f6bfc | |||
| d5abb46112 | |||
| 2508b5fc8a | |||
| 4bd1ef0da3 | |||
| 4efd748571 | |||
| 859892d1fd | |||
| 7adeaf93cd | |||
| 5dbe155ecb | |||
| 667d065bee | |||
| bb613059de | |||
| 1b2dec4c2a | |||
| 2c2ef97c30 | |||
| 6df1d68237 | |||
| 20c9faedff | |||
| d931a40ea9 | |||
| 1097b98b85 | |||
| 2e597fcf43 | |||
| bb2fa6a261 | |||
| 1dc8369487 | |||
| e6d60f1985 | |||
| 82e3cb8a4d | |||
| aeec0cbf8e | |||
| 7a2d1fc6c5 | |||
| 68033ab7d2 | |||
| 7595c60ea8 | |||
| d9318c04f8 | |||
| 2d76198290 | |||
| 02a7b6ea55 | |||
| ab57076b8f | |||
| 82fba47c98 | |||
| 5f32e2a5af | |||
| 70012fbd7c | |||
| 668f6ca975 | |||
| e78dd301ef | |||
| f8ec3daf10 | |||
| 75c929639d | |||
| e9c1242b8d | |||
| e7eb1d4f12 | |||
| abbd3e809a | |||
| f430b4f5a6 | |||
| e65234b4c8 | |||
| fe04d22c94 | |||
| 44a7a5ebdc | |||
| 119ef0d0c7 | |||
| cdb8eb7931 | |||
| aef176c0cc | |||
| 0c21b55865 | |||
| 964b1444a5 | |||
| 48de343948 | |||
| 2640934c15 | |||
| 442dd4d6db | |||
| 3168feb3d6 | |||
| fbae4933f6 | |||
| 78bb66c344 | |||
| 5af65ef74f | |||
| 4d354ae9ff | |||
| 1e57c1ace7 | |||
| fefd138dfd | |||
| 16a77d3536 | |||
| e03857bdb7 | |||
| fc679bcd72 | |||
| 652b0da021 | |||
| 2d54b0db2f | |||
| 7a29894926 | |||
| f9ae79dd5b | |||
| cadb78ca35 | |||
| 36a6b7ff1d | |||
| efee46f113 | |||
| b6639738c3 | |||
| aab07c5f49 | |||
| 33c0784bc6 | |||
| 6f5ebfc8a6 | |||
| 811091ff14 | |||
| 6666f39928 | |||
| f67db9fe5f | |||
| 45f002803e | |||
| 8c19f90e88 | |||
| a8f5d5cea7 | |||
| 6788071eec | |||
| 0c690b3033 | |||
| dcb963b623 | |||
| 04e30dbdd6 | |||
| ac6d5a7aed | |||
| 3166a31b67 | |||
| fc3283eb3a | |||
| d6a96d4aa3 | |||
| cda349d71c | |||
| 87345a3b3d | |||
| b791d0699c | |||
| 2cb15f3282 | |||
| 8b823ca0d0 | |||
| f3f08797f7 | |||
| cab9672d25 | |||
| c8d3b1bace | |||
| 5ee51b2b57 | |||
| 525dc503a6 | |||
| 12cf28792b | |||
| 33466c1d07 | |||
| 226ae2172e | |||
| 8e2700b8b8 | |||
| 753cbd4d27 | |||
| f16405ca70 | |||
| 23f052c400 | |||
| 90a0a03e15 | |||
| 2dba8ae366 | |||
| 5bb7ad571c | |||
| 0cdb7c2c4f | |||
| 11bdc522cd | |||
| 014835f90d | |||
| 05e66b60ee | |||
| 505b185cf2 | |||
| 50c32ac6ff | |||
| 878ab25fd4 | |||
| 1b31772ff6 | |||
| d4940234b7 | |||
| 9bdc5d4863 | |||
| a73fd7929c | |||
| 8269a61751 | |||
| de072d1437 | |||
| 5b24793f2b | |||
| 660f7ab8f3 | |||
| 6e3fea5fa0 | |||
| 29ecbecbc2 | |||
| ad3bbe9892 | |||
| b570ebee69 | |||
| 207780fa3f | |||
| 8aa219433b | |||
| cb1e9f2be4 | |||
| 202943f58d | |||
| 6dd15b2091 | |||
| 1c55e4f18b | |||
| 6cc36b1728 | |||
| 37751cd5c1 | |||
| 80d882cd20 | |||
| 8bef75e570 | |||
| 077eab7005 | |||
| 715c70d422 | |||
| a3c23f9d53 | |||
| c1e67a8af0 | |||
| 2fdc294293 | |||
| 1f49ba069e | |||
| 43ef0fc0d0 | |||
| a1a920e536 | |||
| dec33b4331 | |||
| 432f290633 | |||
| cf98616a27 | |||
| 1a720cf1ad | |||
| 557c6c34f2 | |||
| 943c76505d | |||
| 66eb83e6a7 | |||
| 0844427357 | |||
| 72e9d9ea2d | |||
| 3db7d47746 | |||
| 95899336b8 | |||
| f6691c110e | |||
| 749722a9df | |||
| 265b668cb4 | |||
| 0527e6a5c1 | |||
| d989b61d19 | |||
| c1f3e6ff13 | |||
| 3c0fff2d82 | |||
| bd599c264f | |||
| 0274093c89 | |||
| 5845b5bdfd | |||
| 3dd0ba6b0b | |||
| e79306f8a3 | |||
| 0171b0c2dd | |||
| a963ef53d3 | |||
| 344f302c76 | |||
| 17ded282b2 | |||
| 2ce57df485 | |||
| 849289287e | |||
| 704fbc2393 | |||
| 168fc8bd03 | |||
| c5598d9efe | |||
| f02315726d | |||
| e56dcb59c0 | |||
| c74f350ea2 | |||
| af9d015cf2 | |||
| 633e5ac5e4 | |||
| 2d0f323854 | |||
| cfe5610ec9 | |||
| 55616ac321 | |||
| 8f2d1deb38 | |||
| e2763ceb80 | |||
| 10462f3140 | |||
| 3aef92486b | |||
| c1a9ee0bbb | |||
| 398fae82cb | |||
| a16443f961 | |||
| ad4e0e1d27 | |||
| 493a2108e9 | |||
| c2c9bb64b1 | |||
| bb1426c0af | |||
| 2505768e4d | |||
| 23a1b70534 | |||
| b34616ecbc | |||
| 0111252f6f | |||
| a561cc5f73 | |||
| 519e6f1f79 | |||
| da871b5d3a | |||
| ae3f939d23 | |||
| fb6ede564e | |||
| 1c701d4fe8 | |||
| c743cc689d | |||
| 8432bec3d6 | |||
| 8e72b0b0e4 | |||
| 19e4bbc01e | |||
| 56a9e412c8 | |||
| 4b75623b86 | |||
| e1ea42a997 | |||
| ee37710e57 | |||
| ae99218789 | |||
| 9fe8214ca0 | |||
| 6ca44a5cc3 | |||
| 801abd197b | |||
| 970977e469 | |||
| f83674b934 | |||
| 57c7dc1b5e | |||
| e9277f4eeb | |||
| 4052b6fe13 | |||
| 608347b1e6 | |||
| 345fe674d4 | |||
| 92f8901a67 | |||
| fcd40308ee | |||
| 2bd023e5d0 | |||
| a697d6330e | |||
| ab56e8be20 | |||
| b9a906b569 | |||
| 02adbae758 | |||
| 866aab7fac | |||
| c81b4d855b | |||
| 4ad58fc285 | |||
| 8227dd4f6a | |||
| beb677734f | |||
| 03db636d51 | |||
| b83abc9499 | |||
| 9738cdcf8f | |||
| 179ae01551 | |||
| 295860a9de | |||
| 0a6c15b6b8 | |||
| 18f80020ee | |||
| 3768fb94bf | |||
| 7b2cadcb91 | |||
| 4eb4e8c1f8 | |||
| 984df15949 | |||
| aa3bd24332 | |||
| 250889623d | |||
| 862fa55986 | |||
| 0f2889904a | |||
| 49f14c7929 | |||
| c3f3c5d5df | |||
| 274259f305 | |||
| a15d7a9cf3 | |||
| 7d154fc28d | |||
| 6727d3be83 | |||
| 4bbd5c3bcb | |||
| 5995f2e427 | |||
| 0910c8e100 | |||
| 8df76dfa90 | |||
| 78e9e66cb0 | |||
| 71c61fc4b3 | |||
| a728aa8689 | |||
| 5a3174a720 | |||
| 652072d660 | |||
| 323f5c0c15 | |||
| 229d78c78b | |||
| 9d9a955149 | |||
| b40a047190 | |||
| ff9e415823 | |||
| 2f7a4fb2b3 | |||
| f8a654a5b6 | |||
| 4449475467 | |||
| bcbbf0dd42 | |||
| 19b9ba52d4 | |||
| bdc30db2dc | |||
| 74634ba96b | |||
| 77feb05bad | |||
| a1f05d006e | |||
| cc3dae1335 | |||
| d9debcfb0d | |||
| 8770d16d52 | |||
| 84953481e4 | |||
| df7aa59f6f | |||
| 008b83119d | |||
| 15dafcb6ff | |||
| 7d291ed888 | |||
| a7221f0750 | |||
| 9a5b2a5d30 | |||
| 7361a00d42 | |||
| e07d402782 | |||
| a34354bee1 | |||
| 84b762b529 | |||
| 2a839951b4 | |||
| 26d7c2f047 | |||
| 151a5b85f3 | |||
| 3f506f53b4 | |||
| 95f734e4b8 | |||
| ff74574e84 | |||
| 843f6c405f | |||
| f8b24ebfce | |||
| d1d26b5424 | |||
| 8aa6f45da8 | |||
| 48d5bee95b | |||
| 98c79d2dec | |||
| 0f51238f62 | |||
| b7323b929f | |||
| 05eb1537f7 | |||
| bc9f27cfed | |||
| 8f457be719 | |||
| 6d590e13c9 | |||
| c987ec172a | |||
| 49f1637c47 | |||
| 13379bd699 | |||
| 5ee762c08c | |||
| edb6d561f1 | |||
| 1b90f77e82 | |||
| c37785ecbc | |||
| c67188edf8 | |||
| 8630c68631 | |||
| ddcb5c5d40 | |||
| 07f0fbb50f | |||
| 3781b0aa72 | |||
| 42a5cea521 | |||
| fb6000ca2c | |||
| 59f98db754 | |||
| 6add842a27 | |||
| 43ca4a6365 | |||
| 68196d6af0 | |||
| a582d39bb9 | |||
| 7a3e89c4d8 | |||
| 87f036ad88 | |||
| 5738b8dd58 | |||
| 8d73371e0f | |||
| 3d870b2305 | |||
| 27d8cd57bd | |||
| 353b2e7a94 | |||
| d77bd87acb | |||
| bae7b924ae | |||
| 89867fdde3 | |||
| c9a1f067ac | |||
| eb26835d57 | |||
| 7301661468 | |||
| e4fe984a70 | |||
| 12ccf622c1 | |||
| 6c35ff6d1c | |||
| 1d7c84302e | |||
| f3e8889710 | |||
| 4b2f9ff306 | |||
| b4ba642f8e | |||
| 5c69b965c0 | |||
| cba196385c | |||
| 00abad56cb | |||
| d1070c4955 | |||
| e84be523c6 | |||
| 5d598e8268 | |||
| 0e9d922204 | |||
| 80c1b04e83 | |||
| b2fca3edf3 | |||
| 2a292c8e80 | |||
| e6052ad0d8 | |||
| 8646e17d5a | |||
| e6785b0afd | |||
| 009033a411 | |||
| fdc3d7742a | |||
| f9ccca1376 | |||
| 0a00fe6ab8 | |||
| 6253e80a70 | |||
| 3dcb7c813f | |||
| 39cdc41a67 | |||
| d3000487f4 | |||
| 80254da1c1 | |||
| 73fc2d92f7 | |||
| f69aa33f3d | |||
| e16202c9f7 | |||
| 7453d0d274 | |||
| c102828c93 | |||
| afeb6dd318 | |||
| 6156623f0e | |||
| ccc06fcc3d | |||
| bbe3942056 | |||
| 49cea59b94 | |||
| ce1323e849 | |||
| fc3c1e65e3 | |||
| f1ebd8f912 | |||
| 98f5bba832 | |||
| e864e8a47f | |||
| a573545ead | |||
| 15ff4386e5 | |||
| 7a4aa3bf87 | |||
| c1ba6288c8 | |||
| d0ba180729 | |||
| 2edc76414b | |||
| 452db5dc39 | |||
| 21c4c53feb | |||
| 6292789cac | |||
| d445bbc02d | |||
| 8627f58cbc | |||
| 66ab0059bb | |||
| 3f06932277 | |||
| 59cf1b435e | |||
| c9a5b0e66e | |||
| bcd5e1da98 | |||
| f74ad050b4 | |||
| b6f43c9b23 | |||
| 298c64a119 | |||
| 8b80679b3a | |||
| 82a2bc561a | |||
| e019542d3f | |||
| 9356247ef0 | |||
| 4db77c18e3 | |||
| 0d0b248988 | |||
| 44f3910266 | |||
| 35c6798a80 | |||
| 0995c7ee63 | |||
| 0a25f78329 | |||
| bb8fffc491 | |||
| 6f6c51fb32 | |||
| 39d14c15d6 | |||
| 17de28965b | |||
| ae7865e508 | |||
| dc8e8cc5c9 | |||
| 5208470c38 | |||
| e7ef7b552f | |||
| a478112b07 | |||
| 7d0f32e1f9 | |||
| d35d8bead3 | |||
| ab9cc52bcc | |||
| 7dc342a6f9 | |||
| 80a3ef9193 | |||
| bb723d20cb | |||
| 0c24a2016c | |||
| 2124965996 | |||
| 6f1e104067 | |||
| b20f40b5a3 | |||
| 1093ca5fc4 | |||
| 0d99c55d3f | |||
| 86dc1a88ed | |||
| 968c20d8ca | |||
| 1b2d978449 | |||
| 30582b2d75 | |||
| 38a8037a4d | |||
| 67de45f787 | |||
| f358f24abb | |||
| fd4e15003a | |||
| 81df249d5c | |||
| ba60997992 | |||
| 8961294a29 | |||
| bc2e0a000a | |||
| 1a693fba2d | |||
| 05b7acf6aa | |||
| de13d8d397 | |||
| 6d7d8ae87c | |||
| cfb1bdcd52 | |||
| 48d41833f1 | |||
| 01535f1afc | |||
| fe845bfa36 | |||
| b94dbc75b7 | |||
| bdf40640d0 | |||
| 496f6c706b | |||
| 79f97d5555 | |||
| 82f679c84c | |||
| 9b789889fa | |||
| dd8dd118da | |||
| 8222a4b6a4 | |||
| 31f8057bfa | |||
| 1d8e0417bd | |||
| 8100ca46c2 | |||
| 94d745cf1c | |||
| a92bd0ad0f | |||
| 94a13ffca2 | |||
| 64c8707957 | |||
| d421a73988 | |||
| f7eef1f8d6 | |||
| 08c780d309 | |||
| baaf1f510f | |||
| 8898dcc054 | |||
| 33521c8aeb | |||
| 9db03bd7b9 | |||
| bd600c1fba | |||
| 82b18b08f1 | |||
| 0aefe616b3 | |||
| 59a66bbd16 | |||
| 155c708e4f | |||
| 5502c2ab1b | |||
| e89b42de40 | |||
| 2562c4f432 | |||
| 17323074ca | |||
| 8a4c5c5e09 | |||
| 3033faff0d | |||
| 0e6aab0e8d | |||
| 75e3dd839a | |||
| 830e6eb88d | |||
| 3c8fae78d1 | |||
| 441c1d7044 | |||
| 203f4633f7 | |||
| e558f4d9fb | |||
| 990e4020e9 | |||
| 1ad3e45519 | |||
| 442ced8381 | |||
| ba46202a4d | |||
| 1c77a1ee90 | |||
| ea74b59535 | |||
| 0e9343d9e2 | |||
| af4cf7257f | |||
| c8d4ee58f8 | |||
| 65e57c22e5 | |||
| 7351ae0a77 | |||
| f1e0d5d137 | |||
| 8a1b71688c | |||
| 8ef1c309e0 | |||
| a32dcc0dc5 | |||
| 16a2fde4b7 | |||
| 7856f7a134 | |||
| c545924fe9 | |||
| b1510bb88c | |||
| 31d2024ce5 | |||
| 23c49f8706 | |||
| 62eb0cda6b | |||
| 9f48c6e625 | |||
| 83ce401cf2 | |||
| 541b025ad0 | |||
| 5fd018df46 | |||
| 2822b0086a | |||
| e27606f443 | |||
| 6c95cd5945 | |||
| d723146060 | |||
| 3b03e61ae3 | |||
| 172513b96c | |||
| 68be42a48d | |||
| 0370e363bc | |||
| ced88476cc | |||
| c5d71505ef | |||
| 489f9859be | |||
| a87ca65839 | |||
| fa78d0976d | |||
| 4aaab2563b | |||
| e073cf2c27 | |||
| 173d406b1f | |||
| 3bcbad6f04 | |||
| e67854fe56 | |||
| 7510e0cb6b | |||
| 30c913b54f | |||
| 2cda9cd736 | |||
| 342d29f272 | |||
| 13f76c9944 | |||
| ce4e55f387 | |||
| 712107ba5a | |||
| 1aa867893e | |||
| 4f6142f9c0 | |||
| c303349666 | |||
| e85df170f2 | |||
| 01333e5b2e | |||
| 514987138a | |||
| 0814c6d065 | |||
| 542a443fb8 | |||
| f2c6bb1c3d | |||
| b85c9cfdb7 | |||
| e07b50c22d | |||
| 4dde9890e0 | |||
| 66d1ae2f64 | |||
| d130835902 | |||
| 52f66368a3 | |||
| 89d0810625 | |||
| 06d3d1202b | |||
| 0698aad8e1 | |||
| 3575a25935 | |||
| cdd034e72d | |||
| 26f7d5cf0d | |||
| 3c69430b94 | |||
| c0162bccc8 | |||
| 1adb800f94 | |||
| 333a5d6b7b | |||
| 9aa68fe955 | |||
| 46af87d89a | |||
| 1066131143 | |||
| a25022532e | |||
| 5162f9a9dc | |||
| cc5694de43 | |||
| f734ba280d | |||
| 500d0da563 | |||
| 6626a2b602 | |||
| 07e66a6e53 | |||
| deef9c7d3b | |||
| 2ec8954dc8 | |||
| c1af803e5b | |||
| 10abdbeba4 | |||
| 1dbde7a6d8 | |||
| a27eb18670 | |||
| 44a49a4533 | |||
| fc11078cae | |||
| a42ae0b39e | |||
| 3163cdc761 | |||
| 5439c63ce7 | |||
| b78287242b | |||
| b869ae38b3 | |||
| 4a320e9ba6 | |||
| 0025928a35 | |||
| c227a1988a | |||
| 3ade45f541 | |||
| 17da8bde80 | |||
| 59d9a59769 | |||
| 7210badde9 | |||
| 21659a73f3 | |||
| c0e49ebeea | |||
| 2d24eac4a3 | |||
| 14c3c8181b | |||
| 036f6049a2 | |||
| 236e4cb35f | |||
| ee77cb0982 | |||
| d474ad05e5 | |||
| dbf1fa37c5 | |||
| 7444bb8b68 | |||
| f7132445a1 | |||
| 0127dd738e |
@@ -0,0 +1,58 @@
|
||||
---
|
||||
:position: before
|
||||
:position_in_additional_file_patterns: before
|
||||
:position_in_class: before
|
||||
:position_in_factory: before
|
||||
:position_in_fixture: before
|
||||
:position_in_routes: before
|
||||
:position_in_serializer: before
|
||||
:position_in_test: before
|
||||
:classified_sort: true
|
||||
:exclude_controllers: true
|
||||
:exclude_factories: true
|
||||
:exclude_fixtures: false
|
||||
:exclude_helpers: true
|
||||
:exclude_scaffolds: true
|
||||
:exclude_serializers: false
|
||||
:exclude_sti_subclasses: false
|
||||
:exclude_tests: false
|
||||
:force: false
|
||||
:format_markdown: false
|
||||
:format_rdoc: false
|
||||
:format_yard: false
|
||||
:frozen: false
|
||||
:ignore_model_sub_dir: false
|
||||
:ignore_unknown_models: false
|
||||
:include_version: false
|
||||
:show_check_constraints: false
|
||||
:show_complete_foreign_keys: false
|
||||
:show_foreign_keys: true
|
||||
:show_indexes: true
|
||||
:simple_indexes: false
|
||||
:sort: false
|
||||
:timestamp: false
|
||||
:trace: false
|
||||
:with_comment: true
|
||||
:with_column_comments: true
|
||||
:with_table_comments: true
|
||||
:active_admin: false
|
||||
:command:
|
||||
:debug: false
|
||||
:hide_default_column_types: ''
|
||||
:hide_limit_column_types: ''
|
||||
:ignore_columns:
|
||||
:ignore_routes:
|
||||
:models: true
|
||||
:routes: false
|
||||
:skip_on_db_migrate: false
|
||||
:target_action: :do_annotations
|
||||
:wrapper:
|
||||
:wrapper_close:
|
||||
:wrapper_open:
|
||||
:classes_default_to_s: []
|
||||
:additional_file_patterns: []
|
||||
:model_dir:
|
||||
- app/models
|
||||
:require: []
|
||||
:root_dir:
|
||||
- ''
|
||||
@@ -8,7 +8,8 @@
|
||||
},
|
||||
"rules": {
|
||||
"vue/no-deprecated-html-element-is": 0,
|
||||
"vue/no-mutating-props": 0
|
||||
"vue/no-mutating-props": 0,
|
||||
"vue/one-component-per-file": 0
|
||||
},
|
||||
"parserOptions": {
|
||||
"ecmaVersion": 2022,
|
||||
|
||||
+53
-16
@@ -6,14 +6,15 @@ jobs:
|
||||
rubocop:
|
||||
name: Rubocop
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 10
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/checkout@v4
|
||||
- name: Install Ruby
|
||||
uses: ruby/setup-ruby@v1
|
||||
with:
|
||||
ruby-version: 3.2.2
|
||||
ruby-version: 3.4.2
|
||||
- name: Cache gems
|
||||
uses: actions/cache@v1
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: vendor/bundle
|
||||
key: ${{ runner.os }}-gem-${{ hashFiles('**/Gemfile.lock') }}
|
||||
@@ -30,14 +31,15 @@ jobs:
|
||||
erblint:
|
||||
name: Erblint
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 10
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/checkout@v4
|
||||
- name: Install Ruby
|
||||
uses: ruby/setup-ruby@v1
|
||||
with:
|
||||
ruby-version: 3.2.2
|
||||
ruby-version: 3.4.2
|
||||
- name: Cache gems
|
||||
uses: actions/cache@v1
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: vendor/bundle
|
||||
key: ${{ runner.os }}-gem-${{ hashFiles('**/Gemfile.lock') }}
|
||||
@@ -49,21 +51,22 @@ jobs:
|
||||
bundle config path vendor/bundle
|
||||
bundle install --jobs 4 --retry 4
|
||||
- name: Run Erblint
|
||||
run: bundle exec erblint ./app
|
||||
run: bundle exec erb_lint ./app
|
||||
|
||||
eslint:
|
||||
name: ESLint
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 10
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/checkout@v4
|
||||
- name: Install Node.js
|
||||
uses: actions/setup-node@v1
|
||||
with:
|
||||
node-version: 16.13.1
|
||||
node-version: 20.9.0
|
||||
- name: Cache directory path
|
||||
id: yarn-cache-dir-path
|
||||
run: echo "::set-output name=dir::$(yarn cache dir)"
|
||||
- uses: actions/cache@v1
|
||||
- uses: actions/cache@v4
|
||||
id: yarn-cache
|
||||
with:
|
||||
path: ${{ steps.yarn-cache-dir-path.outputs.dir }}
|
||||
@@ -77,9 +80,38 @@ jobs:
|
||||
run: |
|
||||
./node_modules/eslint/bin/eslint.js "app/javascript/**/*.js"
|
||||
|
||||
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
|
||||
- name: Cache gems
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: vendor/bundle
|
||||
key: ${{ runner.os }}-gem-${{ hashFiles('**/Gemfile.lock') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-gem-
|
||||
- name: Install gems
|
||||
run: |
|
||||
gem install bundler
|
||||
bundle config path vendor/bundle
|
||||
bundle install --jobs 4 --retry 4
|
||||
yarn install
|
||||
sudo apt-get update
|
||||
sudo apt-get install libvips
|
||||
- name: Run Brakeman
|
||||
run: bundle exec brakeman -q --exit-on-warn
|
||||
|
||||
rspec:
|
||||
name: RSpec
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 10
|
||||
|
||||
services:
|
||||
postgres:
|
||||
@@ -96,24 +128,26 @@ jobs:
|
||||
--health-retries 5
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/checkout@v4
|
||||
- name: Install Ruby
|
||||
uses: ruby/setup-ruby@v1
|
||||
with:
|
||||
ruby-version: 3.2.2
|
||||
ruby-version: 3.4.2
|
||||
- name: Set up Node
|
||||
uses: actions/setup-node@v1
|
||||
with:
|
||||
node-version: 16.13.1
|
||||
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@v1
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: node_modules
|
||||
key: ${{ runner.os }}-node-${{ hashFiles('**/yarn.lock') }}
|
||||
- name: Cache gems
|
||||
uses: actions/cache@v1
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: vendor/bundle
|
||||
key: ${{ runner.os }}-gem-${{ hashFiles('**/Gemfile.lock') }}
|
||||
@@ -128,7 +162,10 @@ jobs:
|
||||
bundle install --jobs 4 --retry 4
|
||||
yarn install
|
||||
sudo apt-get update
|
||||
sudo apt-get install libvips
|
||||
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
|
||||
- name: Run
|
||||
env:
|
||||
RAILS_ENV: test
|
||||
|
||||
@@ -7,7 +7,8 @@ on:
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ubuntu-24.04-arm
|
||||
timeout-minutes: 30
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
@@ -15,29 +16,30 @@ 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@v2
|
||||
uses: docker/setup-qemu-action@v3
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v2
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Create .version file
|
||||
run: echo ${{ github.ref_name }} > .version
|
||||
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v2
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
- name: Build and push Docker image
|
||||
uses: docker/build-push-action@v4
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: .
|
||||
push: true
|
||||
|
||||
@@ -36,3 +36,5 @@ yarn-debug.log*
|
||||
/attachments
|
||||
/docuseal
|
||||
/ee
|
||||
dump.rdb
|
||||
*.onnx
|
||||
|
||||
+31
-9
@@ -10,7 +10,8 @@ AllCops:
|
||||
- node_modules/**/*
|
||||
- bin/*
|
||||
- vendor/**/*
|
||||
TargetRubyVersion: '3.2'
|
||||
TargetRubyVersion: '3.3'
|
||||
SuggestExtensions: false
|
||||
|
||||
Metrics/BlockLength:
|
||||
Exclude:
|
||||
@@ -30,9 +31,10 @@ Metrics/ParameterLists:
|
||||
Max: 10
|
||||
|
||||
Metrics/MethodLength:
|
||||
Max: 25
|
||||
Max: 30
|
||||
Exclude:
|
||||
- 'db/migrate/**'
|
||||
- 'spec/**/*'
|
||||
|
||||
Metrics/CyclomaticComplexity:
|
||||
Max: 15
|
||||
@@ -40,29 +42,49 @@ Metrics/CyclomaticComplexity:
|
||||
Metrics/PerceivedComplexity:
|
||||
Max: 15
|
||||
|
||||
Style/MultipleComparison:
|
||||
Enabled: false
|
||||
|
||||
Style/NumericPredicate:
|
||||
Enabled: false
|
||||
|
||||
Style/MinMaxComparison:
|
||||
Enabled: false
|
||||
|
||||
Naming/PredicateMethod:
|
||||
Enabled: false
|
||||
|
||||
Layout/LineLength:
|
||||
AllowedPatterns: ['\A\s*#']
|
||||
|
||||
Metrics/AbcSize:
|
||||
Max: 35
|
||||
Max: 45
|
||||
Exclude:
|
||||
- spec/**/*
|
||||
|
||||
Metrics/ModuleLength:
|
||||
Max: 500
|
||||
Max: 1000
|
||||
|
||||
Metrics/ClassLength:
|
||||
Max: 500
|
||||
Max: 1000
|
||||
|
||||
RSpec/NestedGroups:
|
||||
Max: 6
|
||||
|
||||
RSpec/MultipleExpectations:
|
||||
Max: 20
|
||||
Max: 25
|
||||
|
||||
RSpec/ExampleLength:
|
||||
Max: 40
|
||||
Max: 500
|
||||
|
||||
RSpec/MultipleMemoizedHelpers:
|
||||
Max: 6
|
||||
Max: 15
|
||||
|
||||
RSpec/AnyInstance:
|
||||
Enabled: false
|
||||
|
||||
Metrics/BlockNesting:
|
||||
Max: 5
|
||||
|
||||
Rails/I18nLocaleTexts:
|
||||
Enabled: false
|
||||
@@ -76,5 +98,5 @@ Rails/SkipsModelValidations:
|
||||
Rails/ApplicationController:
|
||||
Enabled: false
|
||||
|
||||
Capybara/ClickLinkOrButtonStyle:
|
||||
Rails/Output:
|
||||
Enabled: false
|
||||
|
||||
+66
-22
@@ -1,4 +1,22 @@
|
||||
FROM ruby:3.2.2-alpine as webpack
|
||||
FROM ruby:3.4.2-alpine AS download
|
||||
|
||||
WORKDIR /fonts
|
||||
|
||||
RUN apk --no-cache add fontforge wget && \
|
||||
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 /model.onnx "https://github.com/docusealco/fields-detection/releases/download/2.1.0/model_704_int8.onnx" && \
|
||||
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" && \
|
||||
mkdir -p /pdfium-linux && \
|
||||
tar -xzf pdfium-linux.tgz -C /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
|
||||
|
||||
ENV RAILS_ENV=production
|
||||
ENV NODE_ENV=production
|
||||
@@ -15,45 +33,71 @@ RUN yarn install --network-timeout 1000000
|
||||
COPY ./bin/shakapacker ./bin/shakapacker
|
||||
COPY ./config/webpack ./config/webpack
|
||||
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.application.config.js ./tailwind.application.config.js ./
|
||||
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.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.2.2-alpine as app
|
||||
FROM ruby:3.4.2-alpine AS app
|
||||
|
||||
ENV RAILS_ENV=production
|
||||
ENV BUNDLE_WITHOUT="development:test"
|
||||
ENV LD_PRELOAD=/lib/libgcompat.so.0
|
||||
ENV OPENSSL_CONF=/etc/openssl_legacy.cnf
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
RUN apk add --no-cache build-base sqlite-dev libpq-dev mariadb-dev vips-dev vips-poppler poppler-utils vips-heif libc6-compat ttf-freefont ttf-liberation && mkdir /fonts && cp /usr/share/fonts/liberation/LiberationSans-Regular.ttf /usr/share/fonts/liberation/LiberationSans-Bold.ttf /fonts && apk del ttf-liberation && wget -O /fonts/DancingScript.otf "https://github.com/impallari/DancingScript/raw/master/fonts/DancingScript-Regular.otf" && wget -O /fonts/DancingScript-License.txt https://github.com/impallari/DancingScript/blob/master/OFL.txt
|
||||
RUN apk add --no-cache sqlite-dev libpq-dev mariadb-dev vips-dev yaml-dev redis libheif vips-heif gcompat ttf-freefont && mkdir /fonts && rm /usr/share/fonts/freefont/FreeSans.otf
|
||||
|
||||
COPY ./Gemfile ./Gemfile.lock ./
|
||||
RUN addgroup -g 2000 docuseal && adduser -u 2000 -G docuseal -s /bin/sh -D -h /home/docuseal docuseal
|
||||
|
||||
RUN bundle update --bundler && bundle install && rm -rf ~/.bundle
|
||||
RUN echo $'.include = /etc/ssl/openssl.cnf\n\
|
||||
\n\
|
||||
[provider_sect]\n\
|
||||
default = default_sect\n\
|
||||
legacy = legacy_sect\n\
|
||||
\n\
|
||||
[default_sect]\n\
|
||||
activate = 1\n\
|
||||
\n\
|
||||
[legacy_sect]\n\
|
||||
activate = 1' >> /etc/openssl_legacy.cnf
|
||||
|
||||
COPY ./bin ./bin
|
||||
COPY ./app ./app
|
||||
COPY ./config ./config
|
||||
COPY ./db ./db
|
||||
COPY ./log ./log
|
||||
COPY ./lib ./lib
|
||||
COPY ./public ./public
|
||||
COPY ./tmp ./tmp
|
||||
COPY LICENSE README.md Rakefile config.ru ./
|
||||
COPY --chown=docuseal:docuseal ./Gemfile ./Gemfile.lock ./
|
||||
|
||||
COPY --from=webpack /app/public/packs ./public/packs
|
||||
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 && ln -sf /usr/lib/libonnxruntime.so.1 $(ruby -e "print Dir[Gem::Specification.find_by_name('onnxruntime').gem_dir + '/vendor/*.so'].first")
|
||||
|
||||
RUN ln -s /fonts /app/public/fonts
|
||||
RUN bundle exec bootsnap precompile --gemfile app/ lib/
|
||||
RUN echo 'https://dl-cdn.alpinelinux.org/alpine/edge/main' >> /etc/apk/repositories && echo 'https://dl-cdn.alpinelinux.org/alpine/edge/community' >> /etc/apk/repositories && apk add --no-cache onnxruntime
|
||||
|
||||
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 README.md Rakefile config.ru .version ./
|
||||
COPY --chown=docuseal:docuseal .version ./public/version
|
||||
|
||||
COPY --chown=docuseal:docuseal --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 --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 --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 && \
|
||||
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
|
||||
|
||||
EXPOSE 3000
|
||||
CMD ["/app/bin/rails", "server"]
|
||||
CMD ["/app/bin/bundle", "exec", "puma", "-C", "/app/config/puma.rb", "--dir", "/app"]
|
||||
|
||||
@@ -2,12 +2,16 @@
|
||||
|
||||
source 'https://rubygems.org'
|
||||
|
||||
ruby '3.2.2'
|
||||
ruby '3.4.2'
|
||||
|
||||
gem 'arabic-letter-connector', require: 'arabic-letter-connector/logic'
|
||||
gem 'aws-sdk-s3', require: false
|
||||
gem 'aws-sdk-secretsmanager', require: false
|
||||
gem 'azure-storage-blob', require: false
|
||||
gem 'bootsnap', require: false
|
||||
gem 'cancancan'
|
||||
gem 'csv'
|
||||
gem 'csv-safe'
|
||||
gem 'devise'
|
||||
gem 'devise-two-factor'
|
||||
gem 'dotenv', require: false
|
||||
@@ -20,31 +24,32 @@ gem 'image_processing'
|
||||
gem 'jwt'
|
||||
gem 'lograge'
|
||||
gem 'mysql2', require: false
|
||||
gem 'numo-narray'
|
||||
gem 'oj'
|
||||
gem 'omniauth-google-oauth2'
|
||||
gem 'omniauth-rails_csrf_protection'
|
||||
gem 'onnxruntime'
|
||||
gem 'pagy'
|
||||
gem 'pdf-reader'
|
||||
gem 'pg', require: false
|
||||
gem 'premailer-rails'
|
||||
gem 'puma'
|
||||
gem 'pretender'
|
||||
gem 'puma', require: false
|
||||
gem 'rack'
|
||||
gem 'rails'
|
||||
gem 'rails_autolink'
|
||||
gem 'rails-i18n'
|
||||
gem 'rollbar', require: ENV.key?('ROLLBAR_ACCESS_TOKEN')
|
||||
gem 'rotp'
|
||||
gem 'rouge', require: false
|
||||
gem 'rqrcode'
|
||||
gem 'ruby-vips'
|
||||
gem 'rubyXL'
|
||||
gem 'shakapacker'
|
||||
gem 'sidekiq', require: ENV.key?('REDIS_URL')
|
||||
gem 'sidekiq'
|
||||
gem 'sqlite3', require: false
|
||||
gem 'strip_attributes'
|
||||
gem 'turbo-rails'
|
||||
gem 'twitter_cldr', require: false
|
||||
gem 'tzinfo-data'
|
||||
|
||||
group :development, :test do
|
||||
gem 'annotate'
|
||||
gem 'better_html'
|
||||
gem 'bullet'
|
||||
gem 'debug'
|
||||
@@ -61,6 +66,9 @@ group :development, :test do
|
||||
end
|
||||
|
||||
group :development do
|
||||
gem 'annotaterb'
|
||||
gem 'brakeman', require: false
|
||||
gem 'foreman', require: false
|
||||
gem 'letter_opener_web'
|
||||
gem 'web-console'
|
||||
end
|
||||
|
||||
+395
-331
@@ -1,94 +1,103 @@
|
||||
GEM
|
||||
remote: https://rubygems.org/
|
||||
specs:
|
||||
Ascii85 (1.1.0)
|
||||
actioncable (7.0.7)
|
||||
actionpack (= 7.0.7)
|
||||
activesupport (= 7.0.7)
|
||||
actioncable (8.0.2.1)
|
||||
actionpack (= 8.0.2.1)
|
||||
activesupport (= 8.0.2.1)
|
||||
nio4r (~> 2.0)
|
||||
websocket-driver (>= 0.6.1)
|
||||
actionmailbox (7.0.7)
|
||||
actionpack (= 7.0.7)
|
||||
activejob (= 7.0.7)
|
||||
activerecord (= 7.0.7)
|
||||
activestorage (= 7.0.7)
|
||||
activesupport (= 7.0.7)
|
||||
mail (>= 2.7.1)
|
||||
net-imap
|
||||
net-pop
|
||||
net-smtp
|
||||
actionmailer (7.0.7)
|
||||
actionpack (= 7.0.7)
|
||||
actionview (= 7.0.7)
|
||||
activejob (= 7.0.7)
|
||||
activesupport (= 7.0.7)
|
||||
mail (~> 2.5, >= 2.5.4)
|
||||
net-imap
|
||||
net-pop
|
||||
net-smtp
|
||||
rails-dom-testing (~> 2.0)
|
||||
actionpack (7.0.7)
|
||||
actionview (= 7.0.7)
|
||||
activesupport (= 7.0.7)
|
||||
rack (~> 2.0, >= 2.2.4)
|
||||
zeitwerk (~> 2.6)
|
||||
actionmailbox (8.0.2.1)
|
||||
actionpack (= 8.0.2.1)
|
||||
activejob (= 8.0.2.1)
|
||||
activerecord (= 8.0.2.1)
|
||||
activestorage (= 8.0.2.1)
|
||||
activesupport (= 8.0.2.1)
|
||||
mail (>= 2.8.0)
|
||||
actionmailer (8.0.2.1)
|
||||
actionpack (= 8.0.2.1)
|
||||
actionview (= 8.0.2.1)
|
||||
activejob (= 8.0.2.1)
|
||||
activesupport (= 8.0.2.1)
|
||||
mail (>= 2.8.0)
|
||||
rails-dom-testing (~> 2.2)
|
||||
actionpack (8.0.2.1)
|
||||
actionview (= 8.0.2.1)
|
||||
activesupport (= 8.0.2.1)
|
||||
nokogiri (>= 1.8.5)
|
||||
rack (>= 2.2.4)
|
||||
rack-session (>= 1.0.1)
|
||||
rack-test (>= 0.6.3)
|
||||
rails-dom-testing (~> 2.0)
|
||||
rails-html-sanitizer (~> 1.0, >= 1.2.0)
|
||||
actiontext (7.0.7)
|
||||
actionpack (= 7.0.7)
|
||||
activerecord (= 7.0.7)
|
||||
activestorage (= 7.0.7)
|
||||
activesupport (= 7.0.7)
|
||||
rails-dom-testing (~> 2.2)
|
||||
rails-html-sanitizer (~> 1.6)
|
||||
useragent (~> 0.16)
|
||||
actiontext (8.0.2.1)
|
||||
actionpack (= 8.0.2.1)
|
||||
activerecord (= 8.0.2.1)
|
||||
activestorage (= 8.0.2.1)
|
||||
activesupport (= 8.0.2.1)
|
||||
globalid (>= 0.6.0)
|
||||
nokogiri (>= 1.8.5)
|
||||
actionview (7.0.7)
|
||||
activesupport (= 7.0.7)
|
||||
actionview (8.0.2.1)
|
||||
activesupport (= 8.0.2.1)
|
||||
builder (~> 3.1)
|
||||
erubi (~> 1.4)
|
||||
rails-dom-testing (~> 2.0)
|
||||
rails-html-sanitizer (~> 1.1, >= 1.2.0)
|
||||
activejob (7.0.7)
|
||||
activesupport (= 7.0.7)
|
||||
erubi (~> 1.11)
|
||||
rails-dom-testing (~> 2.2)
|
||||
rails-html-sanitizer (~> 1.6)
|
||||
activejob (8.0.2.1)
|
||||
activesupport (= 8.0.2.1)
|
||||
globalid (>= 0.3.6)
|
||||
activemodel (7.0.7)
|
||||
activesupport (= 7.0.7)
|
||||
activerecord (7.0.7)
|
||||
activemodel (= 7.0.7)
|
||||
activesupport (= 7.0.7)
|
||||
activestorage (7.0.7)
|
||||
actionpack (= 7.0.7)
|
||||
activejob (= 7.0.7)
|
||||
activerecord (= 7.0.7)
|
||||
activesupport (= 7.0.7)
|
||||
activemodel (8.0.2.1)
|
||||
activesupport (= 8.0.2.1)
|
||||
activerecord (8.0.2.1)
|
||||
activemodel (= 8.0.2.1)
|
||||
activesupport (= 8.0.2.1)
|
||||
timeout (>= 0.4.0)
|
||||
activestorage (8.0.2.1)
|
||||
actionpack (= 8.0.2.1)
|
||||
activejob (= 8.0.2.1)
|
||||
activerecord (= 8.0.2.1)
|
||||
activesupport (= 8.0.2.1)
|
||||
marcel (~> 1.0)
|
||||
mini_mime (>= 1.1.0)
|
||||
activesupport (7.0.7)
|
||||
concurrent-ruby (~> 1.0, >= 1.0.2)
|
||||
activesupport (8.0.2.1)
|
||||
base64
|
||||
benchmark (>= 0.3)
|
||||
bigdecimal
|
||||
concurrent-ruby (~> 1.0, >= 1.3.1)
|
||||
connection_pool (>= 2.2.5)
|
||||
drb
|
||||
i18n (>= 1.6, < 2)
|
||||
logger (>= 1.4.2)
|
||||
minitest (>= 5.1)
|
||||
tzinfo (~> 2.0)
|
||||
addressable (2.8.5)
|
||||
public_suffix (>= 2.0.2, < 6.0)
|
||||
afm (0.2.2)
|
||||
annotate (3.2.0)
|
||||
activerecord (>= 3.2, < 8.0)
|
||||
rake (>= 10.4, < 14.0)
|
||||
ast (2.4.2)
|
||||
aws-eventstream (1.2.0)
|
||||
aws-partitions (1.807.0)
|
||||
aws-sdk-core (3.180.3)
|
||||
aws-eventstream (~> 1, >= 1.0.2)
|
||||
aws-partitions (~> 1, >= 1.651.0)
|
||||
aws-sigv4 (~> 1.5)
|
||||
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)
|
||||
arabic-letter-connector (0.1.1)
|
||||
ast (2.4.3)
|
||||
aws-eventstream (1.4.0)
|
||||
aws-partitions (1.1197.0)
|
||||
aws-sdk-core (3.240.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.71.0)
|
||||
aws-sdk-core (~> 3, >= 3.177.0)
|
||||
aws-sigv4 (~> 1.1)
|
||||
aws-sdk-s3 (1.132.1)
|
||||
aws-sdk-core (~> 3, >= 3.179.0)
|
||||
logger
|
||||
aws-sdk-kms (1.118.0)
|
||||
aws-sdk-core (~> 3, >= 3.239.1)
|
||||
aws-sigv4 (~> 1.5)
|
||||
aws-sdk-s3 (1.208.0)
|
||||
aws-sdk-core (~> 3, >= 3.234.0)
|
||||
aws-sdk-kms (~> 1)
|
||||
aws-sigv4 (~> 1.6)
|
||||
aws-sigv4 (1.6.0)
|
||||
aws-sigv4 (~> 1.5)
|
||||
aws-sdk-secretsmanager (1.110.0)
|
||||
aws-sdk-core (~> 3, >= 3.210.0)
|
||||
aws-sigv4 (~> 1.5)
|
||||
aws-sigv4 (1.12.1)
|
||||
aws-eventstream (~> 1, >= 1.0.2)
|
||||
azure-storage-blob (2.0.3)
|
||||
azure-storage-common (~> 2.0)
|
||||
@@ -98,82 +107,97 @@ GEM
|
||||
faraday_middleware (~> 1.0, >= 1.0.0.rc1)
|
||||
net-http-persistent (~> 4.0)
|
||||
nokogiri (~> 1, >= 1.10.8)
|
||||
bcrypt (3.1.19)
|
||||
better_html (2.0.2)
|
||||
base64 (0.3.0)
|
||||
bcrypt (3.1.20)
|
||||
benchmark (0.4.1)
|
||||
better_html (2.1.1)
|
||||
actionview (>= 6.0)
|
||||
activesupport (>= 6.0)
|
||||
ast (~> 2.0)
|
||||
erubi (~> 1.4)
|
||||
parser (>= 2.4)
|
||||
smart_properties
|
||||
bigdecimal (3.2.2)
|
||||
bindex (0.8.1)
|
||||
bootsnap (1.16.0)
|
||||
bootsnap (1.18.4)
|
||||
msgpack (~> 1.2)
|
||||
builder (3.2.4)
|
||||
bullet (7.0.7)
|
||||
brakeman (7.0.0)
|
||||
racc
|
||||
builder (3.3.0)
|
||||
bullet (8.0.0)
|
||||
activesupport (>= 3.0.0)
|
||||
uniform_notifier (~> 1.11)
|
||||
cancancan (3.5.0)
|
||||
capybara (3.39.2)
|
||||
camertron-eprun (1.1.1)
|
||||
cancancan (3.6.1)
|
||||
capybara (3.40.0)
|
||||
addressable
|
||||
matrix
|
||||
mini_mime (>= 0.1.3)
|
||||
nokogiri (~> 1.8)
|
||||
nokogiri (~> 1.11)
|
||||
rack (>= 1.6.0)
|
||||
rack-test (>= 0.6.3)
|
||||
regexp_parser (>= 1.5, < 3.0)
|
||||
xpath (~> 3.2)
|
||||
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.2.2)
|
||||
connection_pool (2.4.1)
|
||||
crack (0.4.5)
|
||||
concurrent-ruby (1.3.5)
|
||||
connection_pool (2.5.3)
|
||||
crack (1.0.0)
|
||||
bigdecimal
|
||||
rexml
|
||||
crass (1.0.6)
|
||||
css_parser (1.14.0)
|
||||
css_parser (1.21.0)
|
||||
addressable
|
||||
cuprite (0.14.3)
|
||||
csv (3.3.2)
|
||||
csv-safe (3.3.1)
|
||||
csv (~> 3.0)
|
||||
cuprite (0.15.1)
|
||||
capybara (~> 3.0)
|
||||
ferrum (~> 0.13.0)
|
||||
date (3.3.3)
|
||||
debug (1.8.0)
|
||||
irb (>= 1.5.0)
|
||||
reline (>= 0.3.1)
|
||||
ferrum (~> 0.15.0)
|
||||
date (3.4.1)
|
||||
debug (1.10.0)
|
||||
irb (~> 1.10)
|
||||
reline (>= 0.3.8)
|
||||
declarative (0.0.20)
|
||||
devise (4.9.2)
|
||||
devise (4.9.4)
|
||||
bcrypt (~> 3.0)
|
||||
orm_adapter (~> 0.1)
|
||||
railties (>= 4.1.0)
|
||||
responders
|
||||
warden (~> 1.2.3)
|
||||
devise-two-factor (5.0.0)
|
||||
activesupport (~> 7.0)
|
||||
devise-two-factor (6.1.0)
|
||||
activesupport (>= 7.0, < 8.1)
|
||||
devise (~> 4.0)
|
||||
railties (~> 7.0)
|
||||
railties (>= 7.0, < 8.1)
|
||||
rotp (~> 6.0)
|
||||
diff-lcs (1.5.0)
|
||||
diff-lcs (1.5.1)
|
||||
digest-crc (0.6.5)
|
||||
rake (>= 12.0.0, < 14.0.0)
|
||||
docile (1.4.0)
|
||||
dotenv (2.8.1)
|
||||
docile (1.4.1)
|
||||
dotenv (3.1.7)
|
||||
drb (2.2.3)
|
||||
email_typo (0.2.3)
|
||||
erb_lint (0.4.0)
|
||||
erb (5.0.2)
|
||||
erb_lint (0.7.0)
|
||||
activesupport
|
||||
better_html (>= 2.0.1)
|
||||
parser (>= 2.7.1.4)
|
||||
rainbow
|
||||
rubocop
|
||||
rubocop (>= 1)
|
||||
smart_properties
|
||||
erubi (1.12.0)
|
||||
factory_bot (6.2.1)
|
||||
erubi (1.13.1)
|
||||
factory_bot (6.5.0)
|
||||
activesupport (>= 5.0.0)
|
||||
factory_bot_rails (6.2.0)
|
||||
factory_bot (~> 6.2.0)
|
||||
factory_bot_rails (6.4.4)
|
||||
factory_bot (~> 6.5)
|
||||
railties (>= 5.0.0)
|
||||
faker (3.2.1)
|
||||
faker (3.5.1)
|
||||
i18n (>= 1.8.11, < 2)
|
||||
faraday (1.10.3)
|
||||
faraday (1.10.4)
|
||||
faraday-em_http (~> 1.0)
|
||||
faraday-em_synchrony (~> 1.0)
|
||||
faraday-excon (~> 1.1)
|
||||
@@ -191,94 +215,107 @@ GEM
|
||||
faraday-follow_redirects (0.3.0)
|
||||
faraday (>= 1, < 3)
|
||||
faraday-httpclient (1.0.1)
|
||||
faraday-multipart (1.0.4)
|
||||
multipart-post (~> 2)
|
||||
faraday-net_http (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.0)
|
||||
faraday_middleware (1.2.1)
|
||||
faraday (~> 1.0)
|
||||
ferrum (0.13)
|
||||
ferrum (0.15)
|
||||
addressable (~> 2.5)
|
||||
concurrent-ruby (~> 1.1)
|
||||
webrick (~> 1.7)
|
||||
websocket-driver (>= 0.6, < 0.8)
|
||||
ffi (1.15.5)
|
||||
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)
|
||||
geom2d (0.4.1)
|
||||
globalid (1.1.0)
|
||||
activesupport (>= 5.0)
|
||||
google-apis-core (0.11.1)
|
||||
globalid (1.2.1)
|
||||
activesupport (>= 6.1)
|
||||
google-apis-core (0.15.1)
|
||||
addressable (~> 2.5, >= 2.5.1)
|
||||
googleauth (>= 0.16.2, < 2.a)
|
||||
httpclient (>= 2.8.1, < 3.a)
|
||||
googleauth (~> 1.9)
|
||||
httpclient (>= 2.8.3, < 3.a)
|
||||
mini_mime (~> 1.0)
|
||||
mutex_m
|
||||
representable (~> 3.0)
|
||||
retriable (>= 2.0, < 4.a)
|
||||
rexml
|
||||
webrick
|
||||
google-apis-iamcredentials_v1 (0.17.0)
|
||||
google-apis-core (>= 0.11.0, < 2.a)
|
||||
google-apis-storage_v1 (0.19.0)
|
||||
google-apis-core (>= 0.9.0, < 2.a)
|
||||
google-cloud-core (1.6.0)
|
||||
google-cloud-env (~> 1.0)
|
||||
google-apis-iamcredentials_v1 (0.22.0)
|
||||
google-apis-core (>= 0.15.0, < 2.a)
|
||||
google-apis-storage_v1 (0.49.0)
|
||||
google-apis-core (>= 0.15.0, < 2.a)
|
||||
google-cloud-core (1.7.1)
|
||||
google-cloud-env (>= 1.0, < 3.a)
|
||||
google-cloud-errors (~> 1.0)
|
||||
google-cloud-env (1.6.0)
|
||||
faraday (>= 0.17.3, < 3.0)
|
||||
google-cloud-errors (1.3.1)
|
||||
google-cloud-storage (1.44.0)
|
||||
google-cloud-env (2.2.1)
|
||||
faraday (>= 1.0, < 3.a)
|
||||
google-cloud-errors (1.4.0)
|
||||
google-cloud-storage (1.54.0)
|
||||
addressable (~> 2.8)
|
||||
digest-crc (~> 0.4)
|
||||
google-apis-iamcredentials_v1 (~> 0.1)
|
||||
google-apis-storage_v1 (~> 0.19.0)
|
||||
google-apis-core (~> 0.13)
|
||||
google-apis-iamcredentials_v1 (~> 0.18)
|
||||
google-apis-storage_v1 (~> 0.38)
|
||||
google-cloud-core (~> 1.6)
|
||||
googleauth (>= 0.16.2, < 2.a)
|
||||
googleauth (~> 1.9)
|
||||
mini_mime (~> 1.0)
|
||||
googleauth (1.7.0)
|
||||
faraday (>= 0.17.3, < 3.a)
|
||||
google-logging-utils (0.1.0)
|
||||
googleauth (1.12.2)
|
||||
faraday (>= 1.0, < 3.a)
|
||||
google-cloud-env (~> 2.2)
|
||||
google-logging-utils (~> 0.1)
|
||||
jwt (>= 1.4, < 3.0)
|
||||
memoist (~> 0.16)
|
||||
multi_json (~> 1.11)
|
||||
os (>= 0.9, < 2.0)
|
||||
signet (>= 0.16, < 2.a)
|
||||
hashdiff (1.0.1)
|
||||
hashery (2.1.2)
|
||||
hashie (5.0.0)
|
||||
hexapdf (0.33.0)
|
||||
hashdiff (1.1.2)
|
||||
hexapdf (1.4.0)
|
||||
cmdparse (~> 3.0, >= 3.0.3)
|
||||
geom2d (~> 0.4, >= 0.4.1)
|
||||
openssl (>= 2.2.1)
|
||||
strscan (>= 3.1.2)
|
||||
htmlentities (4.3.4)
|
||||
httpclient (2.8.3)
|
||||
i18n (1.14.1)
|
||||
i18n (1.14.7)
|
||||
concurrent-ruby (~> 1.0)
|
||||
image_processing (1.12.2)
|
||||
image_processing (1.13.0)
|
||||
mini_magick (>= 4.9.5, < 5)
|
||||
ruby-vips (>= 2.0.17, < 3)
|
||||
io-console (0.6.0)
|
||||
irb (1.7.4)
|
||||
reline (>= 0.3.6)
|
||||
io-console (0.8.1)
|
||||
irb (1.15.2)
|
||||
pp (>= 0.6.0)
|
||||
rdoc (>= 4.0.0)
|
||||
reline (>= 0.4.2)
|
||||
jmespath (1.6.2)
|
||||
json (2.6.3)
|
||||
jwt (2.7.1)
|
||||
language_server-protocol (3.17.0.3)
|
||||
launchy (2.5.2)
|
||||
json (2.15.0)
|
||||
jwt (2.9.3)
|
||||
base64
|
||||
language_server-protocol (3.17.0.5)
|
||||
launchy (3.0.1)
|
||||
addressable (~> 2.8)
|
||||
letter_opener (1.8.1)
|
||||
launchy (>= 2.2, < 3)
|
||||
letter_opener_web (2.0.0)
|
||||
actionmailer (>= 5.2)
|
||||
letter_opener (~> 1.7)
|
||||
railties (>= 5.2)
|
||||
childprocess (~> 5.0)
|
||||
letter_opener (1.10.0)
|
||||
launchy (>= 2.2, < 4)
|
||||
letter_opener_web (3.0.0)
|
||||
actionmailer (>= 6.1)
|
||||
letter_opener (~> 1.9)
|
||||
railties (>= 6.1)
|
||||
rexml
|
||||
lograge (0.13.0)
|
||||
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.21.3)
|
||||
loofah (2.24.1)
|
||||
crass (~> 1.0.2)
|
||||
nokogiri (>= 1.12.0)
|
||||
mail (2.8.1)
|
||||
@@ -286,223 +323,229 @@ GEM
|
||||
net-imap
|
||||
net-pop
|
||||
net-smtp
|
||||
marcel (1.0.2)
|
||||
marcel (1.0.4)
|
||||
matrix (0.4.2)
|
||||
memoist (0.16.2)
|
||||
method_source (1.0.0)
|
||||
mini_magick (4.12.0)
|
||||
method_source (1.1.0)
|
||||
mini_magick (4.13.2)
|
||||
mini_mime (1.1.5)
|
||||
mini_portile2 (2.8.4)
|
||||
minitest (5.20.0)
|
||||
msgpack (1.7.2)
|
||||
mini_portile2 (2.8.9)
|
||||
minitest (5.25.5)
|
||||
msgpack (1.7.5)
|
||||
multi_json (1.15.0)
|
||||
multi_xml (0.6.0)
|
||||
multipart-post (2.3.0)
|
||||
mysql2 (0.5.5)
|
||||
net-http-persistent (4.0.2)
|
||||
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.3.7)
|
||||
net-imap (0.5.9)
|
||||
date
|
||||
net-protocol
|
||||
net-pop (0.1.2)
|
||||
net-protocol
|
||||
net-protocol (0.2.1)
|
||||
net-protocol (0.2.2)
|
||||
timeout
|
||||
net-smtp (0.3.3)
|
||||
net-smtp (0.5.1)
|
||||
net-protocol
|
||||
nio4r (2.5.9)
|
||||
nokogiri (1.15.4)
|
||||
nio4r (2.7.4)
|
||||
nokogiri (1.18.9)
|
||||
mini_portile2 (~> 2.8.2)
|
||||
racc (~> 1.4)
|
||||
nokogiri (1.15.4-arm64-darwin)
|
||||
nokogiri (1.18.9-aarch64-linux-gnu)
|
||||
racc (~> 1.4)
|
||||
oauth2 (2.0.9)
|
||||
faraday (>= 0.17.3, < 3.0)
|
||||
jwt (>= 1.0, < 3.0)
|
||||
multi_xml (~> 0.5)
|
||||
rack (>= 1.2, < 4)
|
||||
snaky_hash (~> 2.0)
|
||||
version_gem (~> 1.1)
|
||||
oj (3.16.0)
|
||||
omniauth (2.1.1)
|
||||
hashie (>= 3.4.6)
|
||||
rack (>= 2.2.3)
|
||||
rack-protection
|
||||
omniauth-google-oauth2 (1.1.1)
|
||||
jwt (>= 2.0)
|
||||
oauth2 (~> 2.0.6)
|
||||
omniauth (~> 2.0)
|
||||
omniauth-oauth2 (~> 1.8.0)
|
||||
omniauth-oauth2 (1.8.0)
|
||||
oauth2 (>= 1.4, < 3)
|
||||
omniauth (~> 2.0)
|
||||
omniauth-rails_csrf_protection (1.0.1)
|
||||
actionpack (>= 4.2)
|
||||
omniauth (~> 2.0)
|
||||
openssl (3.1.0)
|
||||
nokogiri (1.18.9-aarch64-linux-musl)
|
||||
racc (~> 1.4)
|
||||
nokogiri (1.18.9-arm64-darwin)
|
||||
racc (~> 1.4)
|
||||
nokogiri (1.18.9-x86_64-linux-gnu)
|
||||
racc (~> 1.4)
|
||||
nokogiri (1.18.9-x86_64-linux-musl)
|
||||
racc (~> 1.4)
|
||||
numo-narray (0.9.2.1)
|
||||
oj (3.16.11)
|
||||
bigdecimal (>= 3.0)
|
||||
ostruct (>= 0.2)
|
||||
onnxruntime (0.10.1)
|
||||
ffi
|
||||
onnxruntime (0.10.1-aarch64-linux)
|
||||
ffi
|
||||
onnxruntime (0.10.1-arm64-darwin)
|
||||
ffi
|
||||
onnxruntime (0.10.1-x86_64-linux)
|
||||
ffi
|
||||
openssl (3.3.0)
|
||||
orm_adapter (0.5.0)
|
||||
os (1.1.4)
|
||||
pagy (6.0.4)
|
||||
parallel (1.23.0)
|
||||
parser (3.2.2.4)
|
||||
ostruct (0.6.3)
|
||||
package_json (0.1.0)
|
||||
pagy (9.3.3)
|
||||
parallel (1.27.0)
|
||||
parser (3.3.9.0)
|
||||
ast (~> 2.4.1)
|
||||
racc
|
||||
pdf-reader (2.11.0)
|
||||
Ascii85 (~> 1.0)
|
||||
afm (~> 0.2.1)
|
||||
hashery (~> 2.0)
|
||||
ruby-rc4
|
||||
ttfunk
|
||||
pg (1.5.3)
|
||||
premailer (1.21.0)
|
||||
pg (1.5.9)
|
||||
pp (0.6.2)
|
||||
prettyprint
|
||||
premailer (1.27.0)
|
||||
addressable
|
||||
css_parser (>= 1.12.0)
|
||||
css_parser (>= 1.19.0)
|
||||
htmlentities (>= 4.0.0)
|
||||
premailer-rails (1.12.0)
|
||||
actionmailer (>= 3)
|
||||
net-smtp
|
||||
premailer (~> 1.7, >= 1.7.9)
|
||||
pry (0.14.2)
|
||||
pretender (0.5.0)
|
||||
actionpack (>= 6.1)
|
||||
prettyprint (0.2.0)
|
||||
prism (1.5.1)
|
||||
pry (0.15.0)
|
||||
coderay (~> 1.1)
|
||||
method_source (~> 1.0)
|
||||
pry-rails (0.3.9)
|
||||
pry (>= 0.10.4)
|
||||
public_suffix (5.0.3)
|
||||
puma (6.3.1)
|
||||
pry-rails (0.3.11)
|
||||
pry (>= 0.13.0)
|
||||
psych (5.2.6)
|
||||
date
|
||||
stringio
|
||||
public_suffix (6.0.1)
|
||||
puma (6.5.0)
|
||||
nio4r (~> 2.0)
|
||||
racc (1.7.1)
|
||||
rack (2.2.8)
|
||||
rack-protection (3.1.0)
|
||||
rack (~> 2.2, >= 2.2.4)
|
||||
rack-proxy (0.7.6)
|
||||
racc (1.8.1)
|
||||
rack (3.2.3)
|
||||
rack-proxy (0.7.7)
|
||||
rack
|
||||
rack-test (2.1.0)
|
||||
rack-session (2.1.1)
|
||||
base64 (>= 0.1.0)
|
||||
rack (>= 3.0.0)
|
||||
rack-test (2.2.0)
|
||||
rack (>= 1.3)
|
||||
rails (7.0.7)
|
||||
actioncable (= 7.0.7)
|
||||
actionmailbox (= 7.0.7)
|
||||
actionmailer (= 7.0.7)
|
||||
actionpack (= 7.0.7)
|
||||
actiontext (= 7.0.7)
|
||||
actionview (= 7.0.7)
|
||||
activejob (= 7.0.7)
|
||||
activemodel (= 7.0.7)
|
||||
activerecord (= 7.0.7)
|
||||
activestorage (= 7.0.7)
|
||||
activesupport (= 7.0.7)
|
||||
rackup (2.2.1)
|
||||
rack (>= 3)
|
||||
rails (8.0.2.1)
|
||||
actioncable (= 8.0.2.1)
|
||||
actionmailbox (= 8.0.2.1)
|
||||
actionmailer (= 8.0.2.1)
|
||||
actionpack (= 8.0.2.1)
|
||||
actiontext (= 8.0.2.1)
|
||||
actionview (= 8.0.2.1)
|
||||
activejob (= 8.0.2.1)
|
||||
activemodel (= 8.0.2.1)
|
||||
activerecord (= 8.0.2.1)
|
||||
activestorage (= 8.0.2.1)
|
||||
activesupport (= 8.0.2.1)
|
||||
bundler (>= 1.15.0)
|
||||
railties (= 7.0.7)
|
||||
rails-dom-testing (2.2.0)
|
||||
railties (= 8.0.2.1)
|
||||
rails-dom-testing (2.3.0)
|
||||
activesupport (>= 5.0.0)
|
||||
minitest
|
||||
nokogiri (>= 1.6)
|
||||
rails-html-sanitizer (1.6.0)
|
||||
rails-html-sanitizer (1.6.2)
|
||||
loofah (~> 2.21)
|
||||
nokogiri (~> 1.14)
|
||||
rails-i18n (7.0.8)
|
||||
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)
|
||||
i18n (>= 0.7, < 2)
|
||||
railties (>= 6.0.0, < 8)
|
||||
railties (>= 8.0.0, < 9)
|
||||
rails_autolink (1.1.8)
|
||||
actionview (> 3.1)
|
||||
activesupport (> 3.1)
|
||||
railties (> 3.1)
|
||||
railties (7.0.7)
|
||||
actionpack (= 7.0.7)
|
||||
activesupport (= 7.0.7)
|
||||
method_source
|
||||
railties (8.0.2.1)
|
||||
actionpack (= 8.0.2.1)
|
||||
activesupport (= 8.0.2.1)
|
||||
irb (~> 1.13)
|
||||
rackup (>= 1.0.0)
|
||||
rake (>= 12.2)
|
||||
thor (~> 1.0)
|
||||
zeitwerk (~> 2.5)
|
||||
thor (~> 1.0, >= 1.2.2)
|
||||
zeitwerk (~> 2.6)
|
||||
rainbow (3.1.1)
|
||||
rake (13.0.6)
|
||||
redis-client (0.16.0)
|
||||
rake (13.3.0)
|
||||
rdoc (6.14.2)
|
||||
erb
|
||||
psych (>= 4.0.0)
|
||||
redis-client (0.23.0)
|
||||
connection_pool
|
||||
regexp_parser (2.8.2)
|
||||
reline (0.3.8)
|
||||
regexp_parser (2.11.3)
|
||||
reline (0.6.2)
|
||||
io-console (~> 0.5)
|
||||
representable (3.2.0)
|
||||
declarative (< 0.1.0)
|
||||
trailblazer-option (>= 0.1.1, < 0.2.0)
|
||||
uber (< 0.2.0)
|
||||
request_store (1.5.1)
|
||||
request_store (1.7.0)
|
||||
rack (>= 1.4)
|
||||
responders (3.1.0)
|
||||
responders (3.1.1)
|
||||
actionpack (>= 5.2)
|
||||
railties (>= 5.2)
|
||||
retriable (3.1.2)
|
||||
rexml (3.2.6)
|
||||
rollbar (3.4.0)
|
||||
rexml (3.4.4)
|
||||
rotp (6.3.0)
|
||||
rouge (4.5.2)
|
||||
rqrcode (2.2.0)
|
||||
chunky_png (~> 1.0)
|
||||
rqrcode_core (~> 1.0)
|
||||
rqrcode_core (1.2.0)
|
||||
rspec-core (3.12.2)
|
||||
rspec-support (~> 3.12.0)
|
||||
rspec-expectations (3.12.3)
|
||||
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.12.0)
|
||||
rspec-mocks (3.12.6)
|
||||
rspec-support (~> 3.13.0)
|
||||
rspec-mocks (3.13.2)
|
||||
diff-lcs (>= 1.2.0, < 2.0)
|
||||
rspec-support (~> 3.12.0)
|
||||
rspec-rails (6.0.3)
|
||||
actionpack (>= 6.1)
|
||||
activesupport (>= 6.1)
|
||||
railties (>= 6.1)
|
||||
rspec-core (~> 3.12)
|
||||
rspec-expectations (~> 3.12)
|
||||
rspec-mocks (~> 3.12)
|
||||
rspec-support (~> 3.12)
|
||||
rspec-support (3.12.1)
|
||||
rubocop (1.57.2)
|
||||
rspec-support (~> 3.13.0)
|
||||
rspec-rails (7.1.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.81.1)
|
||||
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.2.2.4)
|
||||
parser (>= 3.3.0.2)
|
||||
rainbow (>= 2.2.2, < 4.0)
|
||||
regexp_parser (>= 1.8, < 3.0)
|
||||
rexml (>= 3.2.5, < 4.0)
|
||||
rubocop-ast (>= 1.28.1, < 2.0)
|
||||
regexp_parser (>= 2.9.3, < 3.0)
|
||||
rubocop-ast (>= 1.47.1, < 2.0)
|
||||
ruby-progressbar (~> 1.7)
|
||||
unicode-display_width (>= 2.4.0, < 3.0)
|
||||
rubocop-ast (1.30.0)
|
||||
parser (>= 3.2.1.0)
|
||||
rubocop-capybara (2.19.0)
|
||||
rubocop (~> 1.41)
|
||||
rubocop-factory_bot (2.24.0)
|
||||
rubocop (~> 1.33)
|
||||
rubocop-performance (1.19.1)
|
||||
rubocop (>= 1.7.0, < 2.0)
|
||||
rubocop-ast (>= 0.4.0)
|
||||
rubocop-rails (2.22.1)
|
||||
unicode-display_width (>= 2.4.0, < 4.0)
|
||||
rubocop-ast (1.47.1)
|
||||
parser (>= 3.3.7.2)
|
||||
prism (~> 1.4)
|
||||
rubocop-performance (1.23.0)
|
||||
rubocop (>= 1.48.1, < 2.0)
|
||||
rubocop-ast (>= 1.31.1, < 2.0)
|
||||
rubocop-rails (2.27.0)
|
||||
activesupport (>= 4.2.0)
|
||||
rack (>= 1.1)
|
||||
rubocop (>= 1.33.0, < 2.0)
|
||||
rubocop-rspec (2.25.0)
|
||||
rubocop (~> 1.40)
|
||||
rubocop-capybara (~> 2.17)
|
||||
rubocop-factory_bot (~> 2.22)
|
||||
rubocop (>= 1.52.0, < 2.0)
|
||||
rubocop-ast (>= 1.31.1, < 2.0)
|
||||
rubocop-rspec (3.3.0)
|
||||
rubocop (~> 1.61)
|
||||
ruby-progressbar (1.13.0)
|
||||
ruby-rc4 (0.1.5)
|
||||
ruby-vips (2.1.4)
|
||||
ruby-vips (2.2.2)
|
||||
ffi (~> 1.12)
|
||||
logger
|
||||
ruby2_keywords (0.0.5)
|
||||
rubyXL (3.4.25)
|
||||
rubyXL (3.4.33)
|
||||
nokogiri (>= 1.10.8)
|
||||
rubyzip (>= 1.3.0)
|
||||
rubyzip (2.3.2)
|
||||
semantic_range (3.0.0)
|
||||
shakapacker (7.0.3)
|
||||
securerandom (0.4.1)
|
||||
semantic_range (3.1.0)
|
||||
shakapacker (8.0.2)
|
||||
activesupport (>= 5.2)
|
||||
package_json
|
||||
rack-proxy (>= 0.6.1)
|
||||
railties (>= 5.2)
|
||||
semantic_range (>= 2.3.0)
|
||||
sidekiq (7.1.2)
|
||||
concurrent-ruby (< 2)
|
||||
sidekiq (7.3.7)
|
||||
connection_pool (>= 2.3.0)
|
||||
logger
|
||||
rack (>= 2.2.4)
|
||||
redis-client (>= 0.14.0)
|
||||
signet (0.17.0)
|
||||
redis-client (>= 0.22.2)
|
||||
signet (0.19.0)
|
||||
addressable (~> 2.8)
|
||||
faraday (>= 0.17.5, < 3.a)
|
||||
jwt (>= 1.5, < 3.0)
|
||||
@@ -511,65 +554,83 @@ GEM
|
||||
docile (~> 1.1)
|
||||
simplecov-html (~> 0.11)
|
||||
simplecov_json_formatter (~> 0.1)
|
||||
simplecov-html (0.12.3)
|
||||
simplecov-html (0.13.1)
|
||||
simplecov_json_formatter (0.1.4)
|
||||
smart_properties (1.17.0)
|
||||
snaky_hash (2.0.1)
|
||||
hashie
|
||||
version_gem (~> 1.1, >= 1.1.1)
|
||||
sqlite3 (1.6.3)
|
||||
sqlite3 (2.5.0)
|
||||
mini_portile2 (~> 2.8.0)
|
||||
sqlite3 (1.6.3-arm64-darwin)
|
||||
strip_attributes (1.13.0)
|
||||
activemodel (>= 3.0, < 8.0)
|
||||
thor (1.2.2)
|
||||
timeout (0.4.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.7)
|
||||
strip_attributes (1.14.1)
|
||||
activemodel (>= 3.0, < 9.0)
|
||||
strscan (3.1.5)
|
||||
thor (1.4.0)
|
||||
timeout (0.4.3)
|
||||
trailblazer-option (0.1.2)
|
||||
ttfunk (1.7.0)
|
||||
turbo-rails (1.4.0)
|
||||
turbo-rails (2.0.11)
|
||||
actionpack (>= 6.0.0)
|
||||
activejob (>= 6.0.0)
|
||||
railties (>= 6.0.0)
|
||||
twitter_cldr (6.12.1)
|
||||
camertron-eprun
|
||||
cldr-plurals-runtime-rb (~> 1.1)
|
||||
tzinfo
|
||||
tzinfo (2.0.6)
|
||||
concurrent-ruby (~> 1.0)
|
||||
tzinfo-data (1.2023.3)
|
||||
tzinfo-data (1.2024.2)
|
||||
tzinfo (>= 1.0.0)
|
||||
uber (0.1.0)
|
||||
unicode-display_width (2.5.0)
|
||||
unicode-display_width (3.2.0)
|
||||
unicode-emoji (~> 4.1)
|
||||
unicode-emoji (4.1.0)
|
||||
uniform_notifier (1.16.0)
|
||||
version_gem (1.1.3)
|
||||
uri (1.1.1)
|
||||
useragent (0.16.11)
|
||||
warden (1.2.9)
|
||||
rack (>= 2.0.9)
|
||||
web-console (4.2.0)
|
||||
web-console (4.2.1)
|
||||
actionview (>= 6.0.0)
|
||||
activemodel (>= 6.0.0)
|
||||
bindex (>= 0.4.0)
|
||||
railties (>= 6.0.0)
|
||||
webmock (3.18.1)
|
||||
webmock (3.24.0)
|
||||
addressable (>= 2.8.0)
|
||||
crack (>= 0.3.2)
|
||||
hashdiff (>= 0.4.0, < 2.0.0)
|
||||
webrick (1.8.1)
|
||||
websocket-driver (0.7.6)
|
||||
webrick (1.9.1)
|
||||
websocket-driver (0.8.0)
|
||||
base64
|
||||
websocket-extensions (>= 0.1.0)
|
||||
websocket-extensions (0.1.5)
|
||||
xpath (3.2.0)
|
||||
nokogiri (~> 1.8)
|
||||
zeitwerk (2.6.11)
|
||||
zeitwerk (2.7.3)
|
||||
|
||||
PLATFORMS
|
||||
arm64-darwin-22
|
||||
aarch64-linux
|
||||
aarch64-linux-musl
|
||||
arm64-darwin
|
||||
ruby
|
||||
x86_64-linux
|
||||
x86_64-linux-musl
|
||||
|
||||
DEPENDENCIES
|
||||
annotate
|
||||
annotaterb
|
||||
arabic-letter-connector
|
||||
aws-sdk-s3
|
||||
aws-sdk-secretsmanager
|
||||
azure-storage-blob
|
||||
better_html
|
||||
bootsnap
|
||||
brakeman
|
||||
bullet
|
||||
cancancan
|
||||
capybara
|
||||
csv
|
||||
csv-safe
|
||||
cuprite
|
||||
debug
|
||||
devise
|
||||
@@ -581,6 +642,7 @@ DEPENDENCIES
|
||||
faker
|
||||
faraday
|
||||
faraday-follow_redirects
|
||||
foreman
|
||||
google-cloud-storage
|
||||
hexapdf
|
||||
image_processing
|
||||
@@ -588,20 +650,21 @@ DEPENDENCIES
|
||||
letter_opener_web
|
||||
lograge
|
||||
mysql2
|
||||
numo-narray
|
||||
oj
|
||||
omniauth-google-oauth2
|
||||
omniauth-rails_csrf_protection
|
||||
onnxruntime
|
||||
pagy
|
||||
pdf-reader
|
||||
pg
|
||||
premailer-rails
|
||||
pretender
|
||||
pry-rails
|
||||
puma
|
||||
rack
|
||||
rails
|
||||
rails-i18n
|
||||
rails_autolink
|
||||
rollbar
|
||||
rotp
|
||||
rouge
|
||||
rqrcode
|
||||
rspec-rails
|
||||
rubocop
|
||||
@@ -616,12 +679,13 @@ DEPENDENCIES
|
||||
sqlite3
|
||||
strip_attributes
|
||||
turbo-rails
|
||||
twitter_cldr
|
||||
tzinfo-data
|
||||
web-console
|
||||
webmock
|
||||
|
||||
RUBY VERSION
|
||||
ruby 3.2.2p53
|
||||
ruby 3.4.2p28
|
||||
|
||||
BUNDLED WITH
|
||||
2.4.10
|
||||
2.5.3
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<h1 align="center" style="border-bottom: none">
|
||||
<div>
|
||||
<a href="https://www.docuseal.co">
|
||||
<a href="https://www.docuseal.com">
|
||||
<img alt="DocuSeal" src="https://github.com/docusealco/docuseal/assets/5418788/c12cd051-81cd-4402-bc3a-92f2cfdc1b06" width="80" />
|
||||
<br>
|
||||
</a>
|
||||
@@ -25,25 +25,40 @@
|
||||
DocuSeal is an open source platform that provides secure and efficient digital document signing and processing. Create PDF forms to have them filled and signed online on any device with an easy-to-use, mobile-optimized web tool.
|
||||
</p>
|
||||
<h2 align="center">
|
||||
<a href="https://demo.docuseal.co">✨ Live Demo</a>
|
||||
<a href="https://demo.docuseal.tech">✨ Live Demo</a>
|
||||
<span>|</span>
|
||||
<a href="https://docuseal.co/sign_up">☁️ Try in Cloud</a>
|
||||
<a href="https://docuseal.com/sign_up">☁️ Try in Cloud</a>
|
||||
</h2>
|
||||
|
||||
[](https://demo.docuseal.co)
|
||||
[](https://demo.docuseal.tech)
|
||||
|
||||
## Features
|
||||
- [x] PDF form fields builder (WYSIWYG)
|
||||
- [x] 10 field types available (Signature, Date, File, Checkbox etc.)
|
||||
- [x] Multiple submitters per document
|
||||
- [x] Automated emails via SMTP
|
||||
- [x] Files storage on AWS S3, Google Storage, or Azure
|
||||
- [x] Automatic PDF eSignature
|
||||
- [x] PDF signature verification
|
||||
- [x] Users management
|
||||
- [x] Mobile-optimized
|
||||
- [x] API and Webhooks for integrations
|
||||
- [x] Easy to deploy in minutes
|
||||
- PDF form fields builder (WYSIWYG)
|
||||
- 12 field types available (Signature, Date, File, Checkbox etc.)
|
||||
- Multiple submitters per document
|
||||
- Automated emails via SMTP
|
||||
- Files storage on disk or AWS S3, Google Storage, Azure Cloud
|
||||
- Automatic PDF eSignature
|
||||
- PDF signature verification
|
||||
- Users management
|
||||
- Mobile-optimized
|
||||
- 7 UI languages with signing available in 14 languages
|
||||
- API and Webhooks for integrations
|
||||
- Easy to deploy in minutes
|
||||
|
||||
## Pro Features
|
||||
- Company logo and white-label
|
||||
- User roles
|
||||
- Automated reminders
|
||||
- Invitation and identify verification via SMS
|
||||
- Conditional fields and formulas
|
||||
- Bulk send with CSV, XLSX spreadsheet import
|
||||
- SSO / SAML
|
||||
- Template creation with HTML API ([Guide](https://www.docuseal.com/guides/create-pdf-document-fillable-form-with-html-api))
|
||||
- Template creation with PDF or DOCX and field tags API ([Guide](https://www.docuseal.com/guides/use-embedded-text-field-tags-in-the-pdf-to-create-a-fillable-form))
|
||||
- Embedded signing form ([React](https://github.com/docusealco/docuseal-react), [Vue](https://github.com/docusealco/docuseal-vue), [Angular](https://github.com/docusealco/docuseal-angular) or [JavaScript](https://www.docuseal.com/docs/embedded))
|
||||
- Embedded document form builder ([React](https://github.com/docusealco/docuseal-react), [Vue](https://github.com/docusealco/docuseal-vue), [Angular](https://github.com/docusealco/docuseal-angular) or [JavaScript](https://www.docuseal.com/docs/embedded))
|
||||
- [Learn more](https://www.docuseal.com/pricing)
|
||||
|
||||
## Deploy
|
||||
|
||||
@@ -52,9 +67,6 @@ DocuSeal is an open source platform that provides secure and efficient digital d
|
||||
| [<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)|
|
||||
|**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)
|
||||
|**Koyeb**|**Elestio**|
|
||||
| [<img alt="Deploy on Koyeb" src="https://www.koyeb.com/static/images/deploy/button.svg" height="40">](https://app.koyeb.com/deploy?name=docuseal&type=docker&image=docker.io/docuseal/docuseal&env[PORT]=8000&env[DATABASE_URL]=CHANGE_ME&env[SECRET_KEY_BASE]=CHANGE_ME&ports=8000;http;/) | [<img alt="Deploy on Elestio" src="https://pub-da36157c854648669813f3f76c526c2b.r2.dev/deploy-on-elestio-black.png">](https://dash.elest.io/deploy?soft=DocuSeal&id=339) |
|
||||
|
||||
|
||||
#### Docker
|
||||
|
||||
@@ -73,17 +85,23 @@ curl https://raw.githubusercontent.com/docusealco/docuseal/master/docker-compose
|
||||
|
||||
Run the app under a custom domain over https using docker compose (make sure your DNS points to the server to automatically issue ssl certs with Caddy):
|
||||
```sh
|
||||
HOST=your-domain-name.com docker-compose up
|
||||
sudo HOST=your-domain-name.com docker compose up
|
||||
```
|
||||
|
||||
## For Companies
|
||||
### Integrate seamless document signing into your web or mobile apps with DocuSeal!
|
||||
## For Businesses
|
||||
### Integrate seamless document signing into your web or mobile apps with DocuSeal
|
||||
|
||||
At DocuSeal we have expertise and technologies to make documents creation, filling, signing and processing seamlessly integrated with your product. We specialize in working with various industries, including **Banking, Healthcare, Transport, Real Estate, eCommerce, KYC, CRM, and other software products** that require bulk document signing. By leveraging DocuSeal, we can assist in reducing the overall cost of developing and processing electronic documents while ensuring security and compliance with local electronic document laws.
|
||||
|
||||
[Book a Meeting](https://calendly.com/kriti-docuseal/30min)
|
||||
[Book a Meeting](https://www.docuseal.com/contact)
|
||||
|
||||
## 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.
|
||||
|
||||
## Tools
|
||||
|
||||
- [Signature Maker](https://www.docuseal.com/online-signature)
|
||||
- [Sign Document Online](https://www.docuseal.com/sign-documents-online)
|
||||
- [Fill PDF Online](https://www.docuseal.com/fill-pdf)
|
||||
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
# Reporting a Vulnerability
|
||||
|
||||
If you come across any security concern or vulnarability, please report the information via email to security@docuseal.com instead of opening a GitHub issue. We will promptly respond and will collaborate with you to validate the issue, and resolve it ASAP.
|
||||
|
||||
**We have a bug bounty program to reward security researchers.**
|
||||
|
||||
Out of scope vulnerabilities:
|
||||
|
||||
- CSRF
|
||||
- DNSSEC, CAA, CSP headers
|
||||
- DNS or email security related
|
||||
- Rate Limiting
|
||||
|
||||
Note: We reserve the right to classify any reported vulnerability as out of scope for the bug bounty program.
|
||||
@@ -0,0 +1,59 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class AccountConfigsController < ApplicationController
|
||||
before_action :load_account_config, only: :create
|
||||
authorize_resource :account_config, only: :create
|
||||
|
||||
load_and_authorize_resource :account_config, only: :destroy
|
||||
|
||||
ALLOWED_KEYS = [
|
||||
AccountConfig::ALLOW_TYPED_SIGNATURE,
|
||||
AccountConfig::FORCE_MFA,
|
||||
AccountConfig::ALLOW_TO_RESUBMIT,
|
||||
AccountConfig::ALLOW_TO_DECLINE_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
|
||||
].freeze
|
||||
|
||||
InvalidKey = Class.new(StandardError)
|
||||
|
||||
def create
|
||||
@account_config.update!(account_config_params)
|
||||
|
||||
head :ok
|
||||
end
|
||||
|
||||
def destroy
|
||||
raise InvalidKey unless ALLOWED_KEYS.include?(@account_config.key)
|
||||
|
||||
@account_config.destroy!
|
||||
|
||||
redirect_back(fallback_location: root_path)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def load_account_config
|
||||
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])
|
||||
end
|
||||
|
||||
def account_config_params
|
||||
params.required(:account_config).permit(:key, :value, { value: {} }, { value: [] }).tap do |attrs|
|
||||
attrs[:value] = attrs[:value] == '1' if attrs[:value].in?(%w[1 0])
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -4,9 +4,12 @@ class AccountsController < ApplicationController
|
||||
LOCALE_OPTIONS = {
|
||||
'en-US' => 'English (United States)',
|
||||
'en-GB' => 'English (United Kingdom)',
|
||||
'es-ES' => 'Spanish (Spain)',
|
||||
'pt-PT' => 'Portuguese (Portugal)',
|
||||
'de-DE' => 'German (Germany)'
|
||||
'fr-FR' => 'Français',
|
||||
'es-ES' => 'Español',
|
||||
'pt-PT' => 'Português',
|
||||
'de-DE' => 'Deutsch',
|
||||
'it-IT' => 'Italiano',
|
||||
'nl-NL' => 'Nederlands'
|
||||
}.freeze
|
||||
|
||||
before_action :load_account
|
||||
@@ -17,23 +20,42 @@ class AccountsController < ApplicationController
|
||||
def update
|
||||
current_account.update!(account_params)
|
||||
|
||||
@encrypted_config = EncryptedConfig.find_or_initialize_by(account: current_account,
|
||||
key: EncryptedConfig::APP_URL_KEY)
|
||||
@encrypted_config.assign_attributes(app_url_params)
|
||||
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, 'should be a valid URL')
|
||||
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_entity
|
||||
return render :show, status: :unprocessable_content
|
||||
end
|
||||
|
||||
@encrypted_config.save!
|
||||
|
||||
Docuseal.refresh_default_url_options!
|
||||
end
|
||||
|
||||
@encrypted_config.save!
|
||||
|
||||
Docuseal.refresh_default_url_options!
|
||||
|
||||
redirect_to settings_account_path, notice: 'Account information has been updated'
|
||||
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.skip_reconfirmation!
|
||||
true_user.update!(locked_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'))
|
||||
)
|
||||
# rubocop:enable Layout/LineLength
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Api
|
||||
class ActiveStorageBlobsProxyController < ApiBaseController
|
||||
include ActiveStorage::Streaming
|
||||
|
||||
skip_before_action :authenticate_user!
|
||||
skip_authorization_check
|
||||
|
||||
before_action :set_cors_headers
|
||||
before_action :set_noindex_headers
|
||||
|
||||
def show
|
||||
blob_uuid, purp, exp = ApplicationRecord.signed_id_verifier.verified(params[:signed_uuid])
|
||||
|
||||
if blob_uuid.blank? || purp != 'blob'
|
||||
Rollbar.error('Blob not found') if defined?(Rollbar)
|
||||
|
||||
return head :not_found
|
||||
end
|
||||
|
||||
blob = ActiveStorage::Blob.find_by!(uuid: blob_uuid)
|
||||
|
||||
attachment = blob.attachments.take
|
||||
|
||||
@record = attachment.record
|
||||
@record = @record.record if @record.is_a?(ActiveStorage::Attachment)
|
||||
|
||||
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]
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
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)
|
||||
|
||||
if exp.blank?
|
||||
configs = record.account.account_configs.where(key: [AccountConfig::DOWNLOAD_LINKS_AUTH_KEY,
|
||||
AccountConfig::DOWNLOAD_LINKS_EXPIRE_KEY])
|
||||
|
||||
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
|
||||
|
||||
Rollbar.error('Blob unauthorized') if defined?(Rollbar)
|
||||
|
||||
raise CanCan::AccessDenied
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,46 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Api
|
||||
class ActiveStorageBlobsProxyLegacyController < ApiBaseController
|
||||
include ActiveStorage::Streaming
|
||||
|
||||
skip_before_action :authenticate_user!
|
||||
skip_authorization_check
|
||||
|
||||
before_action :set_cors_headers
|
||||
before_action :set_noindex_headers
|
||||
|
||||
# rubocop:disable Metrics
|
||||
def show
|
||||
Rollbar.info('Blob legacy') if defined?(Rollbar)
|
||||
|
||||
blob = ActiveStorage::Blob.find_signed(params[:signed_blob_id] || params[:signed_id])
|
||||
|
||||
return head :not_found unless blob
|
||||
|
||||
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' } ||
|
||||
a.name == 'logo'
|
||||
end
|
||||
|
||||
unless is_permitted
|
||||
Rollbar.error("Blob account not found: #{blob.id}") if defined?(Rollbar)
|
||||
|
||||
return render json: { error: 'Not authenticated' }, status: :unauthorized
|
||||
end
|
||||
|
||||
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]
|
||||
end
|
||||
end
|
||||
end
|
||||
# rubocop:enable Metrics
|
||||
end
|
||||
end
|
||||
@@ -8,33 +8,106 @@ module Api
|
||||
DEFAULT_LIMIT = 10
|
||||
MAX_LIMIT = 100
|
||||
|
||||
impersonates :user, with: ->(uuid) { User.find_by(uuid:) }
|
||||
|
||||
wrap_parameters false
|
||||
|
||||
before_action :authenticate_user!
|
||||
check_authorization
|
||||
|
||||
if Rails.env.production?
|
||||
rescue_from CanCan::AccessDenied do |e|
|
||||
Rollbar.error(e) if defined?(Rollbar)
|
||||
rescue_from Params::BaseValidator::InvalidParameterError do |e|
|
||||
render json: { error: e.message }, status: :unprocessable_content
|
||||
end
|
||||
|
||||
render json: { error: e.message }, status: :forbidden
|
||||
rescue_from RateLimit::LimitApproached do |e|
|
||||
Rollbar.error(e) if defined?(Rollbar)
|
||||
|
||||
render json: { error: 'Too many requests' }, status: :too_many_requests
|
||||
end
|
||||
|
||||
unless Rails.env.development?
|
||||
rescue_from CanCan::AccessDenied do |e|
|
||||
render json: { error: access_denied_error_message(e) }, status: :forbidden
|
||||
end
|
||||
|
||||
rescue_from JSON::ParserError do |e|
|
||||
Rollbar.warning(e) if defined?(Rollbar)
|
||||
|
||||
render json: { error: "JSON parse error: #{e.message}" }, status: :unprocessable_content
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def paginate(relation)
|
||||
result = relation.order(id: :desc)
|
||||
.limit([params[:limit] || DEFAULT_LIMIT, MAX_LIMIT].min)
|
||||
def access_denied_error_message(error)
|
||||
return 'Not authorized' if request.headers['X-Auth-Token'].blank?
|
||||
return 'Not authorized' unless error.subject.is_a?(ActiveRecord::Base)
|
||||
return 'Not authorized' unless error.subject.respond_to?(:account_id)
|
||||
|
||||
result = result.where('id < ?', params[:after]) if params[:after].present?
|
||||
result = result.where('id > ?', params[:before]) if params[:before].present?
|
||||
linked_account_record_exists =
|
||||
if current_user.account.testing?
|
||||
current_user.account.linked_account_accounts.where(account_type: 'testing')
|
||||
.exists?(account_id: error.subject.account_id)
|
||||
else
|
||||
current_user.account.testing_accounts.exists?(id: error.subject.account_id)
|
||||
end
|
||||
|
||||
return 'Not authorized' unless linked_account_record_exists
|
||||
|
||||
object_name = error.subject.model_name.human
|
||||
id = error.subject.id
|
||||
|
||||
if current_user.account.testing?
|
||||
"#{object_name} #{id} not found using testing API key; Use production API key to " \
|
||||
"access production #{object_name.downcase.pluralize}."
|
||||
else
|
||||
"#{object_name} #{id} not found using production API key; Use testing API key to " \
|
||||
"access testing #{object_name.downcase.pluralize}."
|
||||
end
|
||||
end
|
||||
|
||||
def paginate(relation, field: :id)
|
||||
result = relation.order(field => :desc)
|
||||
.limit([params.fetch(:limit, DEFAULT_LIMIT).to_i, MAX_LIMIT].min)
|
||||
|
||||
if field == :id
|
||||
result = result.where(id: ...params[:after].to_i) if params[:after].present?
|
||||
result = result.where(id: (params[:before].to_i + 1)...) if params[:before].present?
|
||||
else
|
||||
result = result.where(field => ...params[:after]) if params[:after].present?
|
||||
result = result.where(field => (params[:before] + 1)...) if params[:before].present?
|
||||
end
|
||||
|
||||
result
|
||||
end
|
||||
|
||||
def authenticate_user!
|
||||
render json: { error: 'Not authenticated' }, status: :unauthorized unless current_user
|
||||
end
|
||||
|
||||
def current_user
|
||||
super || @current_user ||=
|
||||
if request.headers['X-Auth-Token'].present?
|
||||
sha256 = Digest::SHA256.hexdigest(request.headers['X-Auth-Token'])
|
||||
|
||||
User.joins(:access_token).active.find_by(access_token: { sha256: })
|
||||
end
|
||||
end
|
||||
|
||||
def current_account
|
||||
current_user&.account
|
||||
end
|
||||
|
||||
def set_noindex_headers
|
||||
headers['X-Robots-Tag'] = 'noindex'
|
||||
end
|
||||
|
||||
def set_cors_headers
|
||||
headers['Access-Control-Allow-Origin'] = '*'
|
||||
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
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,16 +1,58 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Api
|
||||
class AttachmentsController < ApiBaseController
|
||||
skip_before_action :authenticate_user!
|
||||
skip_authorization_check
|
||||
class AttachmentsController < ActionController::API
|
||||
include ActionController::Cookies
|
||||
include ActiveStorage::SetCurrent
|
||||
|
||||
COOKIE_STORE_LIMIT = 10
|
||||
|
||||
def create
|
||||
submitter = Submitter.find_by!(slug: params[:submitter_slug])
|
||||
|
||||
if params[:type].in?(%w[initials signature])
|
||||
image = Vips::Image.new_from_file(params[:file].path)
|
||||
|
||||
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
|
||||
end
|
||||
|
||||
attachment = Submitters.create_attachment!(submitter, params)
|
||||
|
||||
render json: attachment.as_json(only: %i[uuid], methods: %i[url filename content_type])
|
||||
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 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
|
||||
end
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Api
|
||||
class FormEventsController < ApiBaseController
|
||||
load_and_authorize_resource :submitter, parent: false
|
||||
|
||||
def index
|
||||
submitters = @submitters.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?
|
||||
|
||||
submitters = paginate(
|
||||
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, expires_at:)
|
||||
}
|
||||
end,
|
||||
pagination: {
|
||||
count: submitters.size,
|
||||
next: submitters.last&.completed_at&.to_i,
|
||||
prev: submitters.first&.completed_at&.to_i
|
||||
}
|
||||
}
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,75 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Api
|
||||
class SubmissionDocumentsController < ApiBaseController
|
||||
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?)
|
||||
build_completed_documents(@submission, merge: is_merge)
|
||||
else
|
||||
build_preview_documents(@submission, merge: is_merge)
|
||||
end
|
||||
|
||||
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, expires_at:) }
|
||||
end
|
||||
}
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def build_completed_documents(submission, merge: false)
|
||||
last_submitter = submission.submitters.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
|
||||
@@ -0,0 +1,55 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Api
|
||||
class SubmissionEventsController < ApiBaseController
|
||||
load_and_authorize_resource :submission, parent: false
|
||||
|
||||
def index
|
||||
submissions = build_completed_query(@submissions)
|
||||
|
||||
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?
|
||||
|
||||
submissions = paginate(submissions.preload(
|
||||
:created_by_user, :submission_events,
|
||||
template: :folder,
|
||||
submitters: { documents_attachments: :blob, attachments_attachments: :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, expires_at:)
|
||||
}
|
||||
end,
|
||||
pagination: {
|
||||
count: submissions.size,
|
||||
next: submissions.last&.completed_at&.to_i,
|
||||
prev: submissions.first&.completed_at&.to_i
|
||||
}
|
||||
}
|
||||
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
|
||||
@@ -3,20 +3,28 @@
|
||||
module Api
|
||||
class SubmissionsController < ApiBaseController
|
||||
load_and_authorize_resource :template, only: :create
|
||||
load_and_authorize_resource :submission, only: %i[show index]
|
||||
load_and_authorize_resource :submission, only: %i[show index destroy]
|
||||
|
||||
before_action only: :create do
|
||||
authorize!(:create, Submission)
|
||||
end
|
||||
|
||||
def index
|
||||
submissions = Submissions.search(@submissions, params[:q])
|
||||
submissions = submissions.where(template_id: params[:template_id]) if params[:template_id].present?
|
||||
submissions = Submissions.search(current_user, @submissions, params[:q])
|
||||
submissions = filter_submissions(submissions, params)
|
||||
|
||||
submissions = paginate(submissions.preload(:created_by_user, :template, :submitters))
|
||||
submissions = paginate(submissions.preload(:created_by_user, :submitters,
|
||||
template: { folder: :parent_folder },
|
||||
combined_document_attachment: :blob,
|
||||
audit_trail_attachment: :blob))
|
||||
|
||||
expires_at = Accounts.link_expires_at(current_account)
|
||||
|
||||
render json: {
|
||||
data: submissions.as_json(serialize_params),
|
||||
data: submissions.map do |s|
|
||||
Submissions::SerializeForApi.call(s, s.submitters, params,
|
||||
with_events: false, with_documents: false, with_values: false, expires_at:)
|
||||
end,
|
||||
pagination: {
|
||||
count: submissions.size,
|
||||
next: submissions.last&.id,
|
||||
@@ -26,132 +34,176 @@ module Api
|
||||
end
|
||||
|
||||
def show
|
||||
serialized_subbmitters =
|
||||
@submission.submitters.preload(documents_attachments: :blob, attachments_attachments: :blob).map do |submitter|
|
||||
Submissions::EnsureResultGenerated.call(submitter) if submitter.completed_at?
|
||||
submitters = @submission.submitters.preload(documents_attachments: :blob, attachments_attachments: :blob)
|
||||
|
||||
Submitters::SerializeForApi.call(submitter)
|
||||
submitters.each do |submitter|
|
||||
if submitter.completed_at? && submitter.documents_attachments.blank?
|
||||
submitter.documents_attachments = Submissions::EnsureResultGenerated.call(submitter)
|
||||
end
|
||||
end
|
||||
|
||||
json = @submission.as_json(
|
||||
serialize_params.deep_merge(
|
||||
include: {
|
||||
submission_events: {
|
||||
only: %i[id submitter_id event_type event_timestamp]
|
||||
}
|
||||
}
|
||||
)
|
||||
)
|
||||
if @submission.audit_trail_attachment.blank? && submitters.all?(&:completed_at?)
|
||||
@submission.audit_trail_attachment = Submissions::EnsureAuditGenerated.call(@submission)
|
||||
end
|
||||
|
||||
json[:submitters] = serialized_subbmitters
|
||||
|
||||
render json:
|
||||
render json: Submissions::SerializeForApi.call(@submission, submitters, params)
|
||||
end
|
||||
|
||||
def create
|
||||
is_send_email = !params[:send_email].in?(['false', false])
|
||||
Params::SubmissionCreateValidator.call(params)
|
||||
|
||||
submissions =
|
||||
if (emails = (params[:emails] || params[:email]).presence) && params[:submission].blank?
|
||||
Submissions.create_from_emails(template: @template,
|
||||
user: current_user,
|
||||
source: :api,
|
||||
mark_as_sent: is_send_email,
|
||||
emails:)
|
||||
else
|
||||
submissions_attrs, attachments = normalize_submissions_params!(submissions_params[:submission], @template)
|
||||
return render json: { error: 'Template not found' }, status: :unprocessable_content if @template.nil?
|
||||
|
||||
Submissions.create_from_submitters(
|
||||
template: @template,
|
||||
user: current_user,
|
||||
source: :api,
|
||||
mark_as_sent: is_send_email,
|
||||
submitters_order: params[:submitters_order] || 'preserved',
|
||||
submissions_attrs:
|
||||
)
|
||||
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_content
|
||||
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.enqueue_events(submissions, 'submission.created')
|
||||
|
||||
Submissions.send_signature_requests(submissions)
|
||||
|
||||
submissions.each do |submission|
|
||||
submission.submitters.each do |submitter|
|
||||
next unless submitter.completed_at?
|
||||
|
||||
ProcessSubmitterCompletionJob.perform_async('submitter_id' => submitter.id, 'send_invitation_email' => false)
|
||||
end
|
||||
end
|
||||
|
||||
Submissions.send_signature_requests(submissions, send_email: is_send_email)
|
||||
SearchEntries.enqueue_reindex(submissions)
|
||||
|
||||
submitters = submissions.flat_map(&:submitters)
|
||||
render json: build_create_json(submissions)
|
||||
rescue Submitters::NormalizeValues::BaseError, Submissions::CreateFromSubmitters::BaseError,
|
||||
DownloadUtils::UnableToDownload => e
|
||||
Rollbar.warning(e) if defined?(Rollbar)
|
||||
|
||||
save_default_value_attachments!(attachments, submitters)
|
||||
|
||||
render json: submitters
|
||||
rescue Submitters::NormalizeValues::UnknownFieldName, Submitters::NormalizeValues::UnknownSubmitterName => e
|
||||
render json: { error: e.message }, status: :unprocessable_entity
|
||||
render json: { error: e.message }, status: :unprocessable_content
|
||||
end
|
||||
|
||||
def destroy
|
||||
@submission.update!(deleted_at: Time.current)
|
||||
if params[:permanently].in?(['true', true])
|
||||
@submission.destroy!
|
||||
else
|
||||
@submission.update!(archived_at: Time.current)
|
||||
|
||||
render json: @submission.as_json(only: %i[id deleted_at])
|
||||
WebhookUrls.enqueue_events(@submission, 'submission.archived')
|
||||
end
|
||||
|
||||
render json: @submission.as_json(only: %i[id archived_at])
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def serialize_params
|
||||
{
|
||||
only: %i[id source submitters_order created_at updated_at],
|
||||
include: {
|
||||
submitters: { only: %i[id slug uuid name email phone
|
||||
completed_at opened_at sent_at
|
||||
created_at updated_at],
|
||||
methods: %i[status] },
|
||||
template: { only: %i[id name created_at updated_at] },
|
||||
created_by_user: { only: %i[id email first_name last_name] }
|
||||
}
|
||||
}
|
||||
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?
|
||||
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)
|
||||
submissions = params[:archived].in?(['true', true]) ? submissions.archived : submissions.active
|
||||
end
|
||||
|
||||
Submissions::Filter.call(submissions, current_user, params)
|
||||
end
|
||||
|
||||
def submissions_params
|
||||
params.permit(submission: [{
|
||||
submitters: [[:uuid, :name, :email, :role, :completed, :phone, :application_key,
|
||||
{ values: {}, readonly_fields: [],
|
||||
fields: [%i[name default_value readonly validation_pattern invalid_message]] }]]
|
||||
}])
|
||||
end
|
||||
|
||||
def normalize_submissions_params!(submissions_params, template)
|
||||
attachments = []
|
||||
|
||||
Array.wrap(submissions_params).each do |submission|
|
||||
submission[:submitters].each_with_index do |submitter, index|
|
||||
default_values = submitter[:values] || {}
|
||||
|
||||
submitter[:fields]&.each { |f| default_values[f[:name]] = f[:default_value] if f[:default_value].present? }
|
||||
|
||||
next if default_values.blank?
|
||||
|
||||
values, new_attachments =
|
||||
Submitters::NormalizeValues.call(template,
|
||||
default_values,
|
||||
submitter[:role] || template.submitters[index]['name'])
|
||||
|
||||
attachments.push(*new_attachments)
|
||||
|
||||
submitter[:values] = values
|
||||
def build_create_json(submissions)
|
||||
json = submissions.flat_map do |submission|
|
||||
submission.submitters.map do |s|
|
||||
Submitters::SerializeForApi.call(s, with_documents: false, with_urls: true, params:)
|
||||
end
|
||||
end
|
||||
|
||||
[submissions_params, attachments]
|
||||
if request.path.ends_with?('/init')
|
||||
json =
|
||||
if submissions.size == 1
|
||||
{
|
||||
id: submissions.first.id,
|
||||
submitters: json,
|
||||
expire_at: submissions.first.expire_at,
|
||||
created_at: submissions.first.created_at
|
||||
}
|
||||
else
|
||||
{ submitters: json }
|
||||
end
|
||||
end
|
||||
|
||||
json
|
||||
end
|
||||
|
||||
def save_default_value_attachments!(attachments, submitters)
|
||||
return if attachments.blank?
|
||||
def create_submissions(template, params)
|
||||
is_send_email = !params[:send_email].in?(['false', false])
|
||||
|
||||
attachments_index = attachments.index_by(&:uuid)
|
||||
if (emails = (params[:emails] || params[:email]).presence) &&
|
||||
params[:submission].blank? && params[:submitters].blank?
|
||||
Submissions.create_from_emails(template:,
|
||||
user: current_user,
|
||||
source: :api,
|
||||
mark_as_sent: is_send_email,
|
||||
emails:,
|
||||
params:)
|
||||
else
|
||||
submissions_attrs, attachments =
|
||||
Submissions::NormalizeParamUtils.normalize_submissions_params!(submissions_params, template)
|
||||
|
||||
submitters.each do |submitter|
|
||||
submitter.values.to_a.each do |_, value|
|
||||
attachment = attachments_index[value]
|
||||
submissions = Submissions.create_from_submitters(
|
||||
template:,
|
||||
user: current_user,
|
||||
source: :api,
|
||||
submitters_order: params[:submitters_order] || params[:order] || 'preserved',
|
||||
submissions_attrs:,
|
||||
params:
|
||||
)
|
||||
|
||||
next unless attachment
|
||||
submitters = submissions.flat_map(&:submitters)
|
||||
|
||||
attachment.record = submitter
|
||||
Submissions::NormalizeParamUtils.save_default_value_attachments!(attachments, submitters)
|
||||
|
||||
attachment.save!
|
||||
submitters.each do |submitter|
|
||||
SubmissionEvents.create_with_tracking_data(submitter, 'api_complete_form', request) if submitter.completed_at?
|
||||
end
|
||||
|
||||
submissions
|
||||
end
|
||||
end
|
||||
|
||||
def submissions_params
|
||||
permitted_attrs = [
|
||||
:send_email, :send_sms, :bcc_completed, :completed_redirect_url, :reply_to, :go_to_last,
|
||||
: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, :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: {}, validation: {} }] }]]
|
||||
}
|
||||
]
|
||||
|
||||
if params.key?(:submitters)
|
||||
params.permit(*permitted_attrs)
|
||||
else
|
||||
key = params.key?(:submission) ? :submission : :submissions
|
||||
|
||||
params.permit(
|
||||
{ key => [permitted_attrs] }, { key => permitted_attrs }
|
||||
).fetch(key, [])
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -6,10 +6,10 @@ module Api
|
||||
skip_authorization_check
|
||||
|
||||
def create
|
||||
submitter = Submitter.find_by!(slug: params[:submitter_slug])
|
||||
@submitter = Submitter.find_by!(slug: params[:submitter_slug])
|
||||
|
||||
if params[:t] == SubmissionEvents.build_tracking_param(submitter, 'click_email')
|
||||
SubmissionEvents.create_with_tracking_data(submitter, 'click_email', request)
|
||||
if params[:t] == SubmissionEvents.build_tracking_param(@submitter, 'click_email')
|
||||
SubmissionEvents.create_with_tracking_data(@submitter, 'click_email', request)
|
||||
end
|
||||
|
||||
render json: {}
|
||||
|
||||
@@ -6,11 +6,14 @@ module Api
|
||||
skip_authorization_check
|
||||
|
||||
def create
|
||||
submitter = Submitter.find_by!(slug: params[:submitter_slug])
|
||||
@submitter = Submitter.find_by!(slug: params[:submitter_slug])
|
||||
|
||||
SubmissionEvents.create_with_tracking_data(submitter, 'view_form', request)
|
||||
@submitter.opened_at = Time.current
|
||||
@submitter.save
|
||||
|
||||
SendFormViewedWebhookRequestJob.perform_later(submitter)
|
||||
SubmissionEvents.create_with_tracking_data(@submitter, 'view_form', request)
|
||||
|
||||
WebhookUrls.enqueue_events(@submitter, 'form.viewed')
|
||||
|
||||
render json: {}
|
||||
end
|
||||
|
||||
@@ -1,35 +0,0 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Api
|
||||
class SubmittersAutocompleteController < ApiBaseController
|
||||
load_and_authorize_resource :submitter, parent: false
|
||||
|
||||
SELECT_COLUMNS = %w[email phone name].freeze
|
||||
LIMIT = 100
|
||||
|
||||
def index
|
||||
submitters = search_submitters(@submitters)
|
||||
|
||||
values = submitters.limit(LIMIT).group(SELECT_COLUMNS.join(', ')).pluck(SELECT_COLUMNS.join(', '))
|
||||
|
||||
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])
|
||||
column = Submitter.arel_table[params[:field].to_sym]
|
||||
|
||||
term = "%#{params[:q].downcase}%"
|
||||
|
||||
submitters.where(column.lower.matches(term))
|
||||
else
|
||||
Submitters.search(submitters, params[:q])
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -5,18 +5,21 @@ module Api
|
||||
load_and_authorize_resource :submitter
|
||||
|
||||
def index
|
||||
submitters = Submitters.search(@submitters, params[:q])
|
||||
submitters = Submitters.search(current_user, @submitters, params[:q])
|
||||
|
||||
submitters = submitters.where(application_key: params[:application_key]) if params[:application_key].present?
|
||||
submitters = submitters.where(submission_id: params[:submission_id]) if params[:submission_id].present?
|
||||
submitters = filter_submitters(submitters, params)
|
||||
|
||||
submitters = paginate(
|
||||
submitters.preload(:template, :submission, :submission_events,
|
||||
documents_attachments: :blob, attachments_attachments: :blob)
|
||||
)
|
||||
|
||||
expires_at = Accounts.link_expires_at(current_account)
|
||||
|
||||
render json: {
|
||||
data: submitters.map { |s| Submitters::SerializeForApi.call(s, with_template: true, with_events: true) },
|
||||
data: submitters.map do |s|
|
||||
Submitters::SerializeForApi.call(s, with_template: true, with_events: true, params:, expires_at:)
|
||||
end,
|
||||
pagination: {
|
||||
count: submitters.size,
|
||||
next: submitters.last&.id,
|
||||
@@ -28,7 +31,187 @@ module Api
|
||||
def show
|
||||
Submissions::EnsureResultGenerated.call(@submitter) if @submitter.completed_at?
|
||||
|
||||
render json: Submitters::SerializeForApi.call(@submitter, with_template: true, with_events: true)
|
||||
render json: Submitters::SerializeForApi.call(@submitter, with_template: true, with_events: true, params:)
|
||||
end
|
||||
|
||||
def update
|
||||
if @submitter.completed_at?
|
||||
return render json: { error: 'Submitter has already completed the submission.' }, status: :unprocessable_content
|
||||
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
|
||||
)
|
||||
|
||||
Submissions::CreateFromSubmitters.maybe_set_template_fields(submission, [normalized_params],
|
||||
default_submitter_uuid: @submitter.uuid)
|
||||
|
||||
assign_submitter_attrs(@submitter, normalized_params)
|
||||
|
||||
ApplicationRecord.transaction do
|
||||
Submissions::NormalizeParamUtils.save_default_value_attachments!(new_attachments, [@submitter])
|
||||
|
||||
@submitter.save!
|
||||
|
||||
@submitter.submission.save!
|
||||
|
||||
SubmissionEvents.create_with_tracking_data(@submitter, 'api_complete_form', request) if @submitter.completed_at?
|
||||
end
|
||||
|
||||
if @submitter.completed_at?
|
||||
ProcessSubmitterCompletionJob.perform_async('submitter_id' => @submitter.id)
|
||||
elsif normalized_params[:send_email] || normalized_params[:send_sms]
|
||||
Submitters.send_signature_requests([@submitter])
|
||||
end
|
||||
|
||||
SearchEntries.enqueue_reindex(@submitter)
|
||||
|
||||
render json: Submitters::SerializeForApi.call(@submitter, with_template: false, with_urls: true,
|
||||
with_events: false, params:)
|
||||
rescue Submitters::NormalizeValues::BaseError, DownloadUtils::UnableToDownload => e
|
||||
Rollbar.warning(e) if defined?(Rollbar)
|
||||
|
||||
render json: { error: e.message }, status: :unprocessable_content
|
||||
end
|
||||
|
||||
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, :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,
|
||||
{ default_value: [], value: [], preferences: {} }]] }
|
||||
)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def maybe_filder_by_completed_at(submitters, params)
|
||||
if params[:completed_after].present?
|
||||
submitters = submitters.where(completed_at: Time.zone.parse(params[:completed_after])..)
|
||||
end
|
||||
|
||||
if params[:completed_before].present?
|
||||
submitters = submitters.where(completed_at: ..Time.zone.parse(params[:completed_before]))
|
||||
end
|
||||
|
||||
submitters
|
||||
end
|
||||
|
||||
def assign_submitter_attrs(submitter, attrs)
|
||||
values = attrs[:values]&.to_unsafe_h || {}
|
||||
|
||||
assign_submission_fields(submitter.submission)
|
||||
|
||||
phone_field_uuid = submitter.submission.template_fields.find do |f|
|
||||
values[f['uuid']].present? && f['type'] == 'phone'
|
||||
end&.dig('uuid')
|
||||
|
||||
submitter.email = Submissions.normalize_email(attrs[:email]) if attrs.key?(:email)
|
||||
submitter.name = attrs[:name] if attrs.key?(:name)
|
||||
|
||||
if attrs.key?(:phone)
|
||||
submitter.phone = attrs[:phone].to_s.gsub(/[^0-9+]/, '')
|
||||
elsif values[phone_field_uuid].present?
|
||||
submitter.phone = values[phone_field_uuid].to_s.gsub(/[^0-9+]/, '')
|
||||
end
|
||||
|
||||
values = values.except(phone_field_uuid)
|
||||
|
||||
submitter.values = submitter.values.merge(values) if values.present?
|
||||
submitter.metadata = attrs[:metadata] if attrs.key?(:metadata)
|
||||
|
||||
maybe_assign_completed_attributes(submitter, attrs)
|
||||
|
||||
assign_external_id(submitter, attrs)
|
||||
assign_preferences(submitter, attrs)
|
||||
|
||||
submitter
|
||||
end
|
||||
|
||||
def maybe_assign_completed_attributes(submitter, attrs)
|
||||
submitter.completed_at = attrs[:completed] ? Time.current : submitter.completed_at
|
||||
|
||||
if attrs[:completed]
|
||||
submitter.values = Submitters::SubmitValues.merge_default_values(submitter)
|
||||
submitter.values = Submitters::SubmitValues.maybe_remove_condition_values(submitter)
|
||||
|
||||
formula_values = Submitters::SubmitValues.build_formula_values(submitter)
|
||||
|
||||
if formula_values.present?
|
||||
submitter.values = submitter.values.merge(formula_values)
|
||||
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
|
||||
end
|
||||
|
||||
submitter
|
||||
end
|
||||
|
||||
def filter_submitters(submitters, params)
|
||||
submitters = submitters.where(external_id: params[:application_key]) if params[:application_key].present?
|
||||
submitters = submitters.where(external_id: params[:external_id]) if params[:external_id].present?
|
||||
submitters = submitters.where(slug: params[:slug]) if params[:slug].present?
|
||||
submitters = submitters.where(submission_id: params[:submission_id]) if params[:submission_id].present?
|
||||
|
||||
if params[:template_id].present?
|
||||
submitters = submitters.joins(:submission).where(submissions: { template_id: params[:template_id] })
|
||||
end
|
||||
|
||||
maybe_filder_by_completed_at(submitters, params)
|
||||
end
|
||||
|
||||
def assign_external_id(submitter, attrs)
|
||||
submitter.external_id = attrs[:application_key] if attrs.key?(:application_key)
|
||||
submitter.external_id = attrs[:external_id] if attrs.key?(:external_id)
|
||||
|
||||
submitter
|
||||
end
|
||||
|
||||
def assign_submission_fields(submission)
|
||||
submission.template_fields ||= submission.template.fields
|
||||
submission.template_schema ||= submission.template.schema
|
||||
end
|
||||
|
||||
def assign_preferences(submitter, attrs)
|
||||
submitter_preferences = Submitters.normalize_preferences(submitter.account, current_user, attrs)
|
||||
|
||||
submitter.preferences['default_values'] = attrs[:values].to_unsafe_h if attrs[:values].present?
|
||||
|
||||
if submitter_preferences.key?('send_email')
|
||||
submitter.preferences['send_email'] = submitter_preferences['send_email']
|
||||
end
|
||||
|
||||
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?('go_to_last')
|
||||
submitter.preferences['go_to_last'] = submitter_preferences['go_to_last']
|
||||
end
|
||||
|
||||
if submitter_preferences.key?('completed_redirect_url')
|
||||
submitter.preferences['completed_redirect_url'] = submitter_preferences['completed_redirect_url']
|
||||
end
|
||||
|
||||
return unless submitter_preferences.key?('email_message_uuid')
|
||||
|
||||
submitter.preferences['email_message_uuid'] = submitter_preferences['email_message_uuid']
|
||||
|
||||
submitter
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,16 +0,0 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Api
|
||||
class TemplateFoldersAutocompleteController < ApiBaseController
|
||||
load_and_authorize_resource :template_folder, parent: false
|
||||
|
||||
LIMIT = 100
|
||||
|
||||
def index
|
||||
template_folders = @template_folders.joins(:templates).where(templates: { deleted_at: nil }).distinct
|
||||
template_folders = TemplateFolders.search(template_folders, params[:q]).limit(LIMIT)
|
||||
|
||||
render json: template_folders.as_json(only: %i[name deleted_at])
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,40 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Api
|
||||
class TemplatesCloneController < ApiBaseController
|
||||
load_and_authorize_resource :template
|
||||
|
||||
def create
|
||||
authorize!(:create, @template)
|
||||
|
||||
ActiveRecord::Associations::Preloader.new(
|
||||
records: [@template],
|
||||
associations: [schema_documents: :preview_images_attachments]
|
||||
).call
|
||||
|
||||
cloned_template = Templates::Clone.call(
|
||||
@template,
|
||||
author: current_user,
|
||||
name: params[:name],
|
||||
external_id: params[:external_id].presence || params[:application_key],
|
||||
folder_name: params[:folder_name]
|
||||
)
|
||||
|
||||
cloned_template.source = :api
|
||||
|
||||
schema_documents = Templates::CloneAttachments.call(template: cloned_template,
|
||||
original_template: @template,
|
||||
documents: params[:documents])
|
||||
|
||||
Templates.maybe_assign_access(cloned_template)
|
||||
|
||||
cloned_template.save!
|
||||
|
||||
WebhookUrls.enqueue_events(cloned_template, 'template.created')
|
||||
|
||||
SearchEntries.enqueue_reindex(cloned_template)
|
||||
|
||||
render json: Templates::SerializeForApi.call(cloned_template, schema_documents:)
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -5,14 +5,34 @@ module Api
|
||||
load_and_authorize_resource :template
|
||||
|
||||
def index
|
||||
templates = Templates.search(@templates, params[:q])
|
||||
templates = filter_templates(@templates, params)
|
||||
|
||||
templates = params[:archived] ? templates.archived : templates.active
|
||||
templates = paginate(templates.preload(:author, folder: :parent_folder))
|
||||
|
||||
templates = paginate(templates.preload(:author, documents_attachments: :blob))
|
||||
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)
|
||||
|
||||
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.as_json(serialize_params),
|
||||
data: templates.map do |t|
|
||||
Templates::SerializeForApi.call(t,
|
||||
schema_documents: schema_documents.select { |e| e.record_id == t.id },
|
||||
preview_image_attachments:,
|
||||
expires_at:)
|
||||
end,
|
||||
pagination: {
|
||||
count: templates.size,
|
||||
next: templates.last&.id,
|
||||
@@ -22,40 +42,89 @@ module Api
|
||||
end
|
||||
|
||||
def show
|
||||
render json: @template.as_json(serialize_params)
|
||||
render json: Templates::SerializeForApi.call(@template)
|
||||
end
|
||||
|
||||
def update
|
||||
if (folder_name = params.dig(:template, :folder_name))
|
||||
if (folder_name = params[:folder_name] || params.dig(:template, :folder_name))
|
||||
@template.folder = TemplateFolders.find_or_create_by_name(current_user, folder_name)
|
||||
end
|
||||
|
||||
Array.wrap(params[:roles].presence || params.dig(:template, :roles).presence).each_with_index do |role, index|
|
||||
if (item = @template.submitters[index])
|
||||
item['name'] = role
|
||||
else
|
||||
@template.submitters << { 'name' => role, 'uuid' => SecureRandom.uuid }
|
||||
end
|
||||
end
|
||||
|
||||
archived = params.key?(:archived) ? params[:archived] : params.dig(:template, :archived)
|
||||
|
||||
if archived.in?([true, false])
|
||||
@template.archived_at = archived == true ? Time.current : nil
|
||||
end
|
||||
|
||||
@template.update!(template_params)
|
||||
|
||||
SearchEntries.enqueue_reindex(@template)
|
||||
|
||||
WebhookUrls.enqueue_events(@template, 'template.updated')
|
||||
|
||||
render json: @template.as_json(only: %i[id updated_at])
|
||||
end
|
||||
|
||||
def destroy
|
||||
@template.update!(deleted_at: Time.current)
|
||||
if params[:permanently].in?(['true', true])
|
||||
@template.destroy!
|
||||
else
|
||||
@template.update!(archived_at: Time.current)
|
||||
end
|
||||
|
||||
render json: @template.as_json(only: %i[id deleted_at])
|
||||
render json: @template.as_json(only: %i[id archived_at])
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def serialize_params
|
||||
{
|
||||
include: { author: { only: %i[id email first_name last_name] },
|
||||
documents: { only: %i[id uuid], methods: %i[url filename] } }
|
||||
}
|
||||
def filter_templates(templates, params)
|
||||
templates = Templates.search(current_user, templates, params[:q])
|
||||
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?
|
||||
|
||||
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
|
||||
|
||||
def template_params
|
||||
params.require(:template).permit(:name,
|
||||
schema: [%i[attachment_uuid name]],
|
||||
submitters: [%i[name uuid]],
|
||||
fields: [[:uuid, :submitter_uuid, :name, :type, :required,
|
||||
{ options: [], areas: [%i[x y w h cell_w attachment_uuid page]] }]])
|
||||
permitted_params = [
|
||||
:name,
|
||||
:external_id,
|
||||
:shared_link,
|
||||
{
|
||||
submitters: [%i[name uuid is_requester invite_by_uuid optional_invite_by_uuid linked_to_uuid email order]],
|
||||
fields: [[:uuid, :submitter_uuid, :name, :type,
|
||||
:required, :readonly, :default_value,
|
||||
:title, :description, :prefillable,
|
||||
{ preferences: {},
|
||||
default_value: [],
|
||||
conditions: [%i[field_uuid value action operation]],
|
||||
options: [%i[value uuid]],
|
||||
validation: %i[message pattern min max step],
|
||||
areas: [%i[x y w h cell_w attachment_uuid option_uuid page]] }]]
|
||||
}
|
||||
]
|
||||
|
||||
if params.key?(:template)
|
||||
params.require(:template).permit(permitted_params)
|
||||
else
|
||||
params.permit(permitted_params)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,27 +0,0 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Api
|
||||
class TemplatesDocumentsController < ApiBaseController
|
||||
load_and_authorize_resource :template
|
||||
|
||||
def create
|
||||
return head :unprocessable_entity if params[:blobs].blank? && params[:files].blank?
|
||||
|
||||
documents = Templates::CreateAttachments.call(@template, params)
|
||||
|
||||
schema = documents.map do |doc|
|
||||
{ attachment_uuid: doc.uuid, name: doc.filename.base }
|
||||
end
|
||||
|
||||
render json: {
|
||||
schema:,
|
||||
documents: documents.as_json(
|
||||
methods: [:metadata],
|
||||
include: {
|
||||
preview_images: { methods: %i[url metadata filename] }
|
||||
}
|
||||
)
|
||||
}
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,41 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Api
|
||||
class ToolsController < ApiBaseController
|
||||
skip_authorization_check
|
||||
|
||||
def merge
|
||||
files = params[:files] || []
|
||||
|
||||
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)
|
||||
}
|
||||
end
|
||||
|
||||
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|
|
||||
{
|
||||
verification_result: sig.verify(trusted_certs:).messages,
|
||||
signer_name: sig.signer_name,
|
||||
signing_reason: sig.signing_reason,
|
||||
signing_time: sig.signing_time,
|
||||
signature_type: sig.signature_type
|
||||
}
|
||||
end
|
||||
}
|
||||
rescue HexaPDF::MalformedPDFError
|
||||
render json: { error: 'Malformed PDF' }, status: :unprocessable_content
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,11 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Api
|
||||
class UsersController < ApiBaseController
|
||||
authorize_resource :current_user
|
||||
|
||||
def show
|
||||
render json: current_user.as_json(only: %i[id first_name last_name email])
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -4,4 +4,14 @@ class ApiSettingsController < ApplicationController
|
||||
def index
|
||||
authorize!(:read, current_user.access_token)
|
||||
end
|
||||
|
||||
def create
|
||||
authorize!(:manage, current_user.access_token)
|
||||
|
||||
current_user.access_token.token = SecureRandom.base58(AccessToken::TOKEN_LENGTH)
|
||||
|
||||
current_user.access_token.save!
|
||||
|
||||
redirect_back(fallback_location: settings_api_index_path, notice: I18n.t('api_token_has_been_updated'))
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,37 +1,100 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class ApplicationController < ActionController::Base
|
||||
BROWSER_LOCALE_REGEXP = /\A\w{2}(?:-\w{2})?/
|
||||
|
||||
include ActiveStorage::SetCurrent
|
||||
include Pagy::Backend
|
||||
|
||||
check_authorization unless: :devise_controller?
|
||||
|
||||
around_action :with_locale
|
||||
before_action :sign_in_for_demo, if: -> { Docuseal.demo? }
|
||||
before_action :maybe_redirect_to_setup, unless: :signed_in?
|
||||
before_action :authenticate_user!, unless: :devise_controller?
|
||||
|
||||
before_action :set_csp, if: -> { request.get? && !request.headers['HTTP_X_TURBO'] }
|
||||
|
||||
helper_method :button_title,
|
||||
:current_account,
|
||||
:true_ability,
|
||||
:form_link_host,
|
||||
:svg_icon
|
||||
|
||||
impersonates :user, with: ->(uuid) { User.find_by(uuid:) }
|
||||
|
||||
rescue_from Pagy::OverflowError do
|
||||
redirect_to request.path
|
||||
end
|
||||
|
||||
if Rails.env.production?
|
||||
rescue_from CanCan::AccessDenied do |e|
|
||||
Rollbar.error(e) if defined?(Rollbar)
|
||||
rescue_from RateLimit::LimitApproached do |e|
|
||||
Rollbar.error(e) if defined?(Rollbar)
|
||||
|
||||
redirect_back(fallback_location: root_path, alert: e.message)
|
||||
redirect_to request.referer, alert: 'Too many requests', status: :too_many_requests
|
||||
end
|
||||
|
||||
if Rails.env.production? || Rails.env.test?
|
||||
rescue_from CanCan::AccessDenied do |e|
|
||||
Rollbar.warning(e) if defined?(Rollbar)
|
||||
|
||||
redirect_to root_path, alert: e.message
|
||||
end
|
||||
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
|
||||
|
||||
def impersonate_user(user)
|
||||
raise ArgumentError unless user
|
||||
raise Pretender::Error unless true_user
|
||||
|
||||
@impersonated_user = user
|
||||
|
||||
request.session[:impersonated_user_id] = user.uuid
|
||||
end
|
||||
|
||||
def pagy_auto(collection, **keyword_args)
|
||||
if current_ability.can?(:manage, :countless)
|
||||
pagy_countless(collection, **keyword_args)
|
||||
else
|
||||
pagy(collection, **keyword_args)
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def with_locale(&)
|
||||
return yield unless current_account
|
||||
|
||||
locale = params[:lang].presence if Rails.env.development?
|
||||
locale ||= current_account.locale
|
||||
|
||||
I18n.with_locale(locale, &)
|
||||
end
|
||||
|
||||
def with_browser_locale(&)
|
||||
return yield if I18n.locale != :'en-US' && I18n.locale != :en
|
||||
|
||||
locale = params[:lang].presence
|
||||
locale ||= request.env['HTTP_ACCEPT_LANGUAGE'].to_s[BROWSER_LOCALE_REGEXP].to_s
|
||||
|
||||
locale =
|
||||
if locale.starts_with?('en-') && locale != 'en-US'
|
||||
'en-GB'
|
||||
else
|
||||
locale.split('-').first.presence || 'en-GB'
|
||||
end
|
||||
|
||||
locale = 'en-GB' unless I18n.locale_available?(locale)
|
||||
|
||||
I18n.with_locale(locale, &)
|
||||
end
|
||||
|
||||
def sign_in_for_demo
|
||||
sign_in(User.active.order('random()').take) unless signed_in?
|
||||
end
|
||||
@@ -40,15 +103,48 @@ 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
|
||||
|
||||
def button_title(title: 'Submit', disabled_with: 'Submitting', icon: nil, icon_disabled: nil)
|
||||
render_to_string(partial: 'shared/button_title', locals: { title:, disabled_with:, icon:, icon_disabled: })
|
||||
def button_title(title: I18n.t('submit'), disabled_with: I18n.t('submitting'), title_class: '', icon: nil,
|
||||
icon_disabled: nil)
|
||||
render_to_string(partial: 'shared/button_title',
|
||||
locals: { title:, disabled_with:, title_class:, icon:, icon_disabled: })
|
||||
end
|
||||
|
||||
def svg_icon(icon_name, class: '')
|
||||
render_to_string(partial: "icons/#{icon_name}", locals: { class: })
|
||||
end
|
||||
|
||||
def form_link_host
|
||||
Docuseal.default_url_options[:host]
|
||||
end
|
||||
|
||||
def maybe_redirect_com
|
||||
return if request.domain != 'docuseal.co'
|
||||
|
||||
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
|
||||
|
||||
@@ -5,14 +5,22 @@ class ConsoleRedirectController < ApplicationController
|
||||
skip_authorization_check
|
||||
|
||||
def index
|
||||
return redirect_to(new_user_session_path({ redir: params[:redir] }.compact)) if current_user.blank?
|
||||
if request.path == '/upgrade'
|
||||
params[:redir] = Docuseal.multitenant? ? "#{Docuseal::CONSOLE_URL}/plans" : "#{Docuseal::CONSOLE_URL}/on_premises"
|
||||
end
|
||||
|
||||
auth = JsonWebToken.encode(uuid: current_user.uuid,
|
||||
params[:redir] = "#{Docuseal::CONSOLE_URL}/manage" if request.path == '/manage'
|
||||
|
||||
return redirect_to(new_user_session_path({ redir: params[:redir] }.compact)) if true_user.blank?
|
||||
|
||||
auth = JsonWebToken.encode(uuid: true_user.uuid,
|
||||
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
|
||||
|
||||
@@ -5,63 +5,37 @@ class DashboardController < ApplicationController
|
||||
|
||||
before_action :maybe_redirect_product_url
|
||||
before_action :maybe_render_landing
|
||||
before_action :maybe_redirect_mfa_setup
|
||||
|
||||
load_and_authorize_resource :template_folder, parent: false
|
||||
load_and_authorize_resource :template, parent: false
|
||||
|
||||
SHOW_TEMPLATES_FOLDERS_THRESHOLD = 9
|
||||
TEMPLATES_PER_PAGE = 12
|
||||
FOLDERS_PER_PAGE = 18
|
||||
skip_authorization_check
|
||||
|
||||
def index
|
||||
@template_folders = filter_template_folders(@template_folders)
|
||||
|
||||
@pagy, @template_folders = pagy(
|
||||
@template_folders,
|
||||
items: FOLDERS_PER_PAGE,
|
||||
page: @template_folders.count > SHOW_TEMPLATES_FOLDERS_THRESHOLD ? params[:page] : 1
|
||||
)
|
||||
|
||||
if @pagy.count > SHOW_TEMPLATES_FOLDERS_THRESHOLD
|
||||
@templates = @templates.none
|
||||
if cookies.permanent[:dashboard_view] == 'submissions'
|
||||
SubmissionsDashboardController.dispatch(:index, request, response)
|
||||
else
|
||||
@template_folders = @template_folders.reject { |e| e.name == TemplateFolder::DEFAULT_NAME }
|
||||
@templates = filter_templates(@templates)
|
||||
|
||||
items =
|
||||
if @template_folders.size < 4
|
||||
TEMPLATES_PER_PAGE
|
||||
else
|
||||
(@template_folders.size < 7 ? 9 : 6)
|
||||
end
|
||||
|
||||
@pagy, @templates = pagy(@templates, items:)
|
||||
TemplatesDashboardController.dispatch(:index, request, response)
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def filter_template_folders(template_folders)
|
||||
rel = template_folders.joins(:active_templates)
|
||||
.order(id: :desc)
|
||||
.distinct
|
||||
|
||||
TemplateFolders.search(rel, params[:q])
|
||||
end
|
||||
|
||||
def filter_templates(templates)
|
||||
rel = templates.active.preload(:author).order(id: :desc)
|
||||
rel = rel.where(folder_id: current_account.default_template_folder.id) if params[:q].blank?
|
||||
|
||||
Templates.search(rel, params[:q])
|
||||
end
|
||||
|
||||
def maybe_redirect_product_url
|
||||
return if !Docuseal.multitenant? || signed_in?
|
||||
|
||||
redirect_to Docuseal::PRODUCT_URL, allow_other_host: true
|
||||
end
|
||||
|
||||
def maybe_redirect_mfa_setup
|
||||
return unless signed_in?
|
||||
return if current_user.otp_required_for_login
|
||||
|
||||
return if !current_user.otp_required_for_login && !AccountConfig.exists?(value: true,
|
||||
account_id: current_user.account_id,
|
||||
key: AccountConfig::FORCE_MFA)
|
||||
|
||||
redirect_to mfa_setup_path, notice: I18n.t('setup_2fa_to_continue')
|
||||
end
|
||||
|
||||
def maybe_render_landing
|
||||
return if signed_in?
|
||||
|
||||
|
||||
@@ -9,16 +9,18 @@ class EmailSmtpSettingsController < ApplicationController
|
||||
|
||||
def create
|
||||
if @encrypted_config.update(email_configs)
|
||||
SettingsMailer.smtp_successful_setup(@encrypted_config.value['from_email']).deliver_now!
|
||||
unless Docuseal.multitenant?
|
||||
SettingsMailer.smtp_successful_setup(@encrypted_config.value['from_email'] || current_user.email).deliver_now!
|
||||
end
|
||||
|
||||
redirect_to settings_email_index_path, notice: 'Changes have been saved'
|
||||
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
|
||||
|
||||
private
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class EmbedScriptsController < ActionController::Metal
|
||||
DUMMY_SCRIPT = <<~JAVASCRIPT.freeze
|
||||
const DummyBuilder = class extends HTMLElement {
|
||||
connectedCallback() {
|
||||
this.innerHTML = `
|
||||
<div style="text-align: center; padding: 20px; font-family: Arial, sans-serif;">
|
||||
<h2>Upgrade to Pro</h2>
|
||||
<p>Unlock embedded components by upgrading to Pro</p>
|
||||
<div style="margin-top: 40px;">
|
||||
<a href="#{Docuseal::CONSOLE_URL}/on_premises" target="_blank" style="padding: 15px 25px; background-color: #222; color: white; text-decoration: none; border-radius: 5px; font-size: 16px; cursor: pointer;">
|
||||
Learn More
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
};
|
||||
|
||||
const DummyForm = class extends DummyBuilder {};
|
||||
|
||||
if (!window.customElements.get('docuseal-builder')) {
|
||||
window.customElements.define('docuseal-builder', DummyBuilder);
|
||||
}
|
||||
|
||||
if (!window.customElements.get('docuseal-form')) {
|
||||
window.customElements.define('docuseal-form', DummyForm);
|
||||
}
|
||||
JAVASCRIPT
|
||||
|
||||
def show
|
||||
headers['Content-Type'] = 'application/javascript'
|
||||
|
||||
self.response_body = DUMMY_SCRIPT
|
||||
|
||||
self.status = 200
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,11 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class EncryptedUserConfigsController < ApplicationController
|
||||
load_and_authorize_resource :encrypted_user_config
|
||||
|
||||
def destroy
|
||||
@encrypted_user_config.destroy!
|
||||
|
||||
redirect_back(fallback_location: root_path)
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,62 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class ErrorsController < ActionController::Base
|
||||
ENTERPRISE_FEATURE_MESSAGE =
|
||||
'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',
|
||||
'/api/templates/doc',
|
||||
'/templates/docx',
|
||||
'/api/templates/docx'
|
||||
].freeze
|
||||
|
||||
SAFE_ERROR_MESSAGE_CLASSES = [
|
||||
ActionDispatch::Http::Parameters::ParseError,
|
||||
JSON::ParserError
|
||||
].freeze
|
||||
|
||||
def show
|
||||
if request.original_fullpath.in?(ENTERPRISE_PATHS) && error_status_code == 404
|
||||
return render json: { status: 404, message: ENTERPRISE_FEATURE_MESSAGE }, status: :not_found
|
||||
end
|
||||
|
||||
respond_to do |f|
|
||||
f.json do
|
||||
set_cors_headers
|
||||
|
||||
exception = request.env['action_dispatch.exception']
|
||||
|
||||
error = exception.message if exception.class.in?(SAFE_ERROR_MESSAGE_CLASSES)
|
||||
|
||||
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 }
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def set_cors_headers
|
||||
headers['Access-Control-Allow-Origin'] = '*'
|
||||
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
|
||||
end
|
||||
|
||||
def error_status_code
|
||||
@error_status_code ||=
|
||||
ActionDispatch::ExceptionWrapper.new(request.env,
|
||||
request.env['action_dispatch.exception']).status_code
|
||||
end
|
||||
end
|
||||
@@ -11,6 +11,8 @@ class EsignSettingsController < ApplicationController
|
||||
end
|
||||
end
|
||||
|
||||
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]
|
||||
@@ -21,9 +23,9 @@ class EsignSettingsController < ApplicationController
|
||||
default_pkcs = GenerateCertificate.load_pkcs(cert_data) if cert_data['cert'].present?
|
||||
|
||||
custom_pkcs_list = (cert_data['custom'] || []).map do |e|
|
||||
{ 'pkcs' => OpenSSL::PKCS12.new(Base64.urlsafe_decode64(e['data']), e['password'].to_s),
|
||||
'name' => e['name'],
|
||||
'status' => e['status'] }
|
||||
pkcs = e['data'].present? ? OpenSSL::PKCS12.new(Base64.urlsafe_decode64(e['data']), e['password'].to_s) : nil
|
||||
|
||||
{ 'pkcs' => pkcs, 'name' => e['name'], 'status' => e['status'] }
|
||||
end
|
||||
|
||||
@pkcs_list = [
|
||||
@@ -48,30 +50,38 @@ class EsignSettingsController < ApplicationController
|
||||
if (@encrypted_config.value && @encrypted_config.value['custom']&.any? { |e| e['name'] == @cert_record.name }) ||
|
||||
@cert_record.name == DEFAULT_CERT_NAME
|
||||
|
||||
@cert_record.errors.add(:name, 'already exists')
|
||||
@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)
|
||||
|
||||
redirect_to settings_esign_path, notice: 'Certificate has been successfully added!'
|
||||
rescue OpenSSL::PKCS12::PKCS12Error
|
||||
@cert_record.errors.add(:password, "is invalid. Make sure you're uploading a valid .p12 file")
|
||||
redirect_to settings_esign_path, notice: I18n.t('certificate_has_been_successfully_added')
|
||||
rescue OpenSSL::PKCS12::PKCS12Error => e
|
||||
Rollbar.error(e) if defined?(Rollbar)
|
||||
|
||||
render turbo_stream: turbo_stream.replace(:modal, template: 'esign_settings/new'), status: :unprocessable_entity
|
||||
@cert_record.errors.add(:password, e.message)
|
||||
|
||||
render turbo_stream: turbo_stream.replace(:modal, template: 'esign_settings/new'), status: :unprocessable_content
|
||||
end
|
||||
|
||||
def update
|
||||
@encrypted_config.value['custom'].each { |e| e['status'] = 'validate' }
|
||||
@encrypted_config.value['custom'].to_a.each { |e| e['status'] = 'validate' }
|
||||
|
||||
custom_cert_data = @encrypted_config.value['custom'].find { |e| e['name'] == params[:name] }
|
||||
custom_cert_data['status'] = 'default' if custom_cert_data
|
||||
custom_cert_data = @encrypted_config.value['custom'].to_a.find { |e| e['name'] == params[:name] }
|
||||
|
||||
if custom_cert_data
|
||||
custom_cert_data['status'] = 'default'
|
||||
elsif params[:name] == Docuseal::AATL_CERT_NAME
|
||||
@encrypted_config.value['custom'] ||= []
|
||||
@encrypted_config.value['custom'] << { 'name' => params[:name], 'status' => 'default' }
|
||||
end
|
||||
|
||||
@encrypted_config.save!
|
||||
|
||||
redirect_to settings_esign_path, notice: 'Default certificate has been selected'
|
||||
redirect_to settings_esign_path, notice: I18n.t('default_certificate_has_been_selected')
|
||||
end
|
||||
|
||||
def destroy
|
||||
@@ -79,7 +89,7 @@ class EsignSettingsController < ApplicationController
|
||||
|
||||
@encrypted_config.save!
|
||||
|
||||
redirect_to settings_esign_path, notice: 'Certificate has been removed'
|
||||
redirect_to settings_esign_path, notice: I18n.t('certificate_has_been_removed')
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -5,13 +5,11 @@ class MfaSetupController < ApplicationController
|
||||
authorize!(:update, current_user)
|
||||
end
|
||||
|
||||
def new
|
||||
current_user.otp_secret ||= User.generate_otp_secret
|
||||
before_action :set_provision_url, only: %i[show new]
|
||||
|
||||
current_user.save!
|
||||
def show; end
|
||||
|
||||
@provision_url = current_user.otp_provisioning_uri(current_user.email, issuer: Docuseal.product_name)
|
||||
end
|
||||
def new; end
|
||||
|
||||
def edit; end
|
||||
|
||||
@@ -20,13 +18,13 @@ class MfaSetupController < ApplicationController
|
||||
current_user.otp_required_for_login = true
|
||||
current_user.save!
|
||||
|
||||
redirect_to settings_profile_index_path, notice: '2FA has been configured'
|
||||
redirect_to settings_profile_index_path, notice: I18n.t('2fa_has_been_configured')
|
||||
else
|
||||
@provision_url = current_user.otp_provisioning_uri(current_user.email, issuer: Docuseal.product_name)
|
||||
|
||||
@error_message = 'Code is invalid'
|
||||
@error_message = I18n.t('code_is_invalid')
|
||||
|
||||
render turbo_stream: turbo_stream.replace(:modal, template: 'mfa_setup/new'), status: :unprocessable_entity
|
||||
render turbo_stream: turbo_stream.replace(:mfa_form, partial: 'mfa_setup/form'), status: :unprocessable_content
|
||||
end
|
||||
end
|
||||
|
||||
@@ -34,11 +32,23 @@ class MfaSetupController < ApplicationController
|
||||
if 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: '2FA has been removed'
|
||||
redirect_to settings_profile_index_path, notice: I18n.t('2fa_has_been_removed')
|
||||
else
|
||||
@error_message = 'Code is invalid'
|
||||
@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
|
||||
|
||||
private
|
||||
|
||||
def set_provision_url
|
||||
return redirect_to root_path, alert: I18n.t('2fa_has_been_set_up_already') if current_user.otp_required_for_login
|
||||
|
||||
current_user.otp_secret ||= User.generate_otp_secret
|
||||
|
||||
current_user.save!
|
||||
|
||||
@provision_url = current_user.otp_provisioning_uri(current_user.email, issuer: Docuseal.product_name)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -12,10 +12,10 @@ class NotificationsSettingsController < ApplicationController
|
||||
def index; end
|
||||
|
||||
def create
|
||||
if @account_config.save
|
||||
redirect_back fallback_location: settings_notifications_path, notice: 'Changes have been saved'
|
||||
if @account_config.value.present? ? @account_config.save : @account_config.delete
|
||||
redirect_back fallback_location: settings_notifications_path, notice: I18n.t('changes_have_been_saved')
|
||||
else
|
||||
redirect_back fallback_location: settings_notifications_path, alert: 'Unable to save'
|
||||
redirect_back fallback_location: settings_notifications_path, alert: I18n.t('unable_to_save')
|
||||
end
|
||||
end
|
||||
|
||||
@@ -35,12 +35,12 @@ class NotificationsSettingsController < ApplicationController
|
||||
|
||||
def load_reminder_config
|
||||
@reminder_config =
|
||||
AccountConfig.find_or_initialize_by(account: current_account, key: AccountConfig::SUBMITTER_REMAILERS)
|
||||
AccountConfig.find_or_initialize_by(account: current_account, key: AccountConfig::SUBMITTER_REMINDERS)
|
||||
end
|
||||
|
||||
def email_config_params
|
||||
params.require(:account_config).permit!.tap do |attrs|
|
||||
attrs[:key] = nil unless attrs[:key].in?([AccountConfig::BCC_EMAILS, AccountConfig::SUBMITTER_REMAILERS])
|
||||
params.require(:account_config).permit(:key, :value, { value: {} }, { value: [] }).tap do |attrs|
|
||||
attrs[:key] = nil unless attrs[:key].in?([AccountConfig::BCC_EMAILS, AccountConfig::SUBMITTER_REMINDERS])
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,16 +0,0 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class OmniauthCallbacksController < Devise::OmniauthCallbacksController
|
||||
def google_oauth2
|
||||
@user = Users.from_omniauth(request.env['omniauth.auth'])
|
||||
|
||||
if @user.persisted?
|
||||
flash[:notice] = I18n.t('devise.omniauth_callbacks.success', kind: 'Google')
|
||||
|
||||
sign_in_and_redirect @user, event: :authentication
|
||||
else
|
||||
redirect_to new_registration_path(oauth_callback: true, user: @user.slice(:email, :first_name, :last_name)),
|
||||
notice: 'Please complete registration with Google auth'
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,13 +1,31 @@
|
||||
# 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
|
||||
|
||||
def create
|
||||
super do |resource|
|
||||
resource.errors.clear unless Docuseal.multitenant?
|
||||
end
|
||||
end
|
||||
|
||||
def update
|
||||
super do |resource|
|
||||
Current.user = resource
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def after_resetting_password_path_for(_)
|
||||
new_session_path(resource_name)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,24 +1,68 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
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,
|
||||
*(Docuseal.multitenant? ? [] : [AccountConfig::POLICY_LINKS_KEY])
|
||||
].freeze
|
||||
|
||||
InvalidKey = Class.new(StandardError)
|
||||
|
||||
before_action :load_and_authorize_account_config, only: :create
|
||||
|
||||
def show
|
||||
authorize!(:read, AccountConfig)
|
||||
end
|
||||
|
||||
def create
|
||||
account_config =
|
||||
current_account.account_configs.find_or_initialize_by(key: encrypted_config_params[:key])
|
||||
if @account_config.value.is_a?(Hash)
|
||||
@account_config.value = @account_config.value.reject do |_, v|
|
||||
v.blank? && v != false
|
||||
end
|
||||
end
|
||||
|
||||
authorize!(:create, account_config)
|
||||
if @account_config.value != false && @account_config.value.blank?
|
||||
@account_config.destroy!
|
||||
else
|
||||
@account_config.save!
|
||||
end
|
||||
|
||||
account_config.update!(encrypted_config_params)
|
||||
|
||||
redirect_back(fallback_location: settings_personalization_path, notice: 'Settings have been saved.')
|
||||
redirect_back(fallback_location: settings_personalization_path, notice: I18n.t('settings_have_been_saved'))
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def encrypted_config_params
|
||||
params.require(:account_config).permit!
|
||||
def load_and_authorize_account_config
|
||||
@account_config =
|
||||
current_account.account_configs.find_or_initialize_by(key: account_config_params[:key])
|
||||
|
||||
@account_config.assign_attributes(account_config_params)
|
||||
|
||||
authorize!(:create, @account_config)
|
||||
|
||||
raise InvalidKey unless ALLOWED_KEYS.include?(@account_config.key)
|
||||
|
||||
@account_config
|
||||
end
|
||||
|
||||
def account_config_params
|
||||
attrs = params.require(:account_config).permit(:key, :value, { value: {} }, { value: [] })
|
||||
|
||||
return attrs if attrs[:value].is_a?(String)
|
||||
|
||||
attrs[:value]&.transform_values! do |value|
|
||||
if value.in?(%w[true false])
|
||||
value == 'true'
|
||||
else
|
||||
value
|
||||
end
|
||||
end
|
||||
|
||||
attrs
|
||||
end
|
||||
end
|
||||
|
||||
@@ -6,11 +6,16 @@ class PreviewDocumentPageController < ActionController::API
|
||||
FORMAT = Templates::ProcessDocument::FORMAT
|
||||
|
||||
def show
|
||||
attachment = ActiveStorage::Attachment.find_by(uuid: params[:attachment_uuid])
|
||||
attachment_uuid = ApplicationRecord.signed_id_verifier.verified(params[:signed_uuid], purpose: :attachment)
|
||||
|
||||
attachment = ActiveStorage::Attachment.find_by(uuid: attachment_uuid) if attachment_uuid
|
||||
|
||||
return head :not_found unless attachment
|
||||
|
||||
preview_image = attachment.preview_images.joins(:blob).find_by(blob: { filename: "#{params[:id]}#{FORMAT}" })
|
||||
@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
|
||||
|
||||
@@ -21,9 +26,10 @@ class PreviewDocumentPageController < ActionController::API
|
||||
find_or_create_document_tempfile_path(attachment)
|
||||
end
|
||||
|
||||
io = Templates::ProcessDocument.generate_pdf_preview_from_file(attachment, file_path, params[:id].to_i)
|
||||
preview_image =
|
||||
Templates::ProcessDocument.generate_pdf_preview_from_file(attachment, file_path, params[:id].to_i)
|
||||
|
||||
render plain: io.tap(&:rewind).read
|
||||
redirect_to preview_image.url, allow_other_host: true
|
||||
end
|
||||
|
||||
def find_or_create_document_tempfile_path(attachment)
|
||||
|
||||
@@ -9,18 +9,25 @@ class ProfileController < ApplicationController
|
||||
|
||||
def update_contact
|
||||
if current_user.update(contact_params)
|
||||
redirect_to settings_profile_index_path, notice: 'Contact information has been updated'
|
||||
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: 'Password has been changed'
|
||||
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,4 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class PwaController < ActionController::Base
|
||||
end
|
||||
@@ -1,62 +0,0 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class RegistrationsController < Devise::RegistrationsController
|
||||
prepend_before_action :require_no_authentication, only: [:show]
|
||||
prepend_before_action :maybe_redirect_if_signed_in, only: [:show]
|
||||
|
||||
def show; end
|
||||
|
||||
def create
|
||||
super
|
||||
|
||||
Accounts.create_default_template(resource.account) if resource.account.persisted?
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def after_sign_up_path_for(...)
|
||||
if params[:redir].present?
|
||||
return console_redirect_index_path(redir: params[:redir]) if params[:redir].starts_with?(Docuseal::CONSOLE_URL)
|
||||
|
||||
return params[:redir]
|
||||
end
|
||||
|
||||
super
|
||||
end
|
||||
|
||||
def maybe_redirect_if_signed_in
|
||||
return unless signed_in?
|
||||
return if params[:redir].blank?
|
||||
|
||||
redirect_to after_sign_up_path_for(current_user), allow_other_host: true
|
||||
end
|
||||
|
||||
def set_flash_message(key, kind, options = {})
|
||||
return if key == :alert && kind == 'already_authenticated'
|
||||
|
||||
super
|
||||
end
|
||||
|
||||
def build_resource(_hash = {})
|
||||
account = Account.new(account_params)
|
||||
account.timezone = Accounts.normalize_timezone(account.timezone)
|
||||
|
||||
self.resource = account.users.new(user_params)
|
||||
|
||||
account.name ||= resource.full_name if params[:action] == 'create'
|
||||
end
|
||||
|
||||
def user_params
|
||||
return {} if params[:user].blank?
|
||||
|
||||
params.require(:user).permit(:first_name, :last_name, :email, :password).compact_blank.tap do |attrs|
|
||||
attrs[:password] ||= SecureRandom.hex if params[:action] == 'create'
|
||||
end
|
||||
end
|
||||
|
||||
def account_params
|
||||
return {} if params[:account].blank?
|
||||
|
||||
params.require(:account).permit(:name, :timezone).compact_blank
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,21 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class RevealAccessTokenController < ApplicationController
|
||||
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,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
|
||||
@@ -7,22 +7,43 @@ class SendSubmissionEmailController < ApplicationController
|
||||
skip_before_action :verify_authenticity_token
|
||||
skip_authorization_check
|
||||
|
||||
def success; end
|
||||
SEND_DURATION = 30.minutes
|
||||
|
||||
def create
|
||||
@submitter =
|
||||
if params[:template_slug]
|
||||
Submitter.joins(submission: :template).find_by!(email: params[:email],
|
||||
template: { slug: params[:template_slug] })
|
||||
else
|
||||
Submitter.find_by!(slug: params[:submitter_slug])
|
||||
if 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]
|
||||
submission = Submission.find_by(slug: params[:submission_slug])
|
||||
|
||||
if submission
|
||||
@submitter = Submitter.completed.find_by(submission: submission, email: params[:email].to_s.downcase)
|
||||
end
|
||||
|
||||
SubmitterMailer.documents_copy_email(@submitter).deliver_later!
|
||||
return redirect_to submissions_preview_completed_path(params[:submission_slug], status: :error) unless @submitter
|
||||
else
|
||||
@submitter = Submitter.completed.find_by!(slug: params[:submitter_slug])
|
||||
end
|
||||
|
||||
RateLimit.call("send-email-#{@submitter.id}", limit: 2, ttl: 5.minutes)
|
||||
|
||||
SubmitterMailer.documents_copy_email(@submitter, sig: true).deliver_later! if can_send?(@submitter)
|
||||
|
||||
respond_to do |f|
|
||||
f.html { redirect_to success_send_submission_email_index_path }
|
||||
f.html { render :success }
|
||||
f.json { head :ok }
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
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,14 +3,20 @@
|
||||
class SessionsController < Devise::SessionsController
|
||||
before_action :configure_permitted_parameters
|
||||
|
||||
around_action :with_browser_locale
|
||||
|
||||
def create
|
||||
if Docuseal.multitenant? && !User.exists?(email: sign_in_params[:email])
|
||||
email = sign_in_params[:email].to_s.downcase
|
||||
|
||||
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: 'Create a new account'
|
||||
notice: I18n.t('create_a_new_account')
|
||||
end
|
||||
|
||||
if User.exists?(email: sign_in_params[: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
|
||||
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_content
|
||||
end
|
||||
|
||||
super
|
||||
|
||||
@@ -21,12 +21,12 @@ class SetupController < ApplicationController
|
||||
@encrypted_config = EncryptedConfig.new(encrypted_config_params)
|
||||
|
||||
unless URI.parse(encrypted_config_params[:value].to_s).class.in?([URI::HTTP, URI::HTTPS])
|
||||
@encrypted_config.errors.add(:value, 'should be a valid URL')
|
||||
@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
|
||||
|
||||
@@ -56,7 +57,7 @@ class SetupController < ApplicationController
|
||||
def account_params
|
||||
return {} unless params[:account]
|
||||
|
||||
params.require(:account).permit(:name, :timezone)
|
||||
params.require(:account).permit(:name, :timezone, :locale)
|
||||
end
|
||||
|
||||
def encrypted_config_params
|
||||
@@ -66,10 +67,10 @@ class SetupController < ApplicationController
|
||||
end
|
||||
|
||||
def redirect_to_root_if_signed
|
||||
redirect_to root_path, notice: 'You are already signed in'
|
||||
redirect_to root_path, notice: I18n.t('you_are_already_signed_in')
|
||||
end
|
||||
|
||||
def ensure_first_user_not_created!
|
||||
redirect_to new_user_session_path, notice: 'Please sign in.' if User.exists?
|
||||
redirect_to new_user_session_path, notice: I18n.t('please_sign_in') if User.exists?
|
||||
end
|
||||
end
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
class SsoSettingsController < ApplicationController
|
||||
before_action :load_encrypted_config
|
||||
authorize_resource :encrypted_config, only: :index
|
||||
authorize_resource :encrypted_config, parent: false, except: :index
|
||||
|
||||
def index; end
|
||||
|
||||
|
||||
@@ -6,55 +6,239 @@ class StartFormController < ApplicationController
|
||||
skip_before_action :authenticate_user!
|
||||
skip_authorization_check
|
||||
|
||||
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
|
||||
|
||||
COOKIES_TTL = 12.hours
|
||||
COOKIES_DEFAULTS = { httponly: true, secure: Rails.env.production? }.freeze
|
||||
|
||||
def show
|
||||
@submitter = @template.submissions.new.submitters.new(uuid: @template.submitters.first['uuid'])
|
||||
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'])
|
||||
render :email_verification if params[:email_verification]
|
||||
else
|
||||
Rollbar.warning("Not shared template: #{@template.id}") if defined?(Rollbar)
|
||||
|
||||
return render :private if current_user && current_ability.can?(:read, @template)
|
||||
|
||||
raise ActionController::RoutingError, I18n.t('not_found')
|
||||
end
|
||||
end
|
||||
|
||||
def update
|
||||
@submitter = Submitter.where(submission: @template.submissions.where(deleted_at: nil))
|
||||
.then { |rel| params[:resubmit].present? ? rel.where(completed_at: nil) : rel }
|
||||
.find_or_initialize_by(**submitter_params.compact_blank)
|
||||
@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
|
||||
@submitter.assign_attributes(
|
||||
uuid: @template.submitters.first['uuid'],
|
||||
ip: request.remote_ip,
|
||||
ua: request.user_agent
|
||||
)
|
||||
if filter_undefined_submitters(@template).size > 1 && @submitter.new_record?
|
||||
@error_message = multiple_submitters_error_message
|
||||
|
||||
@submitter.submission ||= Submission.new(template: @template,
|
||||
template_submitters: @template.submitters,
|
||||
source: :link)
|
||||
return render :show, status: :unprocessable_content
|
||||
end
|
||||
|
||||
if (is_new_record = @submitter.new_record?)
|
||||
assign_submission_attributes(@submitter, @template)
|
||||
|
||||
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, is_new_record:)
|
||||
elsif @submitter.errors.blank? && @submitter.save
|
||||
enqueue_new_submitter_jobs(@submitter) if is_new_record
|
||||
|
||||
if @submitter.save
|
||||
redirect_to submit_form_path(@submitter.slug)
|
||||
else
|
||||
render :show
|
||||
render :show, status: :unprocessable_content
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
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 enqueue_new_submitter_jobs(submitter)
|
||||
WebhookUrls.enqueue_events(submitter.submission, 'submission.created')
|
||||
|
||||
SearchEntries.enqueue_reindex(submitter)
|
||||
|
||||
return unless submitter.submission.expire_at?
|
||||
|
||||
ProcessSubmissionExpiredJob.perform_at(submitter.submission.expire_at, 'submission_id' => submitter.submission_id)
|
||||
end
|
||||
|
||||
def load_resubmit_submitter
|
||||
@resubmit_submitter =
|
||||
if params[:resubmit].present? && !params[:resubmit].in?([true, 'true'])
|
||||
Submitter.find_by(slug: params[:resubmit])
|
||||
end
|
||||
end
|
||||
|
||||
def authorize_start!
|
||||
return redirect_to start_form_path(@template.slug) if @template.archived_at?
|
||||
|
||||
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)
|
||||
|
||||
redirect_to start_form_path(@template.slug)
|
||||
end
|
||||
|
||||
def find_or_initialize_submitter(template, submitter_params)
|
||||
required_fields = template.preferences.fetch('link_form_fields', ['email'])
|
||||
|
||||
required_params = required_fields.index_with { |key| submitter_params[key] }
|
||||
|
||||
find_params = required_params.except('name')
|
||||
|
||||
submitter = Submitter.new if find_params.compact_blank.blank?
|
||||
|
||||
submitter ||=
|
||||
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(template.preferences['shared_link_2fa'] == true ? {} : { ip: [nil, request.remote_ip] })
|
||||
.then { |rel| params[:resubmit].present? || params[:selfsign].present? ? rel.where(completed_at: nil) : rel }
|
||||
.find_or_initialize_by(find_params)
|
||||
|
||||
submitter.name = required_params['name'] if submitter.new_record?
|
||||
|
||||
unless @resubmit_submitter
|
||||
required_params.each do |key, value|
|
||||
submitter.errors.add(key.to_sym, :blank) if value.blank?
|
||||
end
|
||||
end
|
||||
|
||||
submitter
|
||||
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 || {}
|
||||
)
|
||||
|
||||
submitter.assign_attributes(@resubmit_submitter.slice(:name, :email, :phone)) if @resubmit_submitter
|
||||
|
||||
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
|
||||
params.require(:submitter).permit(:email, :phone).tap do |attrs|
|
||||
return { 'email' => current_user.email, 'name' => current_user.full_name } 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
|
||||
slug = params[:slug] || params[:start_form_slug]
|
||||
@template =
|
||||
if @resubmit_submitter
|
||||
@resubmit_submitter.template
|
||||
else
|
||||
Template.find_by!(slug: params[:slug] || params[:start_form_slug])
|
||||
end
|
||||
end
|
||||
|
||||
@template = Template.find_by!(slug:)
|
||||
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, is_new_record:)
|
||||
return render :show, status: :unprocessable_content if submitter.errors.present?
|
||||
|
||||
is_otp_verified = Submitters.verify_link_otp!(params[:one_time_code], submitter)
|
||||
|
||||
if cookies.encrypted[:email_2fa_slug] == submitter.slug || is_otp_verified
|
||||
if submitter.save
|
||||
enqueue_new_submitter_jobs(submitter) if is_new_record
|
||||
|
||||
if is_otp_verified
|
||||
SubmissionEvents.create_with_tracking_data(submitter, 'email_verified', request)
|
||||
|
||||
cookies.encrypted[:email_2fa_slug] =
|
||||
{ value: submitter.slug, expires: COOKIES_TTL.from_now, **COOKIES_DEFAULTS }
|
||||
end
|
||||
|
||||
redirect_to submit_form_path(submitter.slug)
|
||||
else
|
||||
render :show, status: :unprocessable_content
|
||||
end
|
||||
else
|
||||
Submitters.send_shared_link_email_verification_code(submitter, request:)
|
||||
|
||||
render :email_verification
|
||||
end
|
||||
rescue Submitters::UnableToSendCode, Submitters::InvalidOtp => e
|
||||
redirect_to start_form_path(submitter.submission.template.slug,
|
||||
params: submitter_params.merge(email_verification: true)),
|
||||
alert: e.message
|
||||
rescue RateLimit::LimitApproached
|
||||
redirect_to start_form_path(submitter.submission.template.slug,
|
||||
params: submitter_params.merge(email_verification: true)),
|
||||
alert: I18n.t(:too_many_attempts)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class StartFormEmail2faSendController < ApplicationController
|
||||
around_action :with_browser_locale
|
||||
|
||||
skip_before_action :authenticate_user!
|
||||
skip_authorization_check
|
||||
|
||||
def create
|
||||
@template = Template.find_by!(slug: params[:slug])
|
||||
|
||||
@submitter = @template.submissions.new(account_id: @template.account_id)
|
||||
.submitters.new(**submitter_params, account_id: @template.account_id)
|
||||
|
||||
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::UnableToSendCode => e
|
||||
redirect_to start_form_path(@template.slug, params: submitter_params.merge(email_verification: true)),
|
||||
alert: e.message
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def submitter_params
|
||||
params.require(:submitter).permit(:name, :email, :phone)
|
||||
end
|
||||
end
|
||||
@@ -11,9 +11,9 @@ class StorageSettingsController < ApplicationController
|
||||
if @encrypted_config.update(storage_configs)
|
||||
LoadActiveStorageConfigs.reload
|
||||
|
||||
redirect_to settings_storage_index_path, notice: 'Changes have been saved'
|
||||
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
|
||||
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class SubmissionEventsController < ApplicationController
|
||||
SUBMISSION_EVENT_ICONS = {
|
||||
'view_form' => 'eye',
|
||||
'start_form' => 'player_play',
|
||||
'complete_form' => 'check',
|
||||
'send_email' => 'mail_forward',
|
||||
'click_email' => 'hand_click',
|
||||
'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',
|
||||
'start_verification' => 'player_play',
|
||||
'complete_verification' => 'check',
|
||||
'invite_party' => 'user_plus'
|
||||
}.freeze
|
||||
|
||||
load_and_authorize_resource :submission
|
||||
|
||||
def index; end
|
||||
end
|
||||
@@ -0,0 +1,23 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class SubmissionsArchivedController < ApplicationController
|
||||
load_and_authorize_resource :submission, parent: false
|
||||
|
||||
def index
|
||||
@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)
|
||||
|
||||
@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
|
||||
|
||||
@pagy, @submissions = pagy_auto(@submissions.preload(submitters: :start_form_submission_events))
|
||||
end
|
||||
end
|
||||
@@ -6,21 +6,27 @@ class SubmissionsController < ApplicationController
|
||||
|
||||
load_and_authorize_resource :submission, only: %i[show destroy]
|
||||
|
||||
PRELOAD_ALL_PAGES_AMOUNT = 200
|
||||
prepend_before_action :maybe_redirect_com, only: %i[show]
|
||||
|
||||
before_action only: :create do
|
||||
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
|
||||
|
||||
def show
|
||||
ActiveRecord::Associations::Preloader.new(
|
||||
records: [@submission],
|
||||
associations: [:template, { template_schema_documents: :blob }]
|
||||
).call
|
||||
@submission = Submissions.preload_with_pages(@submission)
|
||||
|
||||
total_pages =
|
||||
@submission.template_schema_documents.sum { |e| e.metadata.dig('pdf', 'number_of_pages').to_i }
|
||||
|
||||
if total_pages < PRELOAD_ALL_PAGES_AMOUNT
|
||||
unless @submission.submitters.all?(&:completed_at?)
|
||||
ActiveRecord::Associations::Preloader.new(
|
||||
records: @submission.template_schema_documents,
|
||||
associations: [:blob, { preview_images_attachments: :blob }]
|
||||
records: [@submission],
|
||||
associations: [submitters: :start_form_submission_events]
|
||||
).call
|
||||
end
|
||||
|
||||
@@ -32,7 +38,9 @@ class SubmissionsController < ApplicationController
|
||||
end
|
||||
|
||||
def create
|
||||
authorize!(:create, Submission)
|
||||
save_template_message(@template, params) if params[:save_message] == '1'
|
||||
|
||||
[params.delete(:subject), params.delete(:body)] if params[:is_custom_message] != '1'
|
||||
|
||||
submissions =
|
||||
if params[:emails].present?
|
||||
@@ -40,34 +48,67 @@ class SubmissionsController < ApplicationController
|
||||
user: current_user,
|
||||
source: :invite,
|
||||
mark_as_sent: params[:send_email] == '1',
|
||||
emails: params[:emails])
|
||||
emails: params[:emails],
|
||||
params: params.merge('send_completed_email' => true))
|
||||
else
|
||||
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',
|
||||
mark_as_sent: params[:send_email] == '1',
|
||||
submissions_attrs: submissions_params[:submission].to_h.values)
|
||||
submissions_attrs:,
|
||||
new_fields:,
|
||||
params: params.merge('send_completed_email' => true))
|
||||
end
|
||||
|
||||
Submissions.send_signature_requests(submissions, params)
|
||||
WebhookUrls.enqueue_events(submissions, 'submission.created')
|
||||
|
||||
redirect_to template_path(@template), notice: 'New recipients have been added'
|
||||
Submissions.send_signature_requests(submissions)
|
||||
|
||||
SearchEntries.enqueue_reindex(submissions)
|
||||
|
||||
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_content
|
||||
end
|
||||
|
||||
def destroy
|
||||
@submission.update!(deleted_at: Time.current)
|
||||
notice =
|
||||
if params[:permanently].in?(['true', true])
|
||||
@submission.destroy!
|
||||
|
||||
redirect_back(fallback_location: template_path(@submission.template), notice: 'Submission has been archived')
|
||||
I18n.t('submission_has_been_removed')
|
||||
else
|
||||
@submission.update!(archived_at: Time.current)
|
||||
|
||||
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:)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
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?
|
||||
|
||||
template.save!
|
||||
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 = current_account.templates.find(params[:template_id])
|
||||
@template = Template.accessible_by(current_ability).find(params[:template_id])
|
||||
end
|
||||
end
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class SubmissionsDashboardController < ApplicationController
|
||||
load_and_authorize_resource :submission, parent: false
|
||||
|
||||
def index
|
||||
@submissions = @submissions.left_joins(:template)
|
||||
|
||||
@submissions = @submissions.where(archived_at: nil)
|
||||
.where(templates: { archived_at: nil })
|
||||
.preload(:template_accesses, :created_by_user, template: :author)
|
||||
|
||||
@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
|
||||
|
||||
@pagy, @submissions = pagy_auto(@submissions.preload(submitters: :start_form_submission_events))
|
||||
end
|
||||
end
|
||||
@@ -16,13 +16,16 @@ class SubmissionsDebugController < ApplicationController
|
||||
render 'submit_form/show'
|
||||
end
|
||||
f.pdf do
|
||||
if params[:audit]
|
||||
Submissions::GenerateAuditTrail.call(@submitter.submission)
|
||||
else
|
||||
Submissions::GenerateResultAttachments.call(@submitter)
|
||||
end
|
||||
result =
|
||||
if params[:audit]
|
||||
Submissions::GenerateAuditTrail.call(@submitter.submission)
|
||||
elsif params[:combined]
|
||||
Submissions::GenerateCombinedAttachment.call(@submitter)
|
||||
else
|
||||
Submissions::GenerateResultAttachments.call(@submitter)
|
||||
end
|
||||
|
||||
send_data ActiveStorage::Attachment.where(name: params[:audit] ? :audit_trail : :documents).last.download,
|
||||
send_data Array.wrap(result).first.download,
|
||||
filename: 'debug.pdf',
|
||||
disposition: 'inline',
|
||||
type: 'application/pdf'
|
||||
|
||||
@@ -4,20 +4,81 @@ class SubmissionsDownloadController < ApplicationController
|
||||
skip_before_action :authenticate_user!
|
||||
skip_authorization_check
|
||||
|
||||
TTL = 40.minutes
|
||||
FILES_TTL = 5.minutes
|
||||
|
||||
def index
|
||||
submitter = Submitter.find_by(slug: params[:submitter_slug])
|
||||
@submitter = Submitter.find_signed(params[:sig], purpose: :download_completed) if params[:sig].present?
|
||||
|
||||
Submissions::EnsureResultGenerated.call(submitter)
|
||||
signature_valid =
|
||||
if @submitter&.slug == params[:submitter_slug]
|
||||
true
|
||||
else
|
||||
@submitter = nil
|
||||
end
|
||||
|
||||
last_submitter = submitter.submission.submitters.where.not(completed_at: nil).order(:completed_at).last
|
||||
@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
|
||||
|
||||
return head :not_found unless last_submitter
|
||||
|
||||
Submissions::EnsureResultGenerated.call(last_submitter)
|
||||
|
||||
urls =
|
||||
Submitters.select_attachments_for_download(last_submitter).map do |attachment|
|
||||
helpers.rails_blob_url(attachment)
|
||||
end
|
||||
if last_submitter.completed_at < TTL.ago && !signature_valid && !current_user_submitter?(last_submitter)
|
||||
Rollbar.info("TTL: #{last_submitter.id}") if defined?(Rollbar)
|
||||
|
||||
render json: urls
|
||||
return head :not_found
|
||||
end
|
||||
|
||||
if params[:combined] == 'true'
|
||||
url = build_combined_url(@submitter)
|
||||
|
||||
if url
|
||||
render json: [url]
|
||||
else
|
||||
head :not_found
|
||||
end
|
||||
else
|
||||
render json: build_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::EnsureCombinedGenerated.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
|
||||
|
||||
@@ -10,11 +10,16 @@ class SubmissionsExportController < ApplicationController
|
||||
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
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class SubmissionsFiltersController < ApplicationController
|
||||
ALLOWED_NAMES = %w[
|
||||
author
|
||||
folder
|
||||
completed_at
|
||||
status
|
||||
created_at
|
||||
].freeze
|
||||
|
||||
skip_authorization_check
|
||||
|
||||
def show
|
||||
return head :not_found unless ALLOWED_NAMES.include?(params[:name])
|
||||
|
||||
render params[:name]
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,70 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class SubmissionsPreviewController < ApplicationController
|
||||
around_action :with_browser_locale
|
||||
skip_before_action :authenticate_user!
|
||||
skip_authorization_check
|
||||
|
||||
prepend_before_action :maybe_redirect_com, only: %i[show completed]
|
||||
|
||||
TTL = 40.minutes
|
||||
|
||||
def show
|
||||
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
|
||||
|
||||
true
|
||||
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 &&
|
||||
(!current_user || !current_ability.can?(:read, @submission))
|
||||
raise ActionController::RoutingError, I18n.t('not_found')
|
||||
end
|
||||
|
||||
if use_signature?(@submission) && !signature_valid
|
||||
Rollbar.info("TTL: #{@submission.id}") if defined?(Rollbar)
|
||||
|
||||
return redirect_to submissions_preview_completed_path(@submission.slug)
|
||||
end
|
||||
|
||||
@submission = Submissions.preload_with_pages(@submission)
|
||||
|
||||
render 'submissions/show', layout: 'plain'
|
||||
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'
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
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
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,11 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class SubmissionsUnarchiveController < ApplicationController
|
||||
load_and_authorize_resource :submission
|
||||
|
||||
def create
|
||||
@submission.update!(archived_at: nil)
|
||||
|
||||
redirect_to submission_path(@submission), notice: I18n.t('submission_has_been_unarchived')
|
||||
end
|
||||
end
|
||||
@@ -3,49 +3,126 @@
|
||||
class SubmitFormController < ApplicationController
|
||||
layout 'form'
|
||||
|
||||
around_action :with_browser_locale, only: %i[show completed success]
|
||||
skip_before_action :authenticate_user!
|
||||
skip_authorization_check
|
||||
|
||||
PRELOAD_ALL_PAGES_AMOUNT = 200
|
||||
before_action :load_submitter, only: %i[show update completed]
|
||||
before_action :maybe_render_locked_page, only: :show
|
||||
before_action :maybe_require_link_2fa, only: %i[show update]
|
||||
|
||||
CONFIG_KEYS = [].freeze
|
||||
|
||||
def show
|
||||
@submitter = Submitter.find_by!(slug: params[:slug])
|
||||
submission = @submitter.submission
|
||||
|
||||
return redirect_to submit_form_completed_path(@submitter.slug) if @submitter.completed_at?
|
||||
return render :email_2fa if require_email_2fa?(@submitter)
|
||||
|
||||
ActiveRecord::Associations::Preloader.new(
|
||||
records: [@submitter],
|
||||
associations: [submission: [:template, { template_schema_documents: :blob }]]
|
||||
).call
|
||||
@form_configs = Submitters::FormConfigs.call(@submitter, CONFIG_KEYS)
|
||||
|
||||
total_pages =
|
||||
@submitter.submission.template_schema_documents.sum { |e| e.metadata.dig('pdf', 'number_of_pages').to_i }
|
||||
return render :awaiting if (@form_configs[:enforce_signing_order] ||
|
||||
submission.template&.preferences&.dig('submitters_order') == 'preserved') &&
|
||||
!Submitters.current_submitter_order?(@submitter)
|
||||
|
||||
if total_pages < PRELOAD_ALL_PAGES_AMOUNT
|
||||
ActiveRecord::Associations::Preloader.new(
|
||||
records: @submitter.submission.template_schema_documents,
|
||||
associations: [:blob, { preview_images_attachments: :blob }]
|
||||
).call
|
||||
end
|
||||
Submissions.preload_with_pages(submission)
|
||||
|
||||
Submitters::MaybeUpdateDefaultValues.call(@submitter, current_user)
|
||||
|
||||
cookies[:submitter_sid] = @submitter.signed_id
|
||||
@attachments_index = build_attachments_index(submission)
|
||||
|
||||
render @submitter.submission.template.deleted_at? ? :archived : :show
|
||||
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_attachment ||=
|
||||
Submitters::MaybeAssignDefaultBrowserSignature.call(@submitter, params, cookies, @attachments_index.values)
|
||||
|
||||
@attachments_index[@signature_attachment.uuid] = @signature_attachment if @signature_attachment
|
||||
end
|
||||
|
||||
def update
|
||||
submitter = Submitter.find_by!(slug: params[:slug])
|
||||
if require_email_2fa?(@submitter)
|
||||
return render json: { error: I18n.t('verification_required_refresh_the_page_and_pass_2fa') },
|
||||
status: :unprocessable_content
|
||||
end
|
||||
|
||||
Submitters::SubmitValues.call(submitter, params, request)
|
||||
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_content
|
||||
end
|
||||
|
||||
if @submitter.declined_at?
|
||||
return render json: { error: I18n.t('form_has_been_declined') },
|
||||
status: :unprocessable_content
|
||||
end
|
||||
|
||||
Submitters::SubmitValues.call(@submitter, params, request)
|
||||
|
||||
head :ok
|
||||
rescue Submitters::SubmitValues::RequiredFieldError => e
|
||||
Rollbar.warning("Required field #{@submitter.id}: #{e.message}") if defined?(Rollbar)
|
||||
|
||||
render json: { field_uuid: e.message }, status: :unprocessable_content
|
||||
rescue Submitters::SubmitValues::ValidationError => e
|
||||
render json: { error: e.message }, status: :unprocessable_entity
|
||||
render json: { error: e.message }, status: :unprocessable_content
|
||||
end
|
||||
|
||||
def completed
|
||||
@submitter = Submitter.find_by!(slug: params[:submit_form_slug])
|
||||
raise ActionController::RoutingError, I18n.t('not_found') if @submitter.account.archived_at?
|
||||
|
||||
redirect_to submit_form_path(params[:submit_form_slug]) if require_email_2fa?(@submitter)
|
||||
end
|
||||
|
||||
def success; end
|
||||
|
||||
private
|
||||
|
||||
def maybe_require_link_2fa
|
||||
return if @submitter.submission.source != 'link'
|
||||
return unless @submitter.submission.template&.preferences&.dig('shared_link_2fa') == true
|
||||
return if cookies.encrypted[:email_2fa_slug] == @submitter.slug
|
||||
return if @submitter.email == current_user&.email && current_user&.account_id == @submitter.account_id
|
||||
|
||||
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? ||
|
||||
@submitter.account.archived_at?
|
||||
return render :expired if @submitter.submission.expired?
|
||||
|
||||
render :declined if @submitter.declined_at?
|
||||
end
|
||||
|
||||
def load_submitter
|
||||
@submitter = Submitter.find_by!(slug: params[:slug] || params[:submit_form_slug])
|
||||
end
|
||||
|
||||
def build_attachments_index(submission)
|
||||
ActiveStorage::Attachment.where(record: submission.submitters, name: :attachments)
|
||||
.preload(:blob).index_by(&:uuid)
|
||||
end
|
||||
|
||||
def require_email_2fa?(submitter)
|
||||
return false if submitter.submission.template&.preferences&.dig('require_email_2fa') != true &&
|
||||
submitter.preferences['require_email_2fa'] != true
|
||||
return false if cookies.encrypted[:email_2fa_slug] == submitter.slug
|
||||
|
||||
true
|
||||
end
|
||||
end
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class SubmitFormDeclineController < ApplicationController
|
||||
skip_before_action :authenticate_user!
|
||||
skip_authorization_check
|
||||
|
||||
def create
|
||||
submitter = Submitter.find_by!(slug: params[:submit_form_slug])
|
||||
|
||||
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?
|
||||
|
||||
ApplicationRecord.transaction do
|
||||
submitter.update!(declined_at: Time.current)
|
||||
|
||||
SubmissionEvents.create_with_tracking_data(submitter, 'decline_form', request, { reason: params[:reason] })
|
||||
end
|
||||
|
||||
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!
|
||||
end
|
||||
|
||||
WebhookUrls.enqueue_events(submitter, 'form.declined')
|
||||
|
||||
redirect_to submit_form_path(submitter.slug)
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,40 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class SubmitFormDownloadController < ApplicationController
|
||||
skip_before_action :authenticate_user!
|
||||
skip_authorization_check
|
||||
|
||||
FILES_TTL = 5.minutes
|
||||
|
||||
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 head :unprocessable_content 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)
|
||||
|
||||
last_completed_submitter = @submitter.submission.submitters
|
||||
.where.not(id: @submitter.id)
|
||||
.where.not(completed_at: nil)
|
||||
.max_by(&:completed_at)
|
||||
|
||||
attachments =
|
||||
if last_completed_submitter
|
||||
Submitters.select_attachments_for_download(last_completed_submitter)
|
||||
else
|
||||
@submitter.submission.schema_documents.preload(:blob)
|
||||
end
|
||||
|
||||
urls = attachments.map do |attachment|
|
||||
ActiveStorage::Blob.proxy_url(attachment.blob, expires_at: FILES_TTL.from_now.to_i)
|
||||
end
|
||||
|
||||
render json: urls
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,21 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class SubmitFormDrawSignatureController < ApplicationController
|
||||
layout false
|
||||
|
||||
around_action :with_browser_locale, only: %i[show]
|
||||
skip_before_action :authenticate_user!
|
||||
skip_authorization_check
|
||||
|
||||
def show
|
||||
@submitter = Submitter.find_by!(slug: params[:slug])
|
||||
|
||||
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)
|
||||
end
|
||||
|
||||
render :show
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,52 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class SubmitFormEmail2fasController < ApplicationController
|
||||
around_action :with_browser_locale
|
||||
|
||||
skip_before_action :authenticate_user!
|
||||
skip_authorization_check
|
||||
|
||||
before_action :load_submitter
|
||||
|
||||
COOKIES_TTL = 12.hours
|
||||
COOKIES_DEFAULTS = { httponly: true, secure: Rails.env.production? }.freeze
|
||||
|
||||
def create
|
||||
RateLimit.call("verify-2fa-code-#{@submitter.id}", limit: 2, ttl: 45.seconds, 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: COOKIES_TTL.from_now, **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)
|
||||
|
||||
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
|
||||
@@ -0,0 +1,60 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class SubmitFormInviteController < ApplicationController
|
||||
skip_before_action :authenticate_user!
|
||||
skip_authorization_check
|
||||
|
||||
def create
|
||||
submitter = Submitter.find_by!(slug: params[:submit_form_slug])
|
||||
|
||||
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')
|
||||
|
||||
ApplicationRecord.transaction do
|
||||
(invite_submitters + optional_invite_submitters).each do |item|
|
||||
attrs = submitters_attributes.find { |e| e[:uuid] == item['uuid'] }
|
||||
|
||||
next unless attrs
|
||||
next if attrs[:email].blank?
|
||||
|
||||
submitter.submission.submitters.create!(**attrs, account_id: submitter.account_id)
|
||||
|
||||
SubmissionEvents.create_with_tracking_data(submitter, 'invite_party', request, { uuid: submitter.uuid })
|
||||
end
|
||||
|
||||
submitter.submission.update!(submitters_order: :preserved)
|
||||
end
|
||||
|
||||
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)
|
||||
|
||||
head :ok
|
||||
else
|
||||
head :unprocessable_content
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def can_invite?(submitter)
|
||||
!submitter.declined_at? &&
|
||||
!submitter.completed_at? &&
|
||||
!submitter.submission.archived_at? &&
|
||||
!submitter.submission.expired? &&
|
||||
!submitter.submission.template&.archived_at?
|
||||
end
|
||||
|
||||
def filter_invite_submitters(submitter, key = 'invite_by_uuid')
|
||||
(submitter.submission.template_submitters || submitter.submission.template.submitters).select do |s|
|
||||
s[key] == submitter.uuid && submitter.submission.submitters.none? { |e| e.uuid == s['uuid'] }
|
||||
end
|
||||
end
|
||||
|
||||
def submitters_attributes
|
||||
params.require(:submission).permit(submitters: [%i[uuid email]]).fetch(:submitters, [])
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,23 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class SubmitFormValuesController < ApplicationController
|
||||
skip_before_action :authenticate_user!
|
||||
skip_authorization_check
|
||||
|
||||
def index
|
||||
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?
|
||||
|
||||
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,38 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class SubmittersAutocompleteController < ApplicationController
|
||||
load_and_authorize_resource :submitter, parent: false
|
||||
|
||||
SELECT_COLUMNS = %w[email phone name].freeze
|
||||
LIMIT = 100
|
||||
|
||||
def index
|
||||
submitters = search_submitters(@submitters)
|
||||
|
||||
arel_columns = SELECT_COLUMNS.map { |col| Submitter.arel_table[col] }
|
||||
values = submitters.limit(LIMIT).group(arel_columns).pluck(arel_columns)
|
||||
|
||||
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?
|
||||
Submitters.fulltext_search_field(current_user, submitters, params[:q], params[:field])
|
||||
else
|
||||
column = Submitter.arel_table[params[:field].to_sym]
|
||||
|
||||
term = "#{params[:q].downcase}%"
|
||||
|
||||
submitters.where(column.matches(term))
|
||||
end
|
||||
else
|
||||
Submitters.search(current_user, submitters, params[:q])
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,82 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class SubmittersController < ApplicationController
|
||||
load_and_authorize_resource :submitter, only: %i[edit update]
|
||||
|
||||
def edit
|
||||
@submitter_email_message =
|
||||
if @submitter.preferences['email_message_uuid'].present?
|
||||
@submitter.account.email_messages.find_by(uuid: @submitter.preferences['email_message_uuid'])
|
||||
end
|
||||
end
|
||||
|
||||
def update
|
||||
submission = @submitter.submission
|
||||
|
||||
if @submitter.submission_events.exists?(event_type: 'start_form') || submission.archived_at? || submission.expired?
|
||||
return redirect_back fallback_location: submission_path(submission), alert: I18n.t('submitter_cannot_be_updated')
|
||||
end
|
||||
|
||||
if submitter_params.values.all?(&:blank?)
|
||||
return redirect_back fallback_location: submission_path(submission),
|
||||
alert: I18n.t('at_least_one_field_must_be_filled')
|
||||
end
|
||||
|
||||
if params[:is_custom_message] != '1'
|
||||
params.delete(:subject)
|
||||
params.delete(:body)
|
||||
end
|
||||
|
||||
submitter_preferences = Submitters.normalize_preferences(@submitter.account, current_user, params)
|
||||
|
||||
if submitter_preferences.key?('email_message_uuid')
|
||||
@submitter.preferences['email_message_uuid'] = submitter_preferences['email_message_uuid']
|
||||
end
|
||||
|
||||
assign_submitter_attrs(@submitter, submitter_params)
|
||||
|
||||
if @submitter.save
|
||||
maybe_resend_email_sms(@submitter, params)
|
||||
|
||||
SearchEntries.enqueue_reindex(@submitter)
|
||||
|
||||
redirect_back fallback_location: submission_path(submission), notice: I18n.t('changes_have_been_saved')
|
||||
else
|
||||
redirect_back fallback_location: submission_path(submission), alert: I18n.t('unable_to_save')
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
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',
|
||||
emailable: submitter,
|
||||
event_type: 'send',
|
||||
created_at: 4.hours.ago..Time.current)
|
||||
|
||||
SendSubmitterInvitationEmailJob.perform_async('submitter_id' => submitter.id) unless is_sent_recently
|
||||
end
|
||||
|
||||
return if submitter.phone.blank?
|
||||
return unless params[:send_sms] == '1'
|
||||
|
||||
SendSubmitterInvitationSmsJob.perform_async('submitter_id' => submitter.id)
|
||||
end
|
||||
|
||||
def assign_submitter_attrs(submitter, attrs)
|
||||
submitter.phone = attrs[:phone].to_s.gsub(/[^0-9+]/, '') if attrs.key?(:phone)
|
||||
|
||||
submitter.email = Submissions.normalize_email(attrs[:email]) if attrs.key?(:email)
|
||||
|
||||
submitter.name = attrs[:name] if attrs.key?(:name)
|
||||
|
||||
submitter
|
||||
end
|
||||
|
||||
def submitter_params
|
||||
params.require(:submitter).permit(:email, :name, :phone).transform_values(&:strip)
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,64 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class SubmittersResubmitController < ApplicationController
|
||||
load_and_authorize_resource :submitter, parent: false
|
||||
|
||||
def update
|
||||
return redirect_to submit_form_path(slug: @submitter.slug) if @submitter.email != current_user.email
|
||||
|
||||
submission = @submitter.account.submissions.new(created_by_user: current_user,
|
||||
submitters_order: :preserved,
|
||||
**@submitter.submission.slice(:template_fields,
|
||||
:account_id,
|
||||
:name,
|
||||
:template_id,
|
||||
:template_schema,
|
||||
:template_submitters,
|
||||
:preferences))
|
||||
|
||||
@submitter.submission.submitters.each do |submitter|
|
||||
new_submitter = submission.submitters.new(submitter.slice(:uuid, :email, :phone, :name,
|
||||
:preferences, :metadata, :account_id))
|
||||
|
||||
next unless submitter.uuid == @submitter.uuid
|
||||
|
||||
assign_submitter_values(new_submitter, submitter)
|
||||
|
||||
@new_submitter ||= new_submitter
|
||||
end
|
||||
|
||||
submission.save!
|
||||
|
||||
@submitter.submission.documents_attachments.each do |attachment|
|
||||
submission.documents_attachments.create!(uuid: attachment.uuid, blob_id: attachment.blob_id)
|
||||
end
|
||||
|
||||
redirect_to submit_form_path(slug: @new_submitter.slug)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def assign_submitter_values(new_submitter, submitter)
|
||||
attachments_index = submitter.attachments.index_by(&:uuid)
|
||||
|
||||
submitter.submission.template_fields.each do |field|
|
||||
next if field['submitter_uuid'] != submitter.uuid
|
||||
next if field['default_value'] == '{{date}}'
|
||||
next if field['type'] == 'stamp'
|
||||
next if field['type'] == 'signature'
|
||||
next if field.dig('preferences', 'formula').present?
|
||||
|
||||
value = submitter.values[field['uuid']]
|
||||
|
||||
next if value.blank?
|
||||
|
||||
if field['type'].in?(%w[image file initials])
|
||||
Array.wrap(value).each do |attachment_uuid|
|
||||
new_submitter.attachments << attachments_index[attachment_uuid].dup
|
||||
end
|
||||
end
|
||||
|
||||
new_submitter.values[field['uuid']] = value
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -4,13 +4,20 @@ class SubmittersSendEmailController < ApplicationController
|
||||
load_and_authorize_resource :submitter, id_param: :submitter_slug, find_by: :slug
|
||||
|
||||
def create
|
||||
SubmitterMailer.invitation_email(@submitter).deliver_later!
|
||||
if Docuseal.multitenant? && SubmissionEvent.exists?(submitter: @submitter,
|
||||
event_type: 'send_email',
|
||||
created_at: 10.hours.ago..Time.current)
|
||||
Rollbar.warning("Already sent: #{@submitter.id}") if defined?(Rollbar)
|
||||
|
||||
SubmissionEvent.create!(submitter: @submitter, event_type: 'send_email')
|
||||
return redirect_back(fallback_location: submission_path(@submitter.submission),
|
||||
alert: I18n.t('email_has_been_sent_already'))
|
||||
end
|
||||
|
||||
SendSubmitterInvitationEmailJob.perform_async('submitter_id' => @submitter.id)
|
||||
|
||||
@submitter.sent_at ||= Time.current
|
||||
@submitter.save!
|
||||
|
||||
redirect_back(fallback_location: submission_path(@submitter.submission), notice: 'Email has been sent')
|
||||
redirect_back(fallback_location: submission_path(@submitter.submission), notice: I18n.t('email_has_been_sent'))
|
||||
end
|
||||
end
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class TemplateDocumentsController < ApplicationController
|
||||
load_and_authorize_resource :template
|
||||
|
||||
FILES_TTL = 5.minutes
|
||||
|
||||
def index
|
||||
render json: @template.schema_documents.map { |d| ActiveStorage::Blob.proxy_url(d.blob, expires_at: FILES_TTL.from_now.to_i) }
|
||||
end
|
||||
|
||||
def create
|
||||
if params[:blobs].blank? && params[:files].blank?
|
||||
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)
|
||||
|
||||
schema = documents.map do |doc|
|
||||
{ attachment_uuid: doc.uuid, name: doc.filename.base }
|
||||
end
|
||||
|
||||
render json: {
|
||||
schema:,
|
||||
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],
|
||||
include: {
|
||||
preview_images: { methods: %i[url metadata filename] }
|
||||
}
|
||||
)
|
||||
}
|
||||
rescue Templates::CreateAttachments::PdfEncrypted
|
||||
render json: { error: 'PDF encrypted', status: 'pdf_encrypted' }, status: :unprocessable_content
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,33 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class TemplateFoldersAutocompleteController < ApplicationController
|
||||
load_and_authorize_resource :template_folder, parent: false
|
||||
|
||||
LIMIT = 30
|
||||
|
||||
def index
|
||||
parent_name, name =
|
||||
if params[:parent_name].present?
|
||||
[params[:parent_name], params[:q]]
|
||||
else
|
||||
params[:q].to_s.split(' /', 2).map(&:squish)
|
||||
end
|
||||
|
||||
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,11 +3,41 @@
|
||||
class TemplateFoldersController < ApplicationController
|
||||
load_and_authorize_resource :template_folder
|
||||
|
||||
def show
|
||||
@templates = @template_folder.templates.active.preload(:author).order(id: :desc)
|
||||
@templates = Templates.search(@templates, params[:q])
|
||||
helper_method :selected_order
|
||||
|
||||
@pagy, @templates = pagy(@templates, items: 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 =
|
||||
@template_folder.subfolders.where(id: Template.accessible_by(current_ability).active.select(:folder_id))
|
||||
|
||||
@template_folders = TemplateFolders.search(@template_folders, params[:q])
|
||||
@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, limit:)
|
||||
|
||||
load_related_submissions if params[:q].present? && @templates.blank?
|
||||
else
|
||||
@pagy, @template_folders = pagy(@template_folders, limit: FOLDERS_PER_PAGE)
|
||||
|
||||
@templates = @templates.none
|
||||
end
|
||||
end
|
||||
|
||||
def edit; end
|
||||
@@ -15,15 +45,42 @@ class TemplateFoldersController < ApplicationController
|
||||
def update
|
||||
if @template_folder != current_account.default_template_folder &&
|
||||
@template_folder.update(template_folder_params)
|
||||
redirect_to folder_path(@template_folder), notice: 'Folder name has been updated'
|
||||
redirect_to folder_path(@template_folder), notice: I18n.t('folder_name_has_been_updated')
|
||||
else
|
||||
redirect_to folder_path(@template_folder), alert: 'Unable to rename folder'
|
||||
redirect_to folder_path(@template_folder), alert: I18n.t('unable_to_rename_folder')
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def selected_order
|
||||
@selected_order ||=
|
||||
if cookies.permanent[:dashboard_templates_order].blank? ||
|
||||
(cookies.permanent[:dashboard_templates_order] == 'used_at' && can?(:manage, :countless))
|
||||
'created_at'
|
||||
else
|
||||
cookies.permanent[:dashboard_templates_order]
|
||||
end
|
||||
end
|
||||
|
||||
def template_folder_params
|
||||
params.require(:template_folder).permit(:name)
|
||||
end
|
||||
|
||||
def load_related_submissions
|
||||
@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)
|
||||
|
||||
@related_submissions_pagy, @related_submissions = pagy_auto(@related_submissions, limit: 5)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class TemplateSharingsTestingController < ApplicationController
|
||||
load_and_authorize_resource :template, parent: true
|
||||
|
||||
before_action do
|
||||
authorize!(:manage, TemplateSharing.new(template: @template))
|
||||
end
|
||||
|
||||
def create
|
||||
testing_account = Accounts.find_or_create_testing_user(true_user.account).account
|
||||
|
||||
if params[:value] == '1'
|
||||
TemplateSharing.create!(ability: :manage, account: testing_account, template: @template)
|
||||
else
|
||||
TemplateSharing.find_by(template: @template, account: testing_account)&.destroy!
|
||||
end
|
||||
|
||||
head :ok
|
||||
end
|
||||
end
|
||||
@@ -4,9 +4,27 @@ class TemplatesArchivedController < ApplicationController
|
||||
load_and_authorize_resource :template, parent: false
|
||||
|
||||
def index
|
||||
@templates = @templates.where.not(deleted_at: nil).preload(:author, :folder).order(id: :desc)
|
||||
@templates = Templates.search(@templates, params[:q])
|
||||
@templates = @templates.where.not(archived_at: nil)
|
||||
.preload(:author, :template_accesses, folder: :parent_folder)
|
||||
.order(id: :desc)
|
||||
|
||||
@pagy, @templates = pagy(@templates, items: 12)
|
||||
@templates = Templates.search(current_user, @templates, params[:q])
|
||||
|
||||
@pagy, @templates = pagy_auto(@templates, limit: 12)
|
||||
|
||||
return unless params[:q].present? && @templates.blank?
|
||||
|
||||
@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)
|
||||
|
||||
@related_submissions_pagy, @related_submissions = pagy_auto(@related_submissions, limit: 5)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -5,10 +5,17 @@ class TemplatesArchivedSubmissionsController < ApplicationController
|
||||
load_and_authorize_resource :submission, through: :template, parent: false
|
||||
|
||||
def index
|
||||
@submissions = @submissions.where.not(deleted_at: nil)
|
||||
@submissions = Submissions.search(@submissions, params[:q])
|
||||
@submissions = @submissions.where.not(archived_at: nil)
|
||||
@submissions = Submissions.search(current_user, @submissions, params[:q], search_values: true)
|
||||
@submissions = Submissions::Filter.call(@submissions, current_user, params)
|
||||
|
||||
@pagy, @submissions = pagy(@submissions.preload(:submitters).order(id: :desc))
|
||||
@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
|
||||
|
||||
@pagy, @submissions = pagy_auto(@submissions.preload(submitters: :start_form_submission_events))
|
||||
rescue ActiveRecord::RecordNotFound
|
||||
redirect_to root_path
|
||||
end
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class TemplatesCloneAndReplaceController < ApplicationController
|
||||
load_and_authorize_resource :template
|
||||
|
||||
def create
|
||||
return head :unprocessable_content if params[:files].blank?
|
||||
|
||||
ActiveRecord::Associations::Preloader.new(
|
||||
records: [@template],
|
||||
associations: [schema_documents: :preview_images_attachments]
|
||||
).call
|
||||
|
||||
cloned_template = Templates::Clone.call(@template, author: current_user)
|
||||
cloned_template.name = File.basename(params[:files].first.original_filename, '.*')
|
||||
cloned_template.save!
|
||||
|
||||
documents = Templates::ReplaceAttachments.call(cloned_template, params, extract_fields: true)
|
||||
|
||||
Templates.maybe_assign_access(cloned_template)
|
||||
|
||||
cloned_template.save!
|
||||
|
||||
Templates::CloneAttachments.call(template: cloned_template, original_template: @template,
|
||||
excluded_attachment_uuids: documents.map(&:uuid))
|
||||
|
||||
SearchEntries.enqueue_reindex(cloned_template)
|
||||
|
||||
respond_to do |f|
|
||||
f.html { redirect_to edit_template_path(cloned_template) }
|
||||
f.json { render json: { id: cloned_template.id } }
|
||||
end
|
||||
rescue Templates::CreateAttachments::PdfEncrypted
|
||||
respond_to do |f|
|
||||
f.html { render turbo_stream: turbo_stream.append(params[:form_id], html: helpers.tag.prompt_password) }
|
||||
f.json { render json: { error: 'PDF encrypted', status: 'pdf_encrypted' }, status: :unprocessable_content }
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,56 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class TemplatesCloneController < ApplicationController
|
||||
load_and_authorize_resource :template, instance_name: :base_template
|
||||
|
||||
def new
|
||||
authorize!(:create, Template)
|
||||
|
||||
@template = Template.new(name: "#{@base_template.name} (#{I18n.t('clone')})")
|
||||
end
|
||||
|
||||
def create
|
||||
ActiveRecord::Associations::Preloader.new(
|
||||
records: [@base_template],
|
||||
associations: [schema_documents: :preview_images_attachments]
|
||||
).call
|
||||
|
||||
@template = Templates::Clone.call(@base_template, author: current_user,
|
||||
name: params.dig(:template, :name),
|
||||
folder_name: params[:folder_name])
|
||||
|
||||
authorize!(:create, @template)
|
||||
|
||||
if params[:account_id].present? && true_ability.authorize!(:manage, Account.find(params[:account_id]))
|
||||
@template.account_id = params[:account_id]
|
||||
@template.author = true_user if true_user.account_id == @template.account_id
|
||||
@template.folder = @template.account.default_template_folder if @template.account_id != current_account.id
|
||||
else
|
||||
@template.account = current_account
|
||||
end
|
||||
|
||||
Templates.maybe_assign_access(@template)
|
||||
|
||||
if @template.save
|
||||
Templates::CloneAttachments.call(template: @template, original_template: @base_template)
|
||||
|
||||
SearchEntries.enqueue_reindex(@template)
|
||||
|
||||
WebhookUrls.enqueue_events(@template, 'template.created')
|
||||
|
||||
maybe_redirect_to_template(@template)
|
||||
else
|
||||
render turbo_stream: turbo_stream.replace(:modal, partial: 'templates_clone/form'), status: :unprocessable_content
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def maybe_redirect_to_template(template)
|
||||
if template.account == current_account
|
||||
redirect_to(edit_template_path(template))
|
||||
else
|
||||
redirect_back(fallback_location: root_path, notice: I18n.t('template_has_been_cloned'))
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,7 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class TemplatesCodeModalController < ApplicationController
|
||||
load_and_authorize_resource :template
|
||||
|
||||
def show; end
|
||||
end
|
||||
@@ -3,37 +3,39 @@
|
||||
class TemplatesController < ApplicationController
|
||||
load_and_authorize_resource :template
|
||||
|
||||
before_action :load_base_template, only: %i[new create]
|
||||
|
||||
def show
|
||||
submissions = @template.submissions
|
||||
submissions = submissions.active if @template.deleted_at.blank?
|
||||
submissions = Submissions.search(submissions, params[:q])
|
||||
submissions = @template.submissions.accessible_by(current_ability)
|
||||
submissions = submissions.active if @template.archived_at.blank?
|
||||
submissions = Submissions.search(current_user, submissions, params[:q], search_values: true)
|
||||
submissions = Submissions::Filter.call(submissions, current_user, params.except(:status))
|
||||
|
||||
@base_submissions = submissions
|
||||
|
||||
submissions = submissions.pending if params[:status] == 'pending'
|
||||
submissions = submissions.completed if params[:status] == 'completed'
|
||||
submissions = Submissions::Filter.filter_by_status(submissions, params)
|
||||
|
||||
@pagy, @submissions = pagy(submissions.preload(:submitters).order(id: :desc))
|
||||
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
|
||||
|
||||
@pagy, @submissions = pagy_auto(submissions.preload(:template_accesses, submitters: :start_form_submission_events))
|
||||
rescue ActiveRecord::RecordNotFound
|
||||
redirect_to root_path
|
||||
end
|
||||
|
||||
def new
|
||||
@template.name = "#{@base_template.name} (Clone)" if @base_template
|
||||
end
|
||||
def new; end
|
||||
|
||||
def edit
|
||||
ActiveRecord::Associations::Preloader.new(
|
||||
records: [@template],
|
||||
associations: [schema_documents: { preview_images_attachments: :blob }]
|
||||
associations: [schema_documents: [:blob, { preview_images_attachments: :blob }]]
|
||||
).call
|
||||
|
||||
@template_data =
|
||||
@template.as_json.merge(
|
||||
documents: @template.schema_documents.as_json(
|
||||
methods: [:metadata],
|
||||
methods: %i[metadata signed_uuid],
|
||||
include: { preview_images: { methods: %i[url metadata filename] } }
|
||||
)
|
||||
).to_json
|
||||
@@ -42,30 +44,47 @@ class TemplatesController < ApplicationController
|
||||
end
|
||||
|
||||
def create
|
||||
@template.account = current_account
|
||||
@template.author = current_user
|
||||
@template.folder = TemplateFolders.find_or_create_by_name(current_user, params[:folder_name])
|
||||
@template.assign_attributes(@base_template.slice(:fields, :schema, :submitters)) if @base_template
|
||||
@template.account = current_account
|
||||
|
||||
Templates.maybe_assign_access(@template)
|
||||
|
||||
if @template.save
|
||||
Templates::CloneAttachments.call(template: @template, original_template: @base_template) if @base_template
|
||||
SearchEntries.enqueue_reindex(@template)
|
||||
|
||||
redirect_to edit_template_path(@template)
|
||||
WebhookUrls.enqueue_events(@template, 'template.created')
|
||||
|
||||
redirect_to(edit_template_path(@template))
|
||||
else
|
||||
render turbo_stream: turbo_stream.replace(:modal, template: 'templates/new'), status: :unprocessable_entity
|
||||
render turbo_stream: turbo_stream.replace(:modal, template: 'templates/new'), status: :unprocessable_content
|
||||
end
|
||||
end
|
||||
|
||||
def update
|
||||
@template.assign_attributes(template_params)
|
||||
|
||||
is_name_changed = @template.name_changed?
|
||||
|
||||
@template.save!
|
||||
|
||||
SearchEntries.enqueue_reindex(@template) if is_name_changed
|
||||
|
||||
WebhookUrls.enqueue_events(@template, 'template.updated')
|
||||
|
||||
head :ok
|
||||
end
|
||||
|
||||
def destroy
|
||||
notice =
|
||||
if !Docuseal.multitenant? && params[:permanently].present?
|
||||
if params[:permanently].in?(['true', true])
|
||||
@template.destroy!
|
||||
|
||||
'Template has been removed.'
|
||||
I18n.t('template_has_been_removed')
|
||||
else
|
||||
@template.update!(deleted_at: Time.current)
|
||||
@template.update!(archived_at: Time.current)
|
||||
|
||||
'Template has been archived.'
|
||||
I18n.t('template_has_been_archived')
|
||||
end
|
||||
|
||||
redirect_back(fallback_location: root_path, notice:)
|
||||
@@ -74,12 +93,20 @@ class TemplatesController < ApplicationController
|
||||
private
|
||||
|
||||
def template_params
|
||||
params.require(:template).permit(:name)
|
||||
end
|
||||
|
||||
def load_base_template
|
||||
return if params[:base_template_id].blank?
|
||||
|
||||
@base_template = current_account.templates.find_by(id: params[:base_template_id])
|
||||
params.require(:template).permit(
|
||||
:name,
|
||||
{ schema: [[:attachment_uuid, :google_drive_file_id, :name,
|
||||
{ conditions: [%i[field_uuid value action operation]] }]],
|
||||
submitters: [%i[name uuid is_requester linked_to_uuid invite_by_uuid optional_invite_by_uuid email order]],
|
||||
fields: [[:uuid, :submitter_uuid, :name, :type,
|
||||
:required, :readonly, :default_value,
|
||||
:title, :description, :prefillable,
|
||||
{ preferences: {},
|
||||
default_value: [],
|
||||
conditions: [%i[field_uuid value action operation]],
|
||||
options: [%i[value uuid]],
|
||||
validation: %i[message pattern min max step],
|
||||
areas: [%i[x y w h cell_w attachment_uuid option_uuid page]] }]] }
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -0,0 +1,96 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class TemplatesDashboardController < ApplicationController
|
||||
load_and_authorize_resource :template_folder, parent: false
|
||||
load_and_authorize_resource :template, parent: false
|
||||
|
||||
SHOW_TEMPLATES_FOLDERS_THRESHOLD = 9
|
||||
TEMPLATES_PER_PAGE = 12
|
||||
FOLDERS_PER_PAGE = 18
|
||||
|
||||
helper_method :selected_order
|
||||
|
||||
def index
|
||||
@template_folders =
|
||||
TemplateFolders.filter_active_folders(@template_folders.where(parent_folder_id: nil), @templates)
|
||||
|
||||
@template_folders = TemplateFolders.search(@template_folders, params[:q])
|
||||
@template_folders = TemplateFolders.sort(@template_folders, current_user, selected_order)
|
||||
|
||||
@pagy, @template_folders = pagy(
|
||||
@template_folders,
|
||||
limit: FOLDERS_PER_PAGE,
|
||||
page: @template_folders.count > SHOW_TEMPLATES_FOLDERS_THRESHOLD ? params[:page] : 1
|
||||
)
|
||||
|
||||
if @pagy.count > SHOW_TEMPLATES_FOLDERS_THRESHOLD
|
||||
@templates = @templates.none
|
||||
else
|
||||
@template_folders = @template_folders.reject { |e| e.name == TemplateFolder::DEFAULT_NAME }
|
||||
@templates = filter_templates(@templates).preload(:author, :template_accesses)
|
||||
@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, limit:)
|
||||
|
||||
load_related_submissions if params[:q].present? && @templates.blank?
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def filter_templates(templates)
|
||||
rel = templates.active
|
||||
|
||||
if params[:q].blank?
|
||||
if Docuseal.multitenant? ? current_account.testing? : current_account.linked_account_account
|
||||
shared_account_ids = [current_user.account_id]
|
||||
shared_account_ids << TemplateSharing::ALL_ID if !Docuseal.multitenant? && !current_account.testing?
|
||||
|
||||
shared_template_ids = TemplateSharing.where(account_id: shared_account_ids).select(:template_id)
|
||||
|
||||
rel = Template.where(
|
||||
Template.arel_table[:id].in(
|
||||
rel.where(folder_id: current_account.default_template_folder.id).select(:id).arel
|
||||
.union(:all, shared_template_ids.arel)
|
||||
)
|
||||
)
|
||||
else
|
||||
rel = rel.where(folder_id: current_account.default_template_folder.id)
|
||||
end
|
||||
end
|
||||
|
||||
Templates.search(current_user, rel, params[:q])
|
||||
end
|
||||
|
||||
def selected_order
|
||||
@selected_order ||=
|
||||
if cookies.permanent[:dashboard_templates_order].blank? ||
|
||||
(cookies.permanent[:dashboard_templates_order] == 'used_at' && can?(:manage, :countless))
|
||||
'created_at'
|
||||
else
|
||||
cookies.permanent[:dashboard_templates_order]
|
||||
end
|
||||
end
|
||||
|
||||
def load_related_submissions
|
||||
@related_submissions = Submission.accessible_by(current_ability)
|
||||
.left_joins(:template)
|
||||
.where(archived_at: nil)
|
||||
.where(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)
|
||||
|
||||
@related_submissions_pagy, @related_submissions = pagy_auto(@related_submissions, limit: 5)
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,67 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class TemplatesDebugController < ApplicationController
|
||||
load_and_authorize_resource :template
|
||||
|
||||
DEBUG_FILE = ''
|
||||
|
||||
def show
|
||||
schema_uuids = @template.schema.index_by { |e| e['attachment_uuid'] }
|
||||
attachment = @template.documents.find { |a| schema_uuids[a.uuid] }
|
||||
|
||||
data = attachment.download
|
||||
|
||||
unless attachment.image?
|
||||
pdf = HexaPDF::Document.new(io: StringIO.new(data))
|
||||
|
||||
fields = Templates::FindAcroFields.call(pdf, attachment, data)
|
||||
end
|
||||
|
||||
fields, = Templates::DetectFields.call(StringIO.new(data), attachment:) if fields.blank?
|
||||
|
||||
attachment.metadata['pdf'] ||= {}
|
||||
attachment.metadata['pdf']['fields'] = fields
|
||||
|
||||
@template.update!(fields: Templates::ProcessDocument.normalize_attachment_fields(@template, [attachment]))
|
||||
|
||||
debug_file if DEBUG_FILE.present?
|
||||
|
||||
ActiveRecord::Associations::Preloader.new(
|
||||
records: [@template],
|
||||
associations: [schema_documents: { preview_images_attachments: :blob }]
|
||||
).call
|
||||
|
||||
@template_data =
|
||||
@template.as_json.merge(
|
||||
documents: @template.schema_documents.as_json(
|
||||
methods: %i[metadata signed_uuid],
|
||||
include: { preview_images: { methods: %i[url metadata filename] } }
|
||||
)
|
||||
).to_json
|
||||
|
||||
render 'templates/edit', layout: 'plain'
|
||||
end
|
||||
|
||||
def debug_file
|
||||
tempfile = Tempfile.new
|
||||
tempfile.binmode
|
||||
tempfile.write(File.read(DEBUG_FILE))
|
||||
tempfile.rewind
|
||||
|
||||
filename = File.basename(DEBUG_FILE)
|
||||
|
||||
file = ActionDispatch::Http::UploadedFile.new(
|
||||
tempfile:,
|
||||
filename:,
|
||||
type: Marcel::MimeType.for(tempfile)
|
||||
)
|
||||
|
||||
params = { files: [file] }
|
||||
|
||||
documents = Templates::CreateAttachments.call(@template, params)
|
||||
|
||||
schema = documents.map { |doc| { attachment_uuid: doc.uuid, name: doc.filename.base } }
|
||||
|
||||
@template.update!(schema:)
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,30 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class TemplatesDetectFieldsController < ApplicationController
|
||||
include ActionController::Live
|
||||
|
||||
load_and_authorize_resource :template
|
||||
|
||||
def create
|
||||
response.headers['Content-Type'] = 'text/event-stream'
|
||||
|
||||
sse = SSE.new(response.stream)
|
||||
|
||||
documents = @template.schema_documents.preload(:blob)
|
||||
documents = documents.where(uuid: params[:attachment_uuid]) if params[:attachment_uuid].present?
|
||||
|
||||
page_number = params[:page].present? ? params[:page].to_i : nil
|
||||
|
||||
documents.each do |document|
|
||||
io = StringIO.new(document.download)
|
||||
|
||||
Templates::DetectFields.call(io, attachment: document, page_number:) do |(attachment_uuid, page, fields)|
|
||||
sse.write({ attachment_uuid:, page:, fields: })
|
||||
end
|
||||
end
|
||||
|
||||
sse.write({ completed: true })
|
||||
ensure
|
||||
response.stream.close
|
||||
end
|
||||
end
|
||||
@@ -6,12 +6,14 @@ class TemplatesFoldersController < ApplicationController
|
||||
def edit; end
|
||||
|
||||
def update
|
||||
@template.folder = TemplateFolders.find_or_create_by_name(current_user, params[:name])
|
||||
name = [params[:parent_name], params[:name]].compact_blank.join(' / ')
|
||||
|
||||
@template.folder = TemplateFolders.find_or_create_by_name(current_user, name)
|
||||
|
||||
if @template.save
|
||||
redirect_back(fallback_location: template_path(@template), notice: 'Document template has been moved')
|
||||
redirect_back(fallback_location: template_path(@template), notice: I18n.t('document_template_has_been_moved'))
|
||||
else
|
||||
redirect_back(fallback_location: template_path(@template), notice: 'Unable to move template into folder')
|
||||
redirect_back(fallback_location: template_path(@template), notice: I18n.t('unable_to_move_template_into_folder'))
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class TemplatesFormPreviewController < ApplicationController
|
||||
layout 'form'
|
||||
|
||||
load_and_authorize_resource :template
|
||||
|
||||
def show
|
||||
@submitter = Submitter.new(uuid: params[:uuid] || @template.submitters.first['uuid'],
|
||||
account: current_account,
|
||||
submission: @template.submissions.new(template_submitters: @template.submitters,
|
||||
account: current_account))
|
||||
|
||||
@submitter.submission.submitters = @template.submitters.map { |item| Submitter.new(uuid: item['uuid']) }
|
||||
|
||||
Submissions.preload_with_pages(@submitter.submission)
|
||||
|
||||
@attachments_index = ActiveStorage::Attachment.where(record: @submitter.submission.submitters, name: :attachments)
|
||||
.preload(:blob).index_by(&:uuid)
|
||||
|
||||
@form_configs = Submitters::FormConfigs.call(@submitter)
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,82 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class TemplatesPreferencesController < ApplicationController
|
||||
load_and_authorize_resource :template
|
||||
|
||||
RESETTABLE_PREFERENCE_KEYS = {
|
||||
AccountConfig::SUBMITTER_INVITATION_EMAIL_KEY => %w[request_email_subject request_email_body submitters],
|
||||
AccountConfig::SUBMITTER_INVITATION_REMINDER_EMAIL_KEY => %w[invitation_reminder_email_subject
|
||||
invitation_reminder_email_body],
|
||||
AccountConfig::SUBMITTER_DOCUMENTS_COPY_EMAIL_KEY => %w[documents_copy_email_subject documents_copy_email_body],
|
||||
AccountConfig::SUBMITTER_COMPLETED_EMAIL_KEY => %w[completed_notification_email_subject
|
||||
completed_notification_email_body]
|
||||
}.freeze
|
||||
|
||||
def show; end
|
||||
|
||||
def create
|
||||
authorize!(:update, @template)
|
||||
|
||||
@template.preferences = @template.preferences.merge(template_params[:preferences])
|
||||
@template.preferences = @template.preferences.reject { |_, v| (v.is_a?(String) || v.is_a?(Hash)) && v.blank? }
|
||||
@template.save!
|
||||
|
||||
head :ok
|
||||
end
|
||||
|
||||
def destroy
|
||||
authorize!(:update, @template)
|
||||
|
||||
config_key = params[:config_key]
|
||||
preferences_to_delete = RESETTABLE_PREFERENCE_KEYS[config_key]
|
||||
|
||||
return head :ok if preferences_to_delete.blank?
|
||||
|
||||
preferences_to_delete.each do |key|
|
||||
@template.preferences.delete(key)
|
||||
end
|
||||
|
||||
@template.save!
|
||||
|
||||
render turbo_stream: turbo_stream.replace("#{config_key}_form",
|
||||
partial: "templates_preferences/#{config_key}_form"),
|
||||
status: :ok
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def template_params
|
||||
params.require(:template).permit(
|
||||
preferences: %i[bcc_completed request_email_subject request_email_body
|
||||
invitation_reminder_email_subject invitation_reminder_email_body
|
||||
documents_copy_email_subject documents_copy_email_body
|
||||
documents_copy_email_enabled documents_copy_email_attach_audit
|
||||
documents_copy_email_attach_documents documents_copy_email_reply_to
|
||||
completed_notification_email_attach_documents
|
||||
completed_redirect_url validate_unique_submitters
|
||||
require_all_submitters submitters_order require_phone_2fa require_email_2fa
|
||||
default_expire_at_duration shared_link_2fa default_expire_at request_email_enabled
|
||||
completed_notification_email_subject completed_notification_email_body
|
||||
completed_notification_email_enabled completed_notification_email_attach_audit] +
|
||||
[completed_message: %i[title body],
|
||||
submitters: [%i[uuid request_email_subject request_email_body]], link_form_fields: []]
|
||||
).tap do |attrs|
|
||||
attrs[:preferences].delete(:submitters) if params[:request_email_per_submitter] != '1'
|
||||
|
||||
if (default_expire_at = attrs.dig(:preferences, :default_expire_at).presence)
|
||||
attrs[:preferences][:default_expire_at] =
|
||||
(ActiveSupport::TimeZone[current_account.timezone] || Time.zone).parse(default_expire_at).utc
|
||||
end
|
||||
|
||||
attrs[:preferences] = attrs[:preferences].transform_values do |value|
|
||||
if %w[true false].include?(value)
|
||||
value == 'true'
|
||||
elsif value.respond_to?(:compact_blank)
|
||||
value.compact_blank
|
||||
else
|
||||
value
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,26 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class TemplatesPrefillableFieldsController < ApplicationController
|
||||
PREFILLABLE_FIELD_TYPES = %w[text number cells date checkbox select radio phone].freeze
|
||||
|
||||
load_and_authorize_resource :template
|
||||
|
||||
def create
|
||||
authorize!(:update, @template)
|
||||
|
||||
field = @template.fields.find { |f| f['uuid'] == params[:field_uuid] }
|
||||
|
||||
if params[:prefillable] == 'false'
|
||||
field.delete('prefillable')
|
||||
field.delete('readonly')
|
||||
elsif params[:prefillable] == 'true'
|
||||
field['prefillable'] = true
|
||||
field['readonly'] = true
|
||||
end
|
||||
|
||||
@template.save!
|
||||
|
||||
render turbo_stream: turbo_stream.replace(:prefillable_fields_list, partial: 'list',
|
||||
locals: { template: @template })
|
||||
end
|
||||
end
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user