Compare commits

..
861 Commits
Author SHA1 Message Date
Alex Turchyn 8f8f69f0bc Merge from docusealco/wip 2026-10-19 14:57:12 +03:00
Pete Matsyburka 04663d55eb update deps 2026-10-19 11:10:48 +03:00
Pete Matsyburka 93d0b935e2 fix dynamic document 2026-10-17 13:17:52 +03:00
Pete Matsyburka 829b2b2855 disable jemalloc 2026-10-17 10:42:20 +03:00
Alex Turchyn cf6de444f4 disable the review form for native apps 2026-10-16 23:22:32 +03:00
Pete Matsyburka 126f5d58a2 convert docx 2026-10-16 20:18:10 +03:00
Pete Matsyburka 09b93067f4 fix close modal 2026-10-16 19:56:36 +03:00
Pete Matsyburka f14458f238 fix generate 2026-10-16 19:56:36 +03:00
Pete Matsyburka 445139f05c adjust mobile nav 2026-10-16 19:56:36 +03:00
Pete Matsyburka e5e4965f05 adjust kba 2026-10-16 19:56:36 +03:00
Pete Matsyburka 1c8f7b0316 use jemalloc 2026-10-15 15:28:54 +03:00
Alex Turchyn 9fe3656652 fix icons shrinking 2026-10-14 08:11:45 +03:00
Pete Matsyburka 17ae46bd61 make remember signature permanent 2026-10-13 16:45:42 +03:00
Pete Matsyburka a036483938 tenants test flag 2026-10-13 09:23:38 +03:00
Alex Turchyn 3c65b0e4ba Merge from docusealco/wip 2026-10-12 15:29:17 +03:00
Pete Matsyburka ced5ab67ab adjust generate previews 2026-10-12 09:33:42 +03:00
Pete Matsyburka 28c4177128 adjsut detailed send button 2026-10-11 17:52:54 +03:00
Pete Matsyburka 5d86a08b5f update send modal 2026-10-10 15:57:08 +03:00
Pete Matsyburka a69fb81429 fix multiselect 2026-10-09 12:57:12 +03:00
Pete Matsyburka e6ac3380d4 dynamic editor image drag 2026-10-09 12:25:21 +03:00
Pete Matsyburka 3a535e7112 adjust date timezone 2026-10-08 16:06:41 +03:00
Pete Matsyburka 7464f9cfad download event log 2026-10-08 10:04:12 +03:00
Pete Matsyburka 58e24ef758 update submitter slug 2026-10-08 10:04:01 +03:00
Pete Matsyburka c1d809f989 update demo 2026-10-07 09:00:58 +03:00
Pete Matsyburka 80b24f0e2a show signature when completed 2026-10-06 12:12:17 +03:00
Pete Matsyburka 2e253904a3 invalid email error 2026-10-06 11:01:04 +03:00
Pete Matsyburka a7f381caf0 update gem 2026-10-05 12:31:08 +03:00
Alex Turchyn c6a7555f54 Merge from docusealco/wip 2026-10-05 12:27:31 +03:00
Pete Matsyburka 0772880bbc download dynamic document 2026-10-05 11:53:57 +03:00
Pete Matsyburka 73339d8cd9 fix editor undo 2026-10-05 11:45:16 +03:00
Pete Matsyburka 25504e8cb1 update routes 2026-10-04 11:04:14 +03:00
Pete Matsyburka 63a874528a improve text selection 2026-10-03 20:07:24 +03:00
Pete Matsyburka c1c99621ff sticky table tools 2026-10-03 18:35:29 +03:00
Pete Matsyburka 5c168f4a29 add page number style 2026-10-03 14:57:11 +03:00
Pete Matsyburka bef825c4e0 optimize add list style 2026-10-03 14:05:38 +03:00
Pete Matsyburka a990d22831 remove pinch to zoom 2026-10-03 14:05:38 +03:00
Pete Matsyburka 1687a372ae adjust editor 2026-10-03 14:05:38 +03:00
Alex Turchyn 9b267d4a07 upload inserted images as attachments 2026-10-03 14:05:38 +03:00
Pete Matsyburka 2189c2e9fe adjust fonts 2026-10-03 14:05:38 +03:00
Alex Turchyn 1e43fe8969 use Word line spacing units and join adjacent lists 2026-10-03 14:05:38 +03:00
Alex Turchyn f77a716658 remove spacing between list items like Word 2026-10-03 14:05:38 +03:00
Alex Turchyn a70ac571e3 simplify dynamic editor code 2026-10-03 14:05:38 +03:00
Alex Turchyn 4f5e72f983 fix table editing 2026-10-03 14:05:38 +03:00
Alex Turchyn 8f47ec6d4e add accessible names to table size counters 2026-10-03 14:05:38 +03:00
Alex Turchyn 9e1bbf2944 fix image resize handles 2026-10-03 14:05:38 +03:00
Alex Turchyn aa7d6e9aaa add pinch zoom and fix touch editing 2026-10-03 14:05:38 +03:00
Alex Turchyn 57c8be33ce adjust dynamic editor menus on mobile 2026-10-03 14:05:38 +03:00
Alex Turchyn ed7109bfc6 store list styles in dynamic document 2026-10-03 14:05:38 +03:00
Alex Turchyn 9e2590489b improve insert menu accessibility 2026-10-03 14:05:38 +03:00
Alex Turchyn d793f00b37 improve dynamic editor 2026-10-03 14:05:38 +03:00
Pete Matsyburka 5896c9a2aa strip edited image 2026-10-03 09:10:31 +03:00
Pete Matsyburka 78b0ce9828 add iban validation 2026-10-01 19:45:55 +03:00
Pete Matsyburka e187644716 refactor otp 2026-10-01 08:57:19 +03:00
Pete Matsyburka a847e7aa9c custom otp email 2026-09-30 19:56:12 +03:00
Pete Matsyburka 0905e7d215 reduce limit 2026-09-30 19:23:58 +03:00
Pete Matsyburka 91147d2f48 fix verify pdf 2026-09-30 19:22:41 +03:00
Pete Matsyburka acce6195b0 rescue validation errors 2026-09-30 13:38:47 +03:00
Pete Matsyburka ce9ab691ec fix document metadata 2026-09-30 12:57:48 +03:00
Pete Matsyburka 4190544edc use ivar 2026-09-28 23:28:13 +03:00
Pete Matsyburka db8e5efbd5 adjust i18n 2026-09-28 17:54:47 +03:00
Pete Matsyburka a5c6f4e40c update gem 2026-09-28 15:32:26 +03:00
Pete Matsyburka 7537d05d1f fix builder resize 2026-09-28 15:30:09 +03:00
Alex Turchyn 44dcc7f312 Merge from docusealco/wip 2026-09-28 14:39:31 +03:00
Pete Matsyburka 218c22da21 add ratelimit 2026-09-28 09:40:15 +03:00
Pete Matsyburka a8433e9bd3 adjust devise lock 2026-09-28 09:26:40 +03:00
Pete Matsyburka 800101f747 add i18n 2026-09-27 11:19:32 +03:00
Pete Matsyburka f637ee907c update routes 2026-09-27 09:43:48 +03:00
Pete Matsyburka 6a9b123cdc adjust verify pdf 2026-09-26 19:57:12 +03:00
Pete Matsyburka 0a2f8dfab3 validate 2fa email 2026-09-26 17:16:37 +03:00
Pete Matsyburka 3dc00ed7fc adjust 2fa rate limit 2026-09-26 17:09:36 +03:00
Pete Matsyburka af160b3f3f fix edit image 2026-09-26 17:07:47 +03:00
Pete Matsyburka 1a9e4026d9 guard test mode key only 2026-09-26 17:06:38 +03:00
Pete Matsyburka 5ef32bcf9e non prefillable types 2026-09-26 16:00:28 +03:00
Pete Matsyburka 2d6f4c521e refactor prefill 2026-09-26 15:17:33 +03:00
Pete Matsyburka 439f95d87e date formulas 2026-09-26 11:25:22 +03:00
Pete Matsyburka ca7b9671f2 refactor routes 2026-09-26 11:25:21 +03:00
Pete Matsyburka 20ec614d3a remove enquiries_controller 2026-09-25 17:18:29 +03:00
Pete Matsyburka 887ebbbcf6 adjust zoom 2026-09-25 17:06:49 +03:00
Pete Matsyburka 4225295295 fix editor 2026-09-25 10:36:26 +03:00
Pete Matsyburka 89367bc905 add i18n 2026-09-25 09:34:39 +03:00
Pete Matsyburka 4bacaa26e1 fix revisions 2026-09-25 08:52:35 +03:00
Pete Matsyburka 0f9fcf3fdc one field per party validation 2026-09-24 19:56:31 +03:00
Pete Matsyburka 437f61dc26 adjust html title 2026-09-24 16:13:25 +03:00
Pete Matsyburka e55542a000 adjust editor 2026-09-24 10:27:11 +03:00
Pete Matsyburka e004ac2d40 fix ui 2026-09-23 19:57:08 +03:00
Pete Matsyburka 92bfedd01d fix payment links 2026-09-23 18:48:24 +03:00
Pete Matsyburka e7fd1e4006 fix search button 2026-09-23 17:00:17 +03:00
Pete Matsyburka 56bba60d3f use open mobile modal 2026-09-23 16:38:05 +03:00
Pete Matsyburka 6b6a917f60 remove respond_to html 2026-09-23 11:00:18 +03:00
Pete Matsyburka df7b3f9dfe format html 2026-09-23 11:00:18 +03:00
Pete Matsyburka 1b21b0c65b adjust ui 2026-09-23 11:00:18 +03:00
Pete Matsyburka 258133d65c fix ci 2026-09-23 11:00:18 +03:00
Pete Matsyburka 8f33c8d8b6 adjust ui 2026-09-23 11:00:18 +03:00
Pete Matsyburka e11f4bddbe review fixes 2026-09-23 11:00:18 +03:00
Pete Matsyburka d071e8c429 ui fixes 2026-09-23 11:00:17 +03:00
Pete Matsyburka 79aa034183 native prefetch 2026-09-23 11:00:17 +03:00
Pete Matsyburka f5dad673de adjust ui 2026-09-23 11:00:17 +03:00
Pete Matsyburka 7e8375aee8 fix dropdowns 2026-09-23 11:00:17 +03:00
Pete Matsyburka 1999611b26 fix share modal 2026-09-23 11:00:17 +03:00
Pete Matsyburka 188c3a8c84 adjust template dropdown 2026-09-23 11:00:17 +03:00
Pete Matsyburka d35538e2ba adjust native 2026-09-23 11:00:17 +03:00
Pete Matsyburka a86f896694 remove focus lost 2026-09-23 11:00:17 +03:00
Pete Matsyburka 35eeeba0c3 adjust settings ui 2026-09-23 11:00:17 +03:00
Alex Turchyn e3e5c237d4 adjust mobile tables 2026-09-23 11:00:17 +03:00
Pete Matsyburka 9e89cb92a9 fix morph 2026-09-23 11:00:17 +03:00
Pete Matsyburka 827850646d use background color 2026-09-23 11:00:17 +03:00
Pete Matsyburka eafae4cda2 add haptics 2026-09-23 11:00:17 +03:00
Pete Matsyburka e012b7f5ac adjust title 2026-09-23 11:00:17 +03:00
Pete Matsyburka da9e7c6366 add send menu 2026-09-23 11:00:17 +03:00
Pete Matsyburka 20cc5d1e2e adjust mobile dropdown 2026-09-23 11:00:17 +03:00
Pete Matsyburka ae2cedbd70 drawer close 2026-09-23 11:00:17 +03:00
Pete Matsyburka 1f46f74400 adjust modal title 2026-09-23 11:00:17 +03:00
Pete Matsyburka 930445258d native dropdown 2026-09-23 11:00:17 +03:00
Pete Matsyburka be533e492e visit replace 2026-09-23 11:00:17 +03:00
Pete Matsyburka 66758d45bc adjust native 2026-09-23 11:00:17 +03:00
Alex Turchyn 6f31bd7af8 hide google drive in native app 2026-09-23 11:00:17 +03:00
Pete Matsyburka f4d2fa4c28 lock swipe 2026-09-23 11:00:17 +03:00
Pete Matsyburka 5d3369d556 fix download 2026-09-23 11:00:17 +03:00
Pete Matsyburka 5ea55c733f fix style 2026-09-23 11:00:17 +03:00
Pete Matsyburka 547bc9aa94 adjust mobile ui 2026-09-23 11:00:17 +03:00
Pete Matsyburka 53ca45a67a adjust flash 2026-09-23 11:00:17 +03:00
Pete Matsyburka ef28b80589 fix infinite scroll reload 2026-09-23 11:00:17 +03:00
Pete Matsyburka 05fa75f9a2 adjust downlaod 2026-09-23 11:00:17 +03:00
Pete Matsyburka 87b57279a4 update submission card 2026-09-23 11:00:17 +03:00
Pete Matsyburka f09732df90 share via copy 2026-09-23 11:00:17 +03:00
Pete Matsyburka a588c2f4a4 adjust signing form 2026-09-23 11:00:17 +03:00
Pete Matsyburka 960d36f628 add submission tabs 2026-09-23 11:00:17 +03:00
Pete Matsyburka 456dbae1cd use turbo remove 2026-09-23 11:00:17 +03:00
Pete Matsyburka 0fc1c0d1ed fix owerflow 2026-09-23 11:00:17 +03:00
Pete Matsyburka 714324ea99 native refresh 2026-09-23 11:00:17 +03:00
Pete Matsyburka 5a29ed22db turbo cache control 2026-09-23 11:00:17 +03:00
Pete Matsyburka 061580594c fix flash and morph 2026-09-23 11:00:17 +03:00
Alex Turchyn 8723ce7f7a migrate to turbo 8 2026-09-23 10:59:54 +03:00
Pete Matsyburka 522df7f476 fix overlap 2026-09-23 10:59:54 +03:00
Pete Matsyburka 026637c5ab show fields list on tablet 2026-09-23 10:59:54 +03:00
Pete Matsyburka fde993a579 adjust payment settings 2026-09-23 10:59:54 +03:00
Pete Matsyburka a960ea268a adjust builder 2026-09-23 10:59:54 +03:00
Pete Matsyburka d59776e8f5 adjust template preview 2026-09-23 10:59:54 +03:00
Pete Matsyburka 27e5c2c81e native modals 2026-09-23 10:59:54 +03:00
Alex Turchyn 0609a70544 upload native files via the page form 2026-09-23 10:59:54 +03:00
Pete Matsyburka 3ed15e383b adjust mobile builder 2026-09-23 10:59:54 +03:00
Pete Matsyburka d29be7c2cf add builder drawer 2026-09-23 10:59:54 +03:00
Pete Matsyburka 79f72c92b3 native platform prop 2026-09-23 10:59:54 +03:00
Pete Matsyburka 7e6331d1ad fix modal size 2026-09-23 10:59:54 +03:00
Pete Matsyburka 0986dfba73 fix native title 2026-09-23 10:59:54 +03:00
Alex Turchyn 7cf0b5aaf7 hide pro badge in the iOS app 2026-09-23 10:59:54 +03:00
Pete Matsyburka ae5f87f8fb add search 2026-09-23 10:59:54 +03:00
Pete Matsyburka 8259ebdb6c archived nav 2026-09-23 10:59:54 +03:00
Pete Matsyburka a4ca0abba1 adjsut card size 2026-09-23 10:59:54 +03:00
Pete Matsyburka ca009252ac rename template 2026-09-23 10:59:54 +03:00
Pete Matsyburka 9678cd1286 add sort menu 2026-09-23 10:59:54 +03:00
Pete Matsyburka 8cb0a7a020 add infinite scroll 2026-09-23 10:59:54 +03:00
Pete Matsyburka d3cc2e1ca8 adjust style 2026-09-23 10:59:54 +03:00
Pete Matsyburka 08ac60c7e6 adjust signign form 2026-09-23 10:59:54 +03:00
Alex Turchyn 49afb2e87c make template form preview native 2026-09-23 10:59:54 +03:00
Alex Turchyn be621339c3 expand native sheet when content overflows 2026-09-23 10:59:54 +03:00
Alex Turchyn 5f5b49a1ba open self sign form in the app 2026-09-23 10:59:54 +03:00
Alex Turchyn c0dd4b85ed hide pro badges only in iOS app 2026-09-23 10:59:54 +03:00
Alex Turchyn 95f7cd5220 ignore get forms in native-modal 2026-09-23 10:59:54 +03:00
Alex Turchyn f0753de99e hide pro badge in the native app 2026-09-23 10:59:54 +03:00
Pete Matsyburka 38ecda82fa adjust nav 2026-09-23 10:59:54 +03:00
Pete Matsyburka 073859a4b7 adjust nav 2026-09-23 10:59:54 +03:00
Pete Matsyburka 44972fcc39 fix folder nav 2026-09-23 10:59:54 +03:00
Pete Matsyburka 1232dfb248 fix folder nav 2026-09-23 10:59:54 +03:00
Pete Matsyburka 5de334522c adjust settings 2026-09-23 10:59:54 +03:00
Pete Matsyburka 7a6b4acea6 native navbar 2026-09-23 10:59:54 +03:00
Alex Turchyn 198a599a2b add native-modal element 2026-09-23 10:59:54 +03:00
Alex Turchyn d36f40cafd make template editor header native 2026-09-23 10:59:54 +03:00
Alex Turchyn 9abdb68bc5 make submission header buttons native 2026-09-23 10:59:54 +03:00
Alex Turchyn 359f695eb5 declare search for native app 2026-09-23 10:59:54 +03:00
Alex Turchyn 2b9e5a86b6 align native sheet padding with pages 2026-09-23 10:59:54 +03:00
Alex Turchyn c07d76e2a4 open all modals as sheets in the native app 2026-09-23 10:59:54 +03:00
Alex Turchyn f5b29642ee make template preferences button native 2026-09-23 10:59:54 +03:00
Alex Turchyn 3f6cdcd15b remove native-header element 2026-09-23 10:59:53 +03:00
Alex Turchyn bde8097288 add json sign in for native app 2026-09-23 10:59:53 +03:00
Alex Turchyn 617d75a4ed add native titles 2026-09-23 10:59:53 +03:00
Alex Turchyn aa1874cdb4 adjust more UI elevent for native app 2026-09-23 10:59:53 +03:00
Alex Turchyn 22b2e7475a add native app 2026-09-23 10:59:53 +03:00
Pete Matsyburka ad3367dae0 add date placeholder 2026-09-23 10:01:51 +03:00
Alex Turchyn 47c090e1f1 Merge from docusealco/wip 2026-09-21 14:53:39 +03:00
Pete Matsyburka 678997fb36 adjust embed script 2026-09-21 09:06:26 +03:00
Pete Matsyburka 24f195c7e5 normalize extension 2026-09-21 09:06:25 +03:00
Pete Matsyburka 5741bb4db2 normalize page index 2026-09-20 17:37:02 +03:00
Pete Matsyburka 1d169668f0 fix ci 2026-09-20 14:58:06 +03:00
Pete Matsyburka ef3d5bec7e mcp use template access 2026-09-20 13:30:28 +03:00
Pete Matsyburka d578ca3cc9 only dss changes 2026-09-20 13:30:28 +03:00
Pete Matsyburka 5432978c0d adjust load vips 2026-09-20 13:30:27 +03:00
Pete Matsyburka de1af49348 fix variable highlight 2026-09-18 09:18:50 +03:00
Alex Turchyn 2f0bcb4a80 hide tooltips on mobile 2026-09-17 23:45:59 +03:00
Pete Matsyburka 11f27c8cf0 reset rotation 2026-09-14 16:06:16 +03:00
Alex Turchyn 6e2430e5a2 Merge from docusealco/wip 2026-09-14 15:54:52 +03:00
Pete Matsyburka 85fbae1813 validate webhook header 2026-09-14 15:07:56 +03:00
Pete Matsyburka 9941122d90 adjust i18n 2026-09-14 11:08:40 +03:00
Pete Matsyburka 71be6db358 adjust delegated values 2026-09-12 18:34:39 +03:00
Pete Matsyburka 994c1b3155 keep pasted field roles 2026-09-12 18:29:56 +03:00
Pete Matsyburka 92b297a620 fix friendly name 2026-09-11 13:00:26 +03:00
Alex Turchyn bd03498b0f adjust show archived 2026-09-11 09:56:10 +03:00
Pete Matsyburka 660b24710e do not email archived user 2026-09-10 19:17:27 +03:00
Pete Matsyburka 1416cf72e8 sms retry 30 seconds 2026-09-10 15:29:27 +03:00
Alex Turchyn ae7b0ce608 fix settings collapse spacing 2026-09-09 16:09:19 +03:00
Pete Matsyburka a3828ec390 xlsx write native boolean 2026-09-09 13:35:20 +03:00
Pete Matsyburka b8c1691bd7 optimize xlsx export 2026-09-09 12:46:28 +03:00
Pete Matsyburka 4b00c896e4 reset signature on delegate 2026-09-08 18:52:12 +03:00
Pete Matsyburka 781da32a07 replace filename slashes 2026-09-08 13:53:35 +03:00
Pete Matsyburka 307036eae6 fix invite party 2026-09-08 09:12:47 +03:00
Pete Matsyburka 9944a9b1a5 adjust bind address 2026-09-07 17:57:07 +03:00
Alex Turchyn 11856a17ac Merge from docusealco/wip 2026-09-07 12:53:23 +03:00
Pete Matsyburka 7b4c74d711 safe page cycle 2026-09-06 19:25:34 +03:00
Pete Matsyburka e0b0d4e803 add authorize 2026-09-06 19:25:34 +03:00
Pete Matsyburka ec04a95e36 fix pdf validation 2026-09-06 09:58:19 +03:00
Pete Matsyburka 887d80f6d7 raw page size 2026-09-05 13:09:09 +03:00
Pete Matsyburka 9ad281d457 handle 2fa email bounce 2026-09-04 09:01:51 +03:00
Pete Matsyburka e316d2006f subject only email message 2026-09-03 18:59:33 +03:00
Pete Matsyburka f9669b0e16 process pdf v2 2026-09-03 11:48:54 +03:00
Pete Matsyburka 785eb789ad fix undo autosave false 2026-09-02 20:42:08 +03:00
Pete Matsyburka 436d947f6a detect fields autosave false 2026-09-02 20:08:42 +03:00
Pete Matsyburka 033ce3a169 fix text align 2026-09-02 20:02:57 +03:00
Alex Turchyn c216e43d24 Merge from docusealco/wip 2026-08-31 11:35:27 +03:00
Pete Matsyburka d2373c1578 fix normalize email 2026-08-31 10:13:17 +03:00
Pete Matsyburka 474603e1c9 hide list form buttons 2026-08-30 18:51:35 +03:00
Pete Matsyburka 1fc1444b8f optimize load bmp 2026-08-30 15:49:54 +03:00
Pete Matsyburka 47361da26f adjust validate required 2026-08-30 15:36:49 +03:00
Pete Matsyburka 11dea042ad adjust test mode auth 2026-08-30 12:07:05 +03:00
Pete Matsyburka 8756931638 render locked page 2026-08-30 12:01:54 +03:00
Pete Matsyburka 180b201dcf fix mfa rate limit 2026-08-30 12:01:54 +03:00
Pete Matsyburka a1c30c2093 update tools verify 2026-08-30 11:04:45 +03:00
Pete Matsyburka 66bf6af7ba adjust verify pdf signature 2026-08-30 10:56:34 +03:00
Pete Matsyburka 1a65e7bcff adjust download route 2026-08-29 21:53:01 +03:00
Pete Matsyburka 59b5f03948 fix buttons order 2026-08-29 20:18:34 +03:00
Pete Matsyburka 4626518c83 fix search 2026-08-29 20:06:14 +03:00
Pete Matsyburka c18e263f34 adjust verify pdf signature 2026-08-29 20:06:13 +03:00
Pete Matsyburka c3dae1bfe4 add size checks 2026-08-29 16:29:52 +03:00
Pete Matsyburka b44152fd52 fix search index 2026-08-29 16:29:52 +03:00
Pete Matsyburka 2889a7c937 squish name 2026-08-29 16:29:52 +03:00
Pete Matsyburka 91e65e4a19 adjust prefill 2026-08-29 16:29:52 +03:00
Pete Matsyburka 2a1a2cbd82 fix generation 2026-08-28 19:15:48 +03:00
Pete Matsyburka 99b3247cf7 archive account 2026-08-25 18:07:48 +03:00
Pete Matsyburka fa91c6d6be fix mobile filters 2026-08-25 15:12:19 +03:00
Alex Turchyn 27a0d10525 Merge from docusealco/wip 2026-08-25 13:46:49 +03:00
Pete Matsyburka d25f0812ca reset decline values 2026-08-24 16:38:05 +03:00
Pete Matsyburka 9e2aa6f8c8 pass fetch options 2026-08-24 14:43:13 +03:00
Pete Matsyburka 11c76bf554 refactor 2026-08-24 09:47:34 +03:00
Pete Matsyburka e5733d88c0 remove uuids scope 2026-08-23 22:15:02 +03:00
Pete Matsyburka 6d38c9f375 adjust resubmit 2026-08-23 21:23:27 +03:00
Pete Matsyburka 91ec29ad2c add i18n 2026-08-23 19:23:05 +03:00
Pete Matsyburka d374c7fa10 fix log 2026-08-23 19:04:18 +03:00
Pete Matsyburka ca0daf6d0b normalize 2fa email 2026-08-23 18:52:12 +03:00
Pete Matsyburka 076c3d805a fix find_or_initialize_submitter 2026-08-23 18:49:32 +03:00
Pete Matsyburka 1f149ac67a new form if fields changed 2026-08-23 18:01:00 +03:00
Pete Matsyburka 616f7b4fd8 adjsut resubmit 2026-08-23 17:48:04 +03:00
Pete Matsyburka 6f4527038b fix redirect 2026-08-23 17:38:17 +03:00
Pete Matsyburka 8ed2883504 adjust email 2fa form 2026-08-23 17:34:17 +03:00
Pete Matsyburka 651b84a460 fix redirect status 2026-08-23 17:24:44 +03:00
Pete Matsyburka 73c4f158ff add self source 2026-08-23 17:19:50 +03:00
Pete Matsyburka 6f1cc34433 adjust log 2026-08-23 16:51:21 +03:00
Pete Matsyburka a77029ffcb start form remember session 2026-08-23 16:51:15 +03:00
Pete Matsyburka 0480572ef5 refactor start form 2026-08-23 15:45:43 +03:00
Pete Matsyburka 807463c465 optimize shared link 2026-08-23 15:45:43 +03:00
Pete Matsyburka 0f61053976 remove secrets yml 2026-08-23 11:50:12 +03:00
Pete Matsyburka b8cf525034 fix cycle safe 2026-08-21 18:56:22 +03:00
Pete Matsyburka fef203daed adjust notification settings 2026-08-21 17:45:19 +03:00
Pete Matsyburka 41b38cc594 fix js error on redirect 2026-08-21 12:00:29 +03:00
Pete Matsyburka 4e4354af99 fix RTL reverse 2026-08-20 17:26:54 +03:00
Pete Matsyburka 113b61588b process pdf from doc 2026-08-20 13:21:24 +03:00
Pete Matsyburka c1aac6b763 add stats 2026-08-19 12:58:24 +03:00
Alex Turchyn f85fd3cff7 add personalization tooltips 2026-08-18 14:24:55 +03:00
Alex Turchyn 004a22c1c8 Merge from docusealco/wip 2026-08-17 12:31:40 +03:00
Pete Matsyburka 69343b1bdd fix send completed emails 2026-08-17 09:16:20 +03:00
Pete Matsyburka 81a306928c fix ci 2026-08-16 10:17:48 +03:00
Alex Turchyn 2bf3d42ed6 add translations 2026-08-16 09:51:41 +03:00
Pete Matsyburka eec08266c9 fix page preview open 2026-08-16 09:45:04 +03:00
Pete Matsyburka cbbc75b485 fix CI 2026-08-15 22:09:08 +03:00
Pete Matsyburka 3d93bbe5aa improve flatten 2026-08-15 21:08:51 +03:00
Pete Matsyburka 5308e3e15b process attachment v2 2026-08-15 21:08:35 +03:00
Pete Matsyburka 22c146d564 adjust pdfium 2026-08-15 19:08:54 +03:00
Pete Matsyburka c8d3af9841 import acro form 2026-08-15 19:08:54 +03:00
Pete Matsyburka ee1d45d461 update dep 2026-08-15 19:08:54 +03:00
Pete Matsyburka 27d075ed63 optimize merge pdf 2026-08-15 19:08:54 +03:00
Pete Matsyburka df63acf0c8 optimize decrypt 2026-08-15 19:08:52 +03:00
Alex Turchyn 185827c951 add Pdfium acro fields detector 2026-08-14 15:29:41 +03:00
Pete Matsyburka e361cbabf4 adjust verify signature 2026-08-14 15:29:41 +03:00
Pete Matsyburka e867bf5c48 use json serializer 2026-08-14 12:16:25 +03:00
Pete Matsyburka 726d0ea4f3 validate url 2026-08-13 15:46:43 +03:00
Pete Matsyburka 90be34b243 update gem 2026-08-11 22:05:14 +03:00
Pete Matsyburka e87c06d811 uniq bcc addresses 2026-08-11 17:23:28 +03:00
Alex Turchyn 54454d1dfc Merge from docusealco/wip 2026-08-11 17:16:45 +03:00
Pete Matsyburka 726174deff authorize reminder config 2026-08-11 08:55:17 +03:00
Pete Matsyburka 52eb8ac863 add log 2026-08-10 15:21:22 +03:00
Pete Matsyburka 97d91d8f27 fix view only download 2026-08-10 12:06:42 +03:00
Pete Matsyburka dd358a8401 fix infinite loop 2026-08-10 11:48:05 +03:00
Pete Matsyburka 8e254b7d3c fix infinite loop 2026-08-10 06:01:20 +03:00
Pete Matsyburka b9018fa442 add completed download scope 2026-08-09 20:14:31 +03:00
Pete Matsyburka 09310f3690 guard save template message 2026-08-09 19:53:19 +03:00
Pete Matsyburka 2dd38260ba guard email 2fa send 2026-08-09 19:51:07 +03:00
Pete Matsyburka ffb3247cdc add authorize user 2026-08-09 18:37:53 +03:00
Pete Matsyburka c59c470321 sanitize search query 2026-08-09 16:01:12 +03:00
Pete Matsyburka f35510ffd6 adjust with reason 2026-08-08 09:00:35 +03:00
Pete Matsyburka eeec852b3b adjust preview cache 2026-08-07 21:54:55 +03:00
Pete Matsyburka 485b7f2c8f fix dynamic variable parse 2026-08-07 14:19:28 +03:00
Alex Turchyn b6498ec049 add PayPal translations 2026-08-07 10:12:52 +03:00
Pete Matsyburka dc1de72e18 fix mysql migrations 2026-08-06 17:50:03 +03:00
Pete Matsyburka 7dddef4c94 fix date time formats 2026-08-04 11:07:13 +03:00
Alex Turchyn a2d8b85549 Merge from docusealco/wip 2026-08-03 12:53:50 +03:00
Pete Matsyburka 4d68783b82 add counters 2026-08-03 10:05:09 +03:00
Pete Matsyburka b05c3b2030 fix autocomplete 2026-08-03 09:41:08 +03:00
Pete Matsyburka 784ae9aa4b fix dynamic document preview 2026-08-03 08:19:35 +03:00
Pete Matsyburka 03b5956cbc bcc limit 2026-08-03 07:42:53 +03:00
Pete Matsyburka 2876c9a18e adjust pagination 2026-08-01 08:38:49 +03:00
Pete Matsyburka c4b7be3925 fix mobile tabs 2026-07-31 12:46:34 +03:00
Pete Matsyburka e2ffbdac1d update gem 2026-07-31 11:43:40 +03:00
Pete Matsyburka 0d197bb2eb fix mobile modal 2026-07-31 10:57:37 +03:00
Pete Matsyburka 55089bee7f adjust pagination 2026-07-31 10:53:34 +03:00
Pete Matsyburka b96fe74db1 adjust filters 2026-07-31 10:46:24 +03:00
Pete Matsyburka 2fc1ba3037 fix layout 2026-07-31 09:23:18 +03:00
Pete Matsyburka c67d37207e update deps 2026-07-30 20:51:46 +03:00
Pete Matsyburka dba50cd1bf mobile fixes 2026-07-30 20:21:40 +03:00
Pete Matsyburka edf2c0d9ed update gem 2026-07-30 18:47:44 +03:00
Pete Matsyburka 35f7c36f28 rename settings index 2026-07-30 18:13:55 +03:00
Pete Matsyburka 3a24ecac63 Revert "break long names in template and folder titles"
This reverts commit 59fe8a7917.
2026-07-30 18:13:55 +03:00
Pete Matsyburka 50ec7682b5 fix open modal mobile 2026-07-30 18:13:54 +03:00
Pete Matsyburka 3667993caf center settings footer 2026-07-30 16:54:36 +03:00
Alex Turchyn 68e8cd1188 fix settings nav offset 2026-07-30 16:45:56 +03:00
Alex Turchyn 3eebf6d2cf unify tooltip and breakpoint classes 2026-07-30 16:45:56 +03:00
Alex Turchyn 34d0e00910 use numeric keyboard for otp code inputs 2026-07-30 16:45:56 +03:00
Alex Turchyn b8ab7190d2 use dvh for modal and drawer 2026-07-30 16:45:56 +03:00
Alex Turchyn 59fe8a7917 break long names in template and folder titles 2026-07-30 16:45:56 +03:00
Alex Turchyn d56070827b fix broken svg 2026-07-30 16:45:56 +03:00
Alex Turchyn 74dbd4bed3 fix trusted signature text 2026-07-30 16:45:56 +03:00
Alex Turchyn 807e82b21e open modals as pages on mobile devices 2026-07-30 16:45:56 +03:00
Alex Turchyn 66862e93c8 unify breakpoint classes 2026-07-30 16:45:56 +03:00
Alex Turchyn 270461ad3b add mobile filters 2026-07-30 16:45:56 +03:00
Alex Turchyn 3edd1ab32d fix invalid classes and html attributes 2026-07-30 16:45:56 +03:00
Alex Turchyn e77998e53c scale down settings titles on mobile 2026-07-30 16:45:56 +03:00
Alex Turchyn 1421bc2bce fix horizontal scroll from preferences tooltip 2026-07-30 16:45:56 +03:00
Alex Turchyn cbbbfdda80 add mobile settings navigation 2026-07-30 16:45:56 +03:00
Alex Turchyn 2d510081be fix esign signature row layout 2026-07-30 16:45:56 +03:00
Alex Turchyn 704893ee1d show view archived link in mobile pagination 2026-07-30 16:45:56 +03:00
Alex Turchyn 88612913a6 wrap smtp security radios on small screens 2026-07-30 16:45:56 +03:00
Alex Turchyn ce6823ed38 hide tooltip wrappers with hidden content 2026-07-30 16:45:56 +03:00
Alex Turchyn 9074d23b3c fix uneven template action buttons 2026-07-30 16:45:56 +03:00
Alex Turchyn 76d0e76fd5 no touch tooltips on nav elements 2026-07-30 16:45:56 +03:00
Pete Matsyburka 5ec8e4ccc7 text formula 2026-07-30 11:55:59 +03:00
Alex TurchynandPete Matsyburka b8c29324b8 paypal configs
Co-authored-by: Pete Matsyburka <pete@docuseal.com>
2026-07-30 07:56:07 +03:00
Pete Matsyburka 972f38242e update rails 2026-07-29 21:53:03 +03:00
Alex Turchyn 7ce6c29f4d Merge from docusealco/wip 2026-07-27 15:56:18 +03:00
Pete Matsyburka 2c6ddd7f90 rate limit remove 2fa 2026-07-27 15:48:04 +03:00
Pete Matsyburka ae50b2e323 add i18n 2026-07-27 14:56:08 +03:00
Pete Matsyburka 152cbc11a6 fix specs 2026-07-26 17:44:01 +03:00
Pete Matsyburka 3a406de655 adjust release tag 2026-07-26 09:37:03 +03:00
Pete Matsyburka 2e31cda08c adjust url pattern 2026-07-26 08:26:32 +03:00
Pete Matsyburka 094b7f47bd set forwarded priority 2026-07-25 20:12:18 +03:00
Pete Matsyburka 99ac349ecc sanitize dynamic document attributes 2026-07-25 19:46:41 +03:00
Pete Matsyburka 8830e03cf3 adjust cert config 2026-07-25 14:46:45 +03:00
Pete Matsyburka 706f3d6d65 fix image load 2026-07-25 13:51:55 +03:00
Pete Matsyburka 12b49f5fa7 log os 2026-07-25 10:53:41 +03:00
Pete Matsyburka 85327760b9 refactor mcp 2026-07-25 10:04:44 +03:00
Pete Matsyburka c94ed9c3d4 update gem 2026-07-24 23:05:57 +03:00
Pete Matsyburka bc50735d2a update gem 2026-07-23 14:13:55 +03:00
Pete Matsyburka d05e125fa9 use :has selectors for search input 2026-07-23 11:13:10 +03:00
Alex Turchyn 48d595ca81 full width search input on mobile devices 2026-07-23 10:36:45 +03:00
Pete Matsyburka a156de5e75 optional initials 2026-07-22 08:45:20 +03:00
Pete Matsyburka a7003bf4d0 bump stars 2026-07-21 13:10:36 +03:00
Alex Turchyn 5fe75c84ff Merge from docusealco/wip 2026-07-20 14:01:14 +03:00
Alex Turchyn 4549a04fc5 disable turbo on sign out form 2026-07-18 14:27:45 +03:00
Pete Matsyburka 0ea6b912a3 file download variables 2026-07-17 12:30:15 +03:00
Pete Matsyburka 9b86fa3d40 add date formats config 2026-07-17 11:27:44 +03:00
Pete Matsyburka 374f66c59f send view only 2026-07-17 11:12:47 +03:00
Pete Matsyburka da43d881f1 edit message per 10 parties 2026-07-17 09:34:43 +03:00
Pete Matsyburka 9cbfa3a0ba fix condition on complete 2026-07-17 00:30:06 +03:00
Pete Matsyburka 77a925f33b fix invite form return to fields 2026-07-16 15:55:21 +03:00
Pete Matsyburka 3f7d7beedd fix sharing toggle 2026-07-15 10:55:04 +03:00
Pete Matsyburka 40d91df732 Revert "adjust shared query"
This reverts commit 56da326709.
2026-07-15 10:49:09 +03:00
Pete Matsyburka c8e783a210 multi select context menu readonly and required 2026-07-14 17:30:41 +03:00
Pete Matsyburka 61fc24d35d add with signature id completed config 2026-07-14 12:47:38 +03:00
Pete Matsyburka 73734b0de7 add variables_schema response 2026-07-13 19:34:09 +03:00
Pete Matsyburka 09bd831d54 adjust submission.name variable 2026-07-13 19:13:46 +03:00
Pete Matsyburka 2c9f1b999e update icon 2026-07-13 17:16:42 +03:00
Alex Turchyn 6a3a185310 Merge from docusealco/wip 2026-07-13 13:23:35 +03:00
Pete Matsyburka 7d381b7aee add reset smtp action 2026-07-13 10:58:08 +03:00
Alex Turchyn bba5626dea add ability to edit html email 2026-07-10 21:57:23 +03:00
Pete Matsyburka bdf5e58929 fix autocomplete index use 2026-07-10 10:32:04 +03:00
Pete Matsyburka 035168230e match generated font frontend 2026-07-09 15:44:34 +03:00
Pete Matsyburka 28168f082d adjust view only 2026-07-09 12:46:38 +03:00
Pete Matsyburka a29e09ed2f add submission update api 2026-07-08 11:16:56 +03:00
Pete Matsyburka e88874e757 view only party 2026-07-07 17:34:23 +03:00
Pete Matsyburka b6fb7c6977 fix find blob by checksum 2026-07-07 09:50:11 +03:00
Pete Matsyburka 8a60f17d13 optimize created_at filter 2026-07-07 09:41:37 +03:00
Pete Matsyburka e1d860a42c add submission created_at index 2026-07-07 09:14:15 +03:00
Pete Matsyburka 56da326709 adjust shared query 2026-07-06 18:45:37 +03:00
Pete Matsyburka 672008f8af add shared templates index param 2026-07-06 17:56:35 +03:00
Alex Turchyn d86b16de6b Merge from docusealco/wip 2026-07-06 13:25:54 +03:00
Pete Matsyburka d41710f507 fix spec 2026-07-04 10:12:08 +03:00
Pete Matsyburka 2b53166763 add completed check 2026-07-04 09:59:35 +03:00
Pete Matsyburka 4e56a16a58 adjust filters 2026-07-04 09:59:35 +03:00
Pete Matsyburka ac7cc77017 use submission completed_at 2026-07-04 09:59:35 +03:00
Pete Matsyburka 9194879c43 populate completed_at 2026-07-04 09:58:56 +03:00
Pete Matsyburka 1902cdaa55 submission completed_at 2026-07-04 09:58:56 +03:00
Alex Turchyn 6314987e69 make possible to unarchive completed submission 2026-07-04 09:57:29 +03:00
Pete Matsyburka 65bcbd7737 adjust authorize 2026-07-04 09:30:43 +03:00
Pete Matsyburka 9b25b8538e radio group font settings 2026-07-02 10:14:43 +03:00
Pete Matsyburka bbf8bb2a94 fix autocomplete index use 2026-07-01 13:06:00 +03:00
Pete Matsyburka 2085227cd9 fix dynamic document areas 2026-06-30 17:55:28 +03:00
Alex Turchyn 93e0730210 update docs 2026-06-30 13:31:31 +03:00
Pete Matsyburka 25c5c11ab3 change footer url 2026-06-30 08:39:26 +03:00
Pete Matsyburka 6ed5b5d35d extract email assets 2026-06-29 13:21:03 +03:00
Alex Turchyn 673cc1e0df Merge from docusealco/wip 2026-06-29 13:13:29 +03:00
Pete Matsyburka 065dcf6f9f fix include combined_document_url 2026-06-29 10:28:59 +03:00
Pete Matsyburka a66b16ce1c fix rubocop 2026-06-28 19:35:34 +03:00
Pete Matsyburka e4c0b30284 fix dedup email messages 2026-06-28 17:03:36 +03:00
Pete Matsyburka 3cb37748e9 optimize select 2026-06-28 11:21:04 +03:00
Pete Matsyburka e54d76f0e2 redact and crop on touchscreen 2026-06-27 19:28:20 +03:00
Pete Matsyburka f19e037011 fix input mode 2026-06-27 19:02:18 +03:00
Pete Matsyburka ee3f47d918 adjust edit condition 2026-06-27 17:21:55 +03:00
Pete Matsyburka a49a645022 fix completed_at index 2026-06-27 11:40:02 +03:00
Pete Matsyburka cf30441ac6 mcp use template submitters order 2026-06-27 09:08:23 +03:00
Pete Matsyburka c9bcf866ab optimize query 2026-06-26 21:33:43 +03:00
Pete Matsyburka 506609f3cd optimize search 2026-06-25 20:26:33 +03:00
Pete Matsyburka 721172c980 adjust accessibility 2026-06-25 15:53:15 +03:00
Pete Matsyburka b183db5b80 fix rubocop 2026-06-25 10:35:48 +03:00
Pete Matsyburka c7f277f404 fix search pagination 2026-06-25 10:28:02 +03:00
Pete Matsyburka 8ac03a9f8a fix template archived send event 2026-06-25 10:04:54 +03:00
Pete Matsyburka dc1c386d7e adjust search 2026-06-25 09:27:59 +03:00
Pete Matsyburka 1eaf4e5bab fix email stats 2026-06-25 08:48:34 +03:00
Pete Matsyburka 3c0f1c2e73 default order if countless 2026-06-25 08:07:06 +03:00
Pete Matsyburka b2790776ef show archived shared templates 2026-06-24 19:29:07 +03:00
Pete Matsyburka 2840359396 adjust shared tag 2026-06-24 17:20:20 +03:00
Pete Matsyburka c18ddb99a8 optimize queries 2026-06-24 17:20:20 +03:00
Pete Matsyburka 432435aa23 adjust shared templates 2026-06-24 17:20:19 +03:00
Pete Matsyburka 90c1a6f4ad fix can send emails 2026-06-24 14:42:49 +03:00
Pete Matsyburka 0e8f478c2e optimize query 2026-06-24 11:31:15 +03:00
Pete Matsyburka 5f23d5a5ab optimize query 2026-06-23 18:51:24 +03:00
Pete Matsyburka 76821a4c2d fix opt email footer 2026-06-23 17:40:40 +03:00
Pete Matsyburka e4a8cca90a update deps 2026-06-23 09:24:27 +03:00
Pete Matsyburka 6024ed4cd1 adjust accessibility 2026-06-23 09:09:31 +03:00
Pete Matsyburka 3811f8bbb8 fix submission nil name 2026-06-23 08:48:49 +03:00
Pete Matsyburka 306218e5df update gem 2026-06-22 13:11:04 +03:00
Alex Turchyn cabfe45608 Merge from docusealco/wip 2026-06-22 13:06:54 +03:00
Pete Matsyburka 4ade6a5911 update gem 2026-06-22 09:26:49 +03:00
Pete Matsyburka 10d45c3e20 add checks 2026-06-22 09:26:48 +03:00
Pete Matsyburka 6fb247ec06 adjust can resubmit 2026-06-21 07:58:23 +03:00
Pete Matsyburka 1546258529 add service url time 2026-06-19 19:20:21 +03:00
Pete Matsyburka 7ec83b8af6 adjust upload url 2026-06-19 08:40:39 +03:00
Pete Matsyburka 21d0a87c17 adjust replace button 2026-06-18 18:31:29 +03:00
Pete Matsyburka 528f59c1bf disable previewers 2026-06-17 10:46:47 +03:00
Pete Matsyburka d1535c4cd8 add redact color 2026-06-15 14:45:06 +03:00
Alex Turchyn da44d51a3b Merge from docusealco/wip 2026-06-15 13:37:25 +03:00
Pete Matsyburka 73ce10b26a add cleanup document helper 2026-06-15 12:05:50 +03:00
Pete Matsyburka 13874b8830 add documents editor 2026-06-15 10:21:23 +03:00
Pete Matsyburka ac66809a05 fix date tz 2026-06-12 14:03:20 +03:00
Pete Matsyburka f5229d5dec fix zoom 2026-06-12 12:47:23 +03:00
Pete Matsyburka 222291d11d update gem 2026-06-09 21:51:06 +03:00
Pete Matsyburka 28f47e1093 handle template archived 2026-06-09 19:49:12 +03:00
Pete Matsyburka 7784346a97 update gem 2026-06-09 12:11:16 +03:00
Alex Turchyn ed46af8418 Merge from docusealco/wip 2026-06-08 11:56:14 +03:00
Pete Matsyburka 095f25a6e9 adjust image type 2026-06-08 09:31:38 +03:00
Pete Matsyburka 3cdab30dc8 remove oj 2026-06-05 16:55:55 +03:00
Pete Matsyburka 8e462047ba fix upload callback 2026-06-05 14:45:22 +03:00
Pete Matsyburka b7f8b65d9d adjust button style 2026-06-05 09:55:21 +03:00
Alex Turchyn da21c37254 add Google Drive replace option 2026-06-05 09:55:21 +03:00
Pete Matsyburka a89aef4a89 adjust zoom 2026-06-04 14:39:02 +03:00
Alex Turchyn dd6516fd11 add cmd+scroll zooming to template builder 2026-06-04 08:20:38 +03:00
Pete Matsyburka 344a3ce198 adjust sanitize 2026-06-03 13:24:08 +03:00
Pete Matsyburka 62f8283587 adjust html to text 2026-06-02 13:44:52 +03:00
Alex Turchyn 1f89accac3 Merge from docusealco/wip 2026-06-01 14:58:06 +03:00
Pete Matsyburka 9fcaef4cf7 use new_from_memory_copy 2026-05-31 14:28:35 +03:00
Pete Matsyburka d5738a0631 disable variant_processor 2026-05-31 12:51:52 +03:00
Pete Matsyburka 8bf7a1f95a adjust image detect fields 2026-05-31 09:20:51 +03:00
Pete Matsyburka 04cf36891e fix complete button press enter 2026-05-31 09:09:57 +03:00
Pete Matsyburka b2d9948c30 remove active storage analyzers 2026-05-31 08:06:52 +03:00
Pete Matsyburka a2c9ac1707 remove image_processing 2026-05-30 21:37:00 +03:00
Pete Matsyburka 46a6bd0108 fix stamp 2026-05-30 12:04:23 +03:00
Pete Matsyburka 5f069e7a40 use load vips 2026-05-30 07:49:40 +03:00
Pete Matsyburka ff57e5c6ae fix pdfa 2026-05-29 15:31:32 +03:00
Alex Turchyn f65b6e2d76 add confirm prompt for template upload via URL 2026-05-29 09:29:21 +03:00
Pete Matsyburka 58fd180ae0 update gem 2026-05-29 08:04:53 +03:00
Pete Matsyburka cd6503c4c3 adjust image size 2026-05-28 15:37:20 +03:00
Pete Matsyburka e8b36c2b6d update gh stars 2026-05-28 14:38:34 +03:00
Pete Matsyburka 51743f1359 fix spec 2026-05-28 10:18:45 +03:00
Pete Matsyburka 504c42646b refactor template builder data 2026-05-28 10:06:47 +03:00
Pete Matsyburka c61e84d1b4 adjust archive 2026-05-28 08:40:05 +03:00
Pete Matsyburka b8ab01c46c add dynamic documents to template response 2026-05-28 08:29:10 +03:00
Pete Matsyburka 9558060bde check resubmit config 2026-05-28 08:29:09 +03:00
Pete Matsyburka 0741c879f1 use expires_at with file links 2026-05-27 22:19:18 +03:00
Pete Matsyburka 2e6dd2867e file url ttl env var 2026-05-26 21:54:59 +03:00
Pete Matsyburka 9f3fcccb61 optimize build 2026-05-26 16:50:10 +03:00
Pete Matsyburka ae08d7abad adjust opacity_layer 2026-05-26 13:11:15 +03:00
Pete Matsyburka 16808a7ec6 use pngload_buffer 2026-05-26 13:11:15 +03:00
Pete Matsyburka b1dfcd6283 fix email typo 2026-05-26 13:11:15 +03:00
Pete Matsyburka 369f4c0f08 sync prefillable field changes 2026-05-26 13:11:15 +03:00
Pete Matsyburka 827de03579 fix result generatrion 2026-05-26 08:57:30 +03:00
Alex Turchyn 0c7f4e5b43 fix sidekiq embedded redis connection 2026-05-25 21:52:34 +03:00
Alex Turchyn 9c700a3fb6 Merge from docusealco/wip 2026-05-25 12:28:05 +03:00
Pete Matsyburka 89bf83febb adjust autorot 2026-05-23 11:43:28 +03:00
Pete Matsyburka d44b35cf0a convert images on upload 2026-05-23 08:25:04 +03:00
Pete Matsyburka 891833e273 refactor process doc 2026-05-22 14:06:04 +03:00
Pete Matsyburka 29d3a80d3f fix rubocop 2026-05-21 21:55:30 +03:00
Pete Matsyburka 90fef58684 ruby 4.0.5 2026-05-21 21:46:27 +03:00
Pete Matsyburka 07841722ac adjust image load 2026-05-21 21:44:13 +03:00
Pete Matsyburka 9e4da5948b convert images on upload 2026-05-21 14:50:30 +03:00
Pete Matsyburka dc6e4313a1 fix ci 2026-05-20 16:15:15 +03:00
Pete Matsyburka a68cc0b689 convert images on upload 2026-05-20 15:23:42 +03:00
Pete Matsyburka d057fb0f67 content type priority 2026-05-19 13:27:16 +03:00
Pete Matsyburka 7e8f045a29 cleanup 2026-05-19 11:42:14 +03:00
Pete Matsyburka 5249d8d18d update gem 2026-05-19 11:02:15 +03:00
Pete Matsyburka f0fafbadd4 isolate editor nodes 2026-05-19 10:52:48 +03:00
Pete Matsyburka 8b51079dcc update gem 2026-05-18 18:42:04 +03:00
Pete Matsyburka 354bccd6e8 handle dangerous extensions 2026-05-18 18:05:16 +03:00
Pete Matsyburka 6806772346 add security headers 2026-05-18 16:47:19 +03:00
Alex Turchyn 60082655d4 Merge from docusealco/wip 2026-05-18 13:45:07 +03:00
Pete Matsyburka 7fe56941fd optimize pdf images 2026-05-17 19:55:03 +03:00
Pete Matsyburka abd498dd33 adjust validation message 2026-05-16 08:15:02 +03:00
Pete Matsyburka 99ca0136ed add percent format 2026-05-16 08:15:02 +03:00
Pete Matsyburka e52830c9b4 add authorization checks 2026-05-16 08:15:01 +03:00
Pete Matsyburka 755decca27 adjust capture revision 2026-05-15 17:50:18 +03:00
Pete Matsyburka 41604008d1 clamp area box 2026-05-15 17:20:29 +03:00
Pete Matsyburka 5bddce8969 fix i18n 2026-05-14 12:16:36 +03:00
Pete Matsyburka b25503f141 fix i18n 2026-05-13 10:08:13 +03:00
Pete Matsyburka bdf1850448 resend emails 2026-05-13 09:59:06 +03:00
Pete Matsyburka 230d3ccf69 fix with download config 2026-05-13 08:52:41 +03:00
Pete Matsyburka e378025a2d fix refactor complete button 2026-05-12 22:24:09 +03:00
Alex Turchyn 04129ded90 add complete button to signing form header 2026-05-12 19:19:49 +03:00
Pete Matsyburka a7891f89f8 do not override custom webhook header 2026-05-11 17:13:26 +03:00
Pete Matsyburka 37d4a8e834 update gem 2026-05-11 14:06:17 +03:00
Alex Turchyn 528a1216f8 Merge from docusealco/wip 2026-05-11 13:52:58 +03:00
Pete Matsyburka f2479bd259 adjust open revisions 2026-05-11 11:11:16 +03:00
Pete Matsyburka 135f0826d2 fix style 2026-05-09 21:17:18 +03:00
Pete Matsyburka 5710f0177b update gh stars 2026-05-09 19:25:42 +03:00
Pete Matsyburka 1a3a0528ba fix i18n 2026-05-09 10:34:20 +03:00
Pete Matsyburka 59793ff374 adjust builder 2026-05-08 19:35:46 +03:00
Pete Matsyburka 3c4ed42419 fix i18n 2026-05-08 18:08:35 +03:00
Pete Matsyburka 6a17b61550 update gem 2026-05-08 16:25:11 +03:00
Pete Matsyburka 76659497b7 typos 2026-05-08 16:12:09 +03:00
Pete Matsyburka ee50c957cb update gem 2026-05-08 15:43:34 +03:00
Pete Matsyburka a9dd200919 add pdfium rotate 2026-05-08 15:43:34 +03:00
Pete Matsyburka 1fa953104a update gh stars 2026-05-08 15:43:34 +03:00
Pete Matsyburka 50eb5b070e fix typo 2026-05-08 15:43:34 +03:00
Pete Matsyburka 10fd624bec add revisions 2026-05-08 15:43:34 +03:00
Alex Turchyn 04ec2f8260 allow permanent delete submissions in archived templates 2026-05-08 15:43:34 +03:00
Pete Matsyburka d828d79574 improve log 2026-05-08 15:43:34 +03:00
Pete Matsyburka 01dd3fefe5 retry webhooks in test mode 2026-05-08 15:43:34 +03:00
Pete Matsyburka 45ae954c0c add hmac webhook secret 2026-05-08 15:43:34 +03:00
Pete Matsyburka 1304849b55 update gh stars 2026-05-08 15:43:34 +03:00
Pete Matsyburka 0875faa079 fix date preview 2026-05-08 15:43:34 +03:00
Pete Matsyburka 3aa15d6ea6 update gem 2026-05-08 15:43:34 +03:00
Pete Matsyburka 6557329e97 gh 13k 2026-05-08 15:43:34 +03:00
Pete Matsyburka be27ce4161 Merge pull request #666 from aqilaziz/docs-fix-readme-typos
docs: fix README typos
2026-05-08 14:02:33 +03:00
aqilaziz a3391a970e docs: fix README typos 2026-05-08 05:23:28 +07:00
Alex Turchyn 744d45d2c5 Merge from docusealco/wip 2026-05-04 13:55:19 +03:00
Pete Matsyburka 310b16f54a use mcp source 2026-05-04 09:42:51 +03:00
Pete Matsyburka 6d3ad4a869 update gh stars 2026-05-03 18:37:56 +03:00
Alex Turchyn 1ce2448820 update create_template and send_documents annotations 2026-05-03 17:43:22 +03:00
Pete Matsyburka 07b4ada5be optimize edit json 2026-05-03 16:32:37 +03:00
Pete Matsyburka f70bf530ba hide qr link 2026-05-03 08:19:58 +03:00
Pete Matsyburka a26aa0f411 remove log 2026-05-03 07:27:24 +03:00
Pete Matsyburka 584577c21f fix ed.jp email typo 2026-05-01 13:50:17 +03:00
Pete Matsyburka 1503375eed respond to HEAD file proxy 2026-04-30 17:55:13 +03:00
Pete Matsyburka 2dec099f20 fix signature canvas minimize 2026-04-30 14:51:24 +03:00
Pete Matsyburka bf6eed4ea2 fix canvas error 2026-04-30 14:42:22 +03:00
Pete Matsyburka e0dc33cfb4 fix email domain search 2026-04-30 11:27:17 +03:00
Pete Matsyburka 9c9dc27537 fix new area order 2026-04-30 11:13:35 +03:00
Pete Matsyburka 7305637b8c sort appears on 2026-04-30 10:32:51 +03:00
Pete Matsyburka a6e22adf2e fix preview 2026-04-29 18:00:47 +03:00
Pete Matsyburka 809c6b1aa8 fix modal open 2026-04-29 10:27:02 +03:00
Pete Matsyburka 1536ded2a6 redis error log 2026-04-28 19:30:51 +03:00
Pete Matsyburka 76d54971ce fix test mode clone 2026-04-28 11:58:17 +03:00
Pete Matsyburka 6dec56198c add require email 2fa to submitter update 2026-04-28 11:58:17 +03:00
Pete Matsyburka d96d252df3 use post for test mode 2026-04-28 11:58:17 +03:00
Pete Matsyburka 3fca76a716 change state param 2026-04-28 11:58:17 +03:00
Pete Matsyburka a184fa11f1 move auth links 2026-04-28 11:58:17 +03:00
Pete Matsyburka e9c0b6a703 rate limit 2026-04-28 11:58:17 +03:00
Pete Matsyburka ec7df08496 adjust send submission email 2026-04-28 11:58:17 +03:00
Pete Matsyburka 6627d7eac5 fix erblint 2026-04-27 15:34:57 +03:00
Pete Matsyburka 1f8945d710 fix expired filter 2026-04-27 15:31:29 +03:00
Alex Turchyn daaa289a5c Merge from docusealco/wip 2026-04-27 13:43:34 +03:00
Pete Matsyburka 81a6a48d70 validate color 2026-04-26 15:13:19 +03:00
Pete Matsyburka b93c7cd261 add has_many document_metadata 2026-04-26 10:24:20 +03:00
Pete Matsyburka 570482e12d rename error 2026-04-25 12:39:26 +03:00
Pete Matsyburka 39407557f2 adjust page preview 2026-04-25 09:01:53 +03:00
Pete Matsyburka 3b040d558d adjust compression 2026-04-24 18:39:19 +03:00
Alex Turchyn 56ba3c2a52 add custom classes 2026-04-24 17:01:33 +03:00
Pete Matsyburka c7afe33c72 add time formats 2026-04-24 14:01:28 +03:00
Pete Matsyburka 82500c2d7d update dep 2026-04-24 10:56:19 +03:00
Pete Matsyburka fb1c1f117b remove unused const 2026-04-24 10:56:19 +03:00
Pete Matsyburka 7c6a7513fd add with custom field tab 2026-04-24 09:59:34 +03:00
Pete Matsyburka 6d13119ee0 input mode condition and formula 2026-04-22 10:58:50 +03:00
Pete Matsyburka 1e2c752937 detect existing fields 2026-04-22 10:27:18 +03:00
Pete Matsyburka e82faf8320 queue page field detection 2026-04-22 07:43:45 +03:00
Pete Matsyburka 4adb1001b0 add rotate incremental config 2026-04-21 12:35:00 +03:00
Pete Matsyburka 992a1b26c0 adjust tooltip position 2026-04-20 20:18:07 +03:00
Pete Matsyburka c07bc6687c add metadata param 2026-04-20 15:43:41 +03:00
Alex Turchyn eea44bda34 Merge from docusealco/wip 2026-04-20 13:57:33 +03:00
Pete Matsyburka 7cdf263da1 add screen reader mode 2026-04-19 20:18:52 +03:00
Pete Matsyburka ee65a5693c improve accessibility 2026-04-19 10:08:16 +03:00
Pete Matsyburka 0af6ccf35f pipeline field detection 2026-04-19 09:01:08 +03:00
Pete Matsyburka 888f1ec6df adjust mcp 2026-04-18 08:33:42 +03:00
Pete Matsyburka c95a8616ac deduplicate submitter uuids 2026-04-17 22:01:03 +03:00
Pete Matsyburka 5ea6289b7a fix test mode modal 2026-04-16 09:00:43 +03:00
Pete Matsyburka d4a79ca5db adjust dynamic editor 2026-04-15 10:06:41 +03:00
Pete Matsyburka 70015ce1c4 adjust dynamic editor 2026-04-14 14:14:25 +03:00
Pete Matsyburka 6b85c28944 add error message 2026-04-14 10:51:12 +03:00
Pete Matsyburka 6c289cf273 optimize build 2026-04-14 09:37:27 +03:00
Pete Matsyburka a64bc3c618 change button style 2026-04-14 09:21:39 +03:00
Pete Matsyburka 46cf1e3067 add log 2026-04-14 09:13:33 +03:00
Pete Matsyburka 565e1eb2bc adjust validation 2026-04-13 19:55:07 +03:00
Pete Matsyburka e689687805 fix erblint 2026-04-13 19:21:24 +03:00
Pete Matsyburka 3c3b61fb47 adjust reason field 2026-04-13 19:18:55 +03:00
Pete Matsyburka 1355d350c5 fix xlsx boolean value 2026-04-13 18:13:38 +03:00
Pete Matsyburka fda911e178 add submitter field validation 2026-04-13 16:32:03 +03:00
Pete Matsyburka f995e1864c add shared link qr code 2026-04-13 13:59:00 +03:00
Alex Turchyn 911e55ccc3 Merge from docusealco/wip 2026-04-13 13:25:16 +03:00
Pete Matsyburka 41fedfcc40 show verification error 2026-04-12 13:30:13 +03:00
Pete Matsyburka 2f9fc95af3 remove unused module 2026-04-11 18:13:25 +03:00
Pete Matsyburka 11b0b16ca7 prefill signature client side 2026-04-11 18:13:25 +03:00
Pete Matsyburka f8cb7ffdab remove mathjs 2026-04-11 18:13:23 +03:00
Pete Matsyburka 8b59c0aaa0 adjust normalize values 2026-04-10 15:20:35 +03:00
Pete Matsyburka bb2fb7a0c2 refactor download 2026-04-10 12:54:48 +03:00
Pete Matsyburka 8f8b36617a fix first party download from preview 2026-04-10 11:15:16 +03:00
Pete Matsyburka ff53436fd4 fix dynamic editor layout 2026-04-08 17:18:38 +03:00
Pete Matsyburka cf09a4f733 update gem 2026-04-08 12:24:31 +03:00
Pete Matsyburka 2303c21cea download users csv 2026-04-08 12:24:22 +03:00
Pete Matsyburka 89e797b95f update message 2026-04-08 10:29:45 +03:00
Pete Matsyburka 20375c3a42 update gem 2026-04-08 08:40:41 +03:00
Pete Matsyburka 339ceda18d upload attachment if not completed 2026-04-07 18:20:16 +03:00
Pete Matsyburka 97ce32fd52 set attachment name 2026-04-07 16:34:15 +03:00
Alex Turchyn 8c4cbd86f7 Merge from docusealco/wip 2026-04-06 15:22:38 +03:00
Pete Matsyburka eaedaa96bb fix rubocop 2026-04-02 13:28:12 +03:00
Pete Matsyburka bd7ad5fce8 update gems 2026-04-02 13:28:12 +03:00
Pete Matsyburka 759dac1b70 update schema 2026-04-01 19:07:57 +03:00
Pete Matsyburka 38577c6235 fix dynamic doc fields assignment 2026-04-01 16:08:49 +03:00
Pete Matsyburka b24fff0f55 fix rtl stamp 2026-04-01 11:10:09 +03:00
Pete Matsyburka 9e0efeb092 fix unicode in stamp 2026-04-01 10:29:31 +03:00
Pete Matsyburka 41152f90fc fix erblint 2026-03-30 15:18:34 +03:00
Alex Turchyn 4486f8040e Merge from docusealco/wip 2026-03-30 14:35:18 +03:00
Pete Matsyburka a98caad968 use musl build 2026-03-29 11:18:41 +03:00
Pete Matsyburka ffb75238b4 disable confetti by default 2026-03-28 16:53:20 +02:00
Pete Matsyburka cba3e74f3a optimize build 2026-03-28 16:44:08 +02:00
Pete Matsyburka 8d3f0b5b98 silence not found trace 2026-03-28 14:50:38 +02:00
Pete Matsyburka a2598b30f7 optimize build 2026-03-28 13:48:39 +02:00
Pete Matsyburka 8d11f5ddae adjust email 2fa 2026-03-28 12:51:00 +02:00
Pete Matsyburka 6ffc88b4ed fix n+1 2026-03-28 09:27:50 +02:00
Pete Matsyburka 640b451727 adjust shared link 2026-03-28 09:07:52 +02:00
Pete Matsyburka db6e2eb7ca preserve order with verification 2026-03-27 17:16:06 +02:00
Pete Matsyburka 407816b576 redirect delegate completed 2026-03-27 14:51:14 +02:00
Pete Matsyburka 486cc493e6 add i18n 2026-03-27 14:21:47 +02:00
Pete Matsyburka 3eb0b8a89f allow to delegate 2026-03-27 14:21:47 +02:00
Pete Matsyburka c23eca0ade fix field style 2026-03-26 16:09:57 +02:00
Pete Matsyburka 67de153016 increase image limit 2026-03-26 11:40:42 +02:00
Pete Matsyburka ba2da69a67 fix load ico 2026-03-26 08:29:17 +02:00
Pete Matsyburka 111bef14e1 update rails 2026-03-24 23:56:40 +02:00
Alex Turchyn 22d4c1bd10 Merge from docusealco/wip 2026-03-23 14:59:53 +02:00
Pete Matsyburka e3dfb0ae83 add SMTP_ENABLE_TLS 2026-03-23 10:02:54 +02:00
Pete Matsyburka 8d7c3f0e26 fix generation 2026-03-23 09:48:39 +02:00
Pete Matsyburka b11775ebda remove premailer 2026-03-22 18:13:41 +02:00
Pete Matsyburka 608191183a update deps 2026-03-21 13:37:31 +02:00
Pete Matsyburka 2ba903e170 fix specs 2026-03-18 10:18:28 +02:00
Alex Turchyn 342b64d502 add translations 2026-03-18 09:52:58 +02:00
Pete Matsyburka 1a8f048aaa update devise 2026-03-18 09:52:21 +02:00
Pete Matsyburka 7309581b75 add template archived event 2026-03-18 09:52:21 +02:00
Pete Matsyburka 36d59c40a7 add template update event 2026-03-18 08:36:36 +02:00
Pete Matsyburka ed3c7c324b adjust save dynamic 2026-03-16 17:24:07 +02:00
Alex Turchyn fd868360a3 Merge from docusealco/wip 2026-03-16 17:07:07 +02:00
Pete Matsyburka 8dc2ebfbe0 fix builder save 2026-03-16 16:59:45 +02:00
Alex Turchyn 997711b34a Merge from docusealco/wip 2026-03-16 13:36:06 +02:00
Pete Matsyburka ac01b7129d add i18n 2026-03-16 12:42:58 +02:00
Pete Matsyburka f4b7528c0d fix send form 2026-03-16 08:52:42 +02:00
Pete Matsyburka d7b9ae5df5 add datenow size 2026-03-16 08:21:36 +02:00
Pete Matsyburka 4db29126e9 fix variables list 2026-03-16 08:04:02 +02:00
Pete Matsyburka 4cfee2fe45 fix schema documents 2026-03-15 15:02:46 +02:00
Pete Matsyburka db5dbd6ac2 adjust dynamic editor 2026-03-15 14:05:20 +02:00
Pete Matsyburka 44db775b89 fix variable style 2026-03-14 17:47:53 +02:00
Alex Turchyn 8e2e780a1a fix webhook events filtering 2026-03-13 20:04:09 +02:00
Pete Matsyburka 7aba2eb4da disable decline 2026-03-12 08:58:56 +02:00
Pete Matsyburka 3e250bfb28 error update if declined 2026-03-11 23:44:06 +02:00
Pete Matsyburka 9c02ed66f5 fix field scroll 2026-03-11 14:43:16 +02:00
Pete Matsyburka f5ae8c60d3 preload dynamic editor 2026-03-11 10:44:12 +02:00
Pete Matsyburka e90ac391b2 update readme 2026-03-10 19:09:00 +02:00
Pete Matsyburka 0e5f949bcf remove email from signing reason 2026-03-10 11:39:00 +02:00
Pete Matsyburka b7c270bef8 dir auto section 2026-03-10 10:31:45 +02:00
Pete Matsyburka 5f16b506f5 fix n+1 2026-03-10 08:12:32 +02:00
Pete Matsyburka 176148fc71 adjust default initial 2026-03-09 17:29:46 +02:00
Alex Turchyn 861daaedf4 Merge from docusealco/wip 2026-03-09 10:40:29 +02:00
Pete Matsyburka d8430f4e63 fix migration 2026-03-09 09:02:56 +02:00
Pete Matsyburka dd44864485 fix field position 2026-03-06 11:37:53 +02:00
Pete Matsyburka b3bb29f383 fix brakeman 2026-03-05 21:42:55 +02:00
Pete Matsyburka b4d137c0de remove flash 2026-03-05 12:30:26 +02:00
Alex Turchyn 62a969d8fe add MCP support 2026-03-05 12:17:14 +02:00
Pete Matsyburka 961f09e092 set dynamic fields 2026-03-05 10:08:29 +02:00
Pete Matsyburka b2dfa83f18 upload json 2026-03-04 12:27:15 +02:00
Pete Matsyburka 37196ff89f add dynamic documents 2026-03-04 10:13:30 +02:00
Pete Matsyburka b50d982497 smtp timeout env vars 2026-03-03 22:49:55 +02:00
Pete Matsyburka 4a484aca69 hide storage nav if configured with env 2026-03-03 22:01:44 +02:00
Pete Matsyburka 0bf37d571a update color 2026-03-03 18:46:06 +02:00
Alex Turchyn d4e94ac2bc Merge from docusealco/wip 2026-03-02 17:26:04 +02:00
Pete Matsyburka 75bc789bb1 add reply to 2026-03-02 14:47:37 +02:00
Pete Matsyburka bc231d5bf1 add reply to 2026-03-02 14:34:16 +02:00
Alex Turchyn ab9008b3be fix brakeman SQL injection warning 2026-03-01 16:14:15 +02:00
Pete Matsyburka 65491936c3 fix archived export 2026-02-26 17:20:05 +02:00
Pete Matsyburka e589998840 add field type class 2026-02-26 11:08:26 +02:00
Pete Matsyburka 0afba8d11e adjust with prefillable 2026-02-26 11:03:08 +02:00
Pete Matsyburka f622ed168f fix autocomplete 2026-02-25 13:01:03 +02:00
Pete Matsyburka 5a31df40d8 add field settings classes 2026-02-25 11:02:00 +02:00
Pete Matsyburka db3b80c96c fix clone 2026-02-25 11:02:00 +02:00
Pete Matsyburka bd28a761d0 adjust bmp loader 2026-02-25 11:02:00 +02:00
Pete Matsyburka 4f2830a34d increase image resolution 2026-02-25 11:02:00 +02:00
Pete Matsyburka a5716cd518 show html textarea 2026-02-25 11:02:00 +02:00
Pete Matsyburka d035525c7a additional terms 2026-02-25 11:02:00 +02:00
Alex Turchyn 13fa87c449 Merge from docusealco/wip 2026-02-23 13:56:04 +02:00
Pete Matsyburka 246ccfb049 fix highlight 2026-02-23 08:31:38 +02:00
Pete Matsyburka cbffb1e5b8 cloudflare r2 fix 2026-02-22 20:13:19 +02:00
Pete Matsyburka c4b007411d radio select in formula 2026-02-22 19:54:06 +02:00
Pete Matsyburka 04264fe937 fix reason field 2026-02-22 12:15:44 +02:00
Pete Matsyburka 0c37ad12b7 add variables key 2026-02-22 09:46:04 +02:00
Pete Matsyburka 8c5298ed2e refactor link tooltip 2026-02-22 09:28:30 +02:00
Pete Matsyburka 262118e047 update deps 2026-02-22 07:37:05 +02:00
Pete Matsyburka e5b63ea2b0 fix multi party email form 2026-02-21 17:31:04 +02:00
Pete Matsyburka 746757d402 rtl in editor 2026-02-21 16:38:25 +02:00
Pete Matsyburka e6e640328b use br for email content 2026-02-21 16:20:48 +02:00
Pete Matsyburka 845782a69c link input width 2026-02-21 15:48:59 +02:00
Pete Matsyburka fc6baa1b3b adjust autolink 2026-02-21 15:48:58 +02:00
Pete Matsyburka 47822ecc15 remove autolink 2026-02-21 14:55:11 +02:00
Pete Matsyburka 3d0c7f1118 markdown in preferences 2026-02-21 14:44:19 +02:00
Pete Matsyburka e9c8e4d325 adjust editor 2026-02-21 14:32:10 +02:00
Alex Turchyn 39cc82ce0c add markdown editor 2026-02-21 11:15:09 +02:00
Pete Matsyburka 451f83421a fix cell render 2026-02-21 10:49:05 +02:00
Pete Matsyburka 5073f2eefb adjsut sign yourself 2026-02-21 10:17:58 +02:00
Pete Matsyburka 231fff5508 canvas blocked message 2026-02-21 09:53:03 +02:00
Pete Matsyburka f1d146eca3 fix typos 2026-02-20 22:39:03 +02:00
Pete Matsyburka b6635fcc4f change user only when root 2026-02-20 22:25:12 +02:00
Pete Matsyburka fb5e13ee4c adjust recipients form 2026-02-20 18:26:10 +02:00
Pete Matsyburka ba84741a64 invite party via field 2026-02-20 12:41:45 +02:00
Pete Matsyburka 65c275ac17 rename 2026-02-20 10:16:06 +02:00
Pete Matsyburka 825322d489 adjust for custom domain 2026-02-20 09:14:36 +02:00
Pete Matsyburka 739e2abdf8 vips size limit 2026-02-19 14:24:05 +02:00
Pete Matsyburka 1d2394e31e size limit 2026-02-19 14:17:59 +02:00
Pete Matsyburka 1b41af798d update 2026-02-17 19:42:47 +02:00
Pete Matsyburka 118f4a231b detailed time format 2026-02-17 18:09:34 +02:00
Pete Matsyburka e48652f425 fix rubocop 2026-02-17 10:29:00 +02:00
Pete Matsyburka 848f01edf8 add spec 2026-02-17 10:19:29 +02:00
Pete Matsyburka 43fbc42770 fix reason field 2026-02-17 10:04:11 +02:00
Pete Matsyburka 2c736a0eed validate readonly field 2026-02-17 08:53:13 +02:00
Alex Turchyn 33ca930055 Merge from docusealco/wip
Wip
2026-02-16 15:08:07 +02:00
Pete Matsyburka 3b396f8421 fix redirect 2026-02-16 10:00:51 +02:00
Pete Matsyburka aa77e3a8d3 adjust port check 2026-02-16 08:08:41 +02:00
Pete Matsyburka 75316d8d87 fix draw custom field 2026-02-15 22:02:00 +02:00
Pete Matsyburka a999109a5c add port check 2026-02-15 12:18:53 +02:00
Pete Matsyburka bdd33c7d6b fix spec 2026-02-15 09:53:31 +02:00
Pete Matsyburka 61c5ee22a0 hide form credentials 2026-02-15 09:11:40 +02:00
Pete Matsyburka 12c5b909e0 use cancan 2026-02-14 19:13:05 +02:00
Pete Matsyburka 871ef6dda6 add fetch options 2026-02-14 18:50:48 +02:00
Pete Matsyburka 40052a2d7c use query_params 2026-02-14 17:42:08 +02:00
Pete Matsyburka 347be0137d refactor 2fa 2026-02-14 17:35:10 +02:00
Pete Matsyburka fe6baba8bf fix erb lint 2026-02-14 12:10:21 +02:00
Pete Matsyburka ed8c313bd4 timestamp controller multitenant 2026-02-14 12:02:37 +02:00
Pete Matsyburka ca0acb34d6 use url for open modal 2026-02-14 12:02:31 +02:00
Pete Matsyburka 34ea639c25 escape wildcard query 2026-02-14 12:02:25 +02:00
Pete Matsyburka 680ab9dbed raise invalid param 2026-02-14 10:18:58 +02:00
Pete Matsyburka 9377766e52 fix url download 2026-02-14 09:08:52 +02:00
Pete Matsyburka 7a2c37454e update favicon 2026-02-12 15:56:54 +02:00
Pete Matsyburka 8f6f418f54 fix font size 2026-02-12 14:27:15 +02:00
Pete Matsyburka 9b7745c565 hide resend on archived 2026-02-11 17:32:04 +02:00
Alex Turchyn a389e8cc38 Merge from docusealco/wip 2026-02-11 16:49:32 +02:00
Pete Matsyburka 2056301f70 remember send tab selection 2026-02-11 13:14:09 +02:00
Pete Matsyburka 93babe3660 update logo 2026-02-11 10:01:12 +02:00
Pete Matsyburka 4aef2d8eb0 update deps 2026-02-10 09:02:37 +02:00
Pete Matsyburka c5ec2ed230 fix document condition 2026-02-05 11:56:22 +02:00
Pete Matsyburka a156d5f6cc increase webhook timeout 2026-02-05 08:42:53 +02:00
Pete Matsyburka f84674cddd adjust field focus 2026-02-04 17:05:12 +02:00
Pete Matsyburka b7b62c197a fix date field 2026-02-04 16:56:35 +02:00
Pete Matsyburka f5350eac36 prefill typed signature and initials from name 2026-02-04 16:48:40 +02:00
Pete Matsyburka ee6228db15 only required field prop 2026-02-04 10:51:11 +02:00
Pete Matsyburka 3e434c0878 fix 2026-02-04 00:47:34 +02:00
Pete Matsyburka 7c5372a8e2 fix pdf sign 2026-02-02 22:04:43 +02:00
Alex Turchyn 6ab68cc36b Merge from docusealco/wip 2026-02-02 14:13:54 +02:00
Pete Matsyburka ee2400a5c5 touch signature attachment 2026-02-02 12:46:34 +02:00
Pete Matsyburka 80be5d2cf5 sandbox 2026-02-01 08:10:02 +02:00
Pete Matsyburka 76cf44a31b fix 2026-01-31 22:17:11 +02:00
Pete Matsyburka eb63113227 use trilogy mysql 2026-01-31 22:15:17 +02:00
Pete Matsyburka b7045fb3d9 fix azure proxy 2026-01-31 13:07:42 +02:00
Pete Matsyburka 11a5b5842d starttls label 2026-01-31 11:41:52 +02:00
Pete Matsyburka 362994afd5 adjust i18n 2026-01-31 10:26:09 +02:00
Pete Matsyburka d7682f4ebd add number conditions 2026-01-31 09:00:24 +02:00
Pete Matsyburka 56d4fe7fa9 add white color 2026-01-31 07:58:12 +02:00
Pete Matsyburka 93a852ac14 fix save modal 2026-01-30 21:45:55 +02:00
Pete Matsyburka 964db0ab08 fix menu 2026-01-30 21:03:56 +02:00
Pete Matsyburka 8547295da7 border color 2026-01-30 20:53:47 +02:00
Pete Matsyburka c75050fa46 adjust kba 2026-01-30 18:17:47 +02:00
Pete Matsyburka 65372c52aa fix order 2026-01-30 17:42:50 +02:00
Alex Turchyn 12c767b7d3 add more options to context menu 2026-01-30 16:36:50 +02:00
Pete Matsyburka 2fc5e9cfb3 format cells 2026-01-30 11:21:47 +02:00
Pete Matsyburka bccc78ee65 optimize builder 2026-01-30 10:23:51 +02:00
Pete Matsyburka c0793f3ce7 cell font settings 2026-01-29 21:43:01 +02:00
Pete Matsyburka a8dd8699a3 adjust kba 2026-01-29 15:55:15 +02:00
Pete Matsyburka 91bb25e8b6 fix reload 2026-01-29 15:33:24 +02:00
Pete Matsyburka c3f4480cdc add submitter index 2026-01-29 13:01:44 +02:00
Pete Matsyburka 33e226861e add SMTP_SSL_VERIFY env 2026-01-29 11:34:37 +02:00
Pete Matsyburka 4f5acbec9d readonly radio 2026-01-28 21:36:48 +02:00
Pete Matsyburka d2c9bbf6ab optimize conditions 2026-01-28 13:24:05 +02:00
Pete Matsyburka b5e57e1f2f add include fields 2026-01-28 11:49:35 +02:00
Pete Matsyburka c82c8b2272 fix race condition 2026-01-28 08:50:44 +02:00
Pete Matsyburka 43f1b598a7 adjust smtp noverify 2026-01-28 08:27:25 +02:00
Pete Matsyburka 5081e387d0 fix undefined 2026-01-27 20:13:26 +02:00
Pete Matsyburka 1e5f55c1d6 fix Point is invalid 2026-01-27 20:07:57 +02:00
Alex Turchyn aa38773be3 Merge from docusealco/wip 2026-01-27 18:39:04 +02:00
Pete Matsyburka 96530bd809 fix race condition 2026-01-27 17:06:30 +02:00
Pete Matsyburka 127f95a9d0 fix typo 2026-01-27 12:31:06 +02:00
Pete Matsyburka cf31e12348 adjust smtp configs 2026-01-27 10:57:04 +02:00
Pete Matsyburka 4adaa531b7 fix pagination 2026-01-27 10:34:18 +02:00
Pete Matsyburka c7659563b8 fix puma warnings 2026-01-26 20:23:11 +02:00
Alex Turchyn 7b5494468f update rails to 8.1.2 2026-01-26 18:22:47 +02:00
Pete Matsyburka d12c67fd54 with force open 2026-01-26 18:00:46 +02:00
Pete Matsyburka e20eacb5ee center on click 2026-01-26 12:35:53 +02:00
Pete Matsyburka 98055f0762 add can parse polyfill 2026-01-26 12:17:57 +02:00
Pete Matsyburka a46c42c3a0 fix resize warning 2026-01-26 12:06:41 +02:00
Pete Matsyburka c3d1503361 cleanup 2026-01-26 11:44:12 +02:00
Pete Matsyburka 65b47d27cd adjust form 2026-01-26 11:38:33 +02:00
Pete Matsyburka 968c07f8be include idcode 2026-01-26 11:13:01 +02:00
Pete Matsyburka 91814ca105 custom fields 2026-01-26 11:12:55 +02:00
Pete Matsyburka 9128f24270 fix synology acl 2026-01-24 18:01:49 +02:00
Pete Matsyburka 264897bbd5 audit with sender 2026-01-23 12:12:37 +02:00
Pete Matsyburka 67d59ff1ba fix invite by order 2026-01-22 21:20:34 +02:00
Pete Matsyburka 81eee56e1e i18n 2026-01-21 20:14:53 +02:00
Pete Matsyburka 92ea8af786 fix options drag 2026-01-21 16:02:20 +02:00
Pete Matsyburka df0616f77e paste options 2026-01-21 15:42:38 +02:00
Pete Matsyburka a337dff85c change model 2026-01-21 10:23:26 +02:00
Pete Matsyburka 48e349c1d2 fix signature pad when resize 2026-01-20 17:24:05 +02:00
Pete Matsyburka df9fa0fec7 fix scroll into field 2026-01-20 15:27:35 +02:00
Alex Turchyn b3e72f0726 Merge from docusealco/wip 2026-01-19 15:40:56 +02:00
Pete Matsyburka 700ebc4727 render reason 2026-01-19 13:56:17 +02:00
Pete Matsyburka 745a18efca fix select mode draw 2026-01-19 11:57:09 +02:00
Pete Matsyburka b4a8147ff0 fix radio group paste 2026-01-19 11:51:00 +02:00
Pete Matsyburka 0543c25054 detect fields in context menu 2026-01-19 09:23:04 +02:00
Pete Matsyburka b1a0afea66 fix menu 2026-01-18 10:29:15 +02:00
Pete Matsyburka 43c41e2557 add select fields mode 2026-01-18 09:52:43 +02:00
Pete Matsyburka bc8eb33f05 Revert "hide area name menu on desktop"
This reverts commit 5212773d0a.
2026-01-17 19:42:27 +02:00
Pete Matsyburka d1cebc92f7 fix eslint 2026-01-16 12:38:29 +02:00
Pete Matsyburka bf02cf4058 fix paste 2026-01-16 12:25:50 +02:00
Pete Matsyburka 5212773d0a hide area name menu on desktop 2026-01-16 12:18:17 +02:00
Alex Turchyn bf53204a41 add context menu to template builder 2026-01-16 12:07:59 +02:00
Pete Matsyburka 092a19966c adjust condition 2026-01-15 17:34:05 +02:00
Pete Matsyburka 6060a2b798 fix icon 2026-01-14 21:58:30 +02:00
Pete Matsyburka ce12af68b2 fix required 2026-01-14 14:32:00 +02:00
Pete Matsyburka c05f8d47aa require 2fa 2026-01-14 12:44:27 +02:00
Pete Matsyburka f8be19c20e pg 18 2026-01-13 20:44:52 +02:00
Pete Matsyburka 2fe32a2145 show version 2026-01-13 12:25:08 +02:00
Pete Matsyburka 794ea4d0e8 remove expired from pending filter 2026-01-13 12:07:39 +02:00
Pete Matsyburka 307b0f5afd fix build 2026-01-13 09:44:58 +02:00
Alex Turchyn 9011e3f4b4 Merge from docusealco/wip 2026-01-12 13:25:54 +02:00
Pete Matsyburka 0b45ce5c0c update model 2026-01-12 12:05:54 +02:00
Pete Matsyburka a8ab93a4cc adjust sort 2026-01-12 11:27:19 +02:00
Pete Matsyburka d1768be636 secure env 2026-01-12 11:27:17 +02:00
Pete Matsyburka b2cd7c82a7 insert new field at page position 2026-01-11 14:45:42 +02:00
Pete Matsyburka b88d99afe0 merge preview docs 2026-01-11 11:09:00 +02:00
Pete Matsyburka 4523d38551 direct links option 2026-01-09 22:05:37 +02:00
Pete Matsyburka c4ba892559 refactor clone 2026-01-08 09:34:45 +02:00
Pete Matsyburka 40f8093e00 adjust onnx 2026-01-07 14:50:27 +02:00
Pete Matsyburka 4375459856 add nmm 2026-01-07 12:19:05 +02:00
Pete Matsyburka b3eb8a70bf fix submitter uuid 2026-01-07 00:01:47 +02:00
Pete Matsyburka 6245a487b6 add kba countdown 2026-01-06 11:24:30 +02:00
Pete Matsyburka a89f4c1b10 adjust trim 2026-01-06 10:22:34 +02:00
Pete Matsyburka 7df729b8b5 containment threshold 2026-01-06 09:04:25 +02:00
Pete Matsyburka f1db7c7e82 adjust trim 2026-01-06 08:54:29 +02:00
683 changed files with 49086 additions and 10486 deletions

No files matched your search

+5
View File
@@ -1,6 +1,11 @@
---
EnableDefaultLinters: true
linters:
AllowedScriptType:
enabled: true
allowed_types:
- text/javascript
- application/json
ErbSafety:
enabled: false
HardCodedString:
+20 -30
View File
@@ -12,7 +12,7 @@ jobs:
- name: Install Ruby
uses: ruby/setup-ruby@v1
with:
ruby-version: 3.4.2
ruby-version: 4.0.5
- name: Cache gems
uses: actions/cache@v4
with:
@@ -37,7 +37,7 @@ jobs:
- name: Install Ruby
uses: ruby/setup-ruby@v1
with:
ruby-version: 3.4.2
ruby-version: 4.0.5
- name: Cache gems
uses: actions/cache@v4
with:
@@ -62,7 +62,7 @@ jobs:
- name: Install Node.js
uses: actions/setup-node@v1
with:
node-version: 20.9.0
node-version: 20.19.0
- name: Cache directory path
id: yarn-cache-dir-path
run: echo "::set-output name=dir::$(yarn cache dir)"
@@ -89,7 +89,7 @@ jobs:
- name: Install Ruby
uses: ruby/setup-ruby@v1
with:
ruby-version: 3.4.2
ruby-version: 4.0.5
- name: Cache gems
uses: actions/cache@v4
with:
@@ -104,55 +104,46 @@ jobs:
bundle install --jobs 4 --retry 4
yarn install
sudo apt-get update
sudo apt-get install libvips
sudo apt-get install libvips liblept5
- name: Run Brakeman
run: bundle exec brakeman -q --exit-on-warn
rspec:
name: RSpec
runs-on: ubuntu-latest
timeout-minutes: 10
timeout-minutes: 15
container:
image: ruby:4.0.5-alpine
services:
postgres:
image: postgres:14
image: postgres:18
env:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
POSTGRES_DB: docuseal_test
ports: ["5432:5432"]
options: >-
--health-cmd pg_isready
--health-cmd "pg_isready -U postgres -d docuseal_test"
--health-interval 10s
--health-timeout 5s
--health-retries 5
steps:
- name: Install packages
run: apk add --no-cache build-base git libpq-dev yaml-dev nodejs yarn tar zstd gzip wget unzip vips leptonica chromium
- uses: actions/checkout@v4
- name: Install Ruby
uses: ruby/setup-ruby@v1
with:
ruby-version: 3.4.2
- name: Set up Node
uses: actions/setup-node@v1
with:
node-version: 20.9.0
- name: Install Chrome
uses: browser-actions/setup-chrome@latest
with:
chrome-version: 125
- name: Cache node_modules
uses: actions/cache@v4
with:
path: node_modules
key: ${{ runner.os }}-node-${{ hashFiles('**/yarn.lock') }}
key: ${{ runner.os }}-musl-node-${{ hashFiles('**/yarn.lock') }}
- name: Cache gems
uses: actions/cache@v4
with:
path: vendor/bundle
key: ${{ runner.os }}-gem-${{ hashFiles('**/Gemfile.lock') }}
key: ${{ runner.os }}-musl-gem-${{ hashFiles('**/Gemfile.lock') }}
restore-keys: |
${{ runner.os }}-gem-
${{ runner.os }}-musl-gem-
- name: Install dependencies
env:
RAILS_ENV: test
@@ -161,17 +152,16 @@ jobs:
bundle config path vendor/bundle
bundle install --jobs 4 --retry 4
yarn install
sudo apt-get update
sudo apt-get install -y libvips
wget -O pdfium-linux.tgz "https://github.com/docusealco/pdfium-binaries/releases/latest/download/pdfium-linux-$(uname -m | sed 's/x86_64/x64/;s/aarch64/arm64/').tgz"
sudo tar -xzf pdfium-linux.tgz --strip-components=1 -C /usr/lib lib/libpdfium.so
rm -f pdfium-linux.tgz
wget -O pdfium-linux.zip "https://github.com/docusealco/pdfium-binaries/releases/latest/download/pdfium-musl-$(uname -m).zip"
unzip -q pdfium-linux.zip -d /tmp/pdfium-linux
cp /tmp/pdfium-linux/lib/libpdfium.so /usr/lib/libpdfium.so
rm -rf pdfium-linux.zip /tmp/pdfium-linux
- name: Run
env:
RAILS_ENV: test
NODE_ENV: test
COVERAGE: true
DATABASE_URL: postgres://postgres:postgres@localhost:5432/docuseal_test
DATABASE_URL: postgres://postgres:postgres@postgres:5432/docuseal_test
run: |
bundle exec rake db:create
bundle exec rake db:migrate
+3 -1
View File
@@ -30,7 +30,9 @@ jobs:
uses: docker/setup-buildx-action@v3
- name: Create .version file
run: echo ${{ github.ref_name }} > .version
env:
REF_NAME: ${{ github.ref_name }}
run: echo "$REF_NAME" > .version
- name: Login to Docker Hub
uses: docker/login-action@v3
+29 -4
View File
@@ -1,4 +1,4 @@
require:
plugins:
- rubocop-performance
- rubocop-rails
- rubocop-rspec
@@ -10,7 +10,7 @@ AllCops:
- node_modules/**/*
- bin/*
- vendor/**/*
TargetRubyVersion: '3.3'
TargetRubyVersion: '4.0'
SuggestExtensions: false
Metrics/BlockLength:
@@ -28,7 +28,7 @@ Lint/MissingSuper:
Enabled: false
Metrics/ParameterLists:
Max: 10
Max: 12
Metrics/MethodLength:
Max: 30
@@ -48,6 +48,18 @@ Style/MultipleComparison:
Style/NumericPredicate:
Enabled: false
Style/MinMaxComparison:
Enabled: false
Style/EmptyClassDefinition:
Enabled: false
Style/OneClassPerFile:
Enabled: false
Layout/MultilineMethodCallIndentation:
Enabled: false
Naming/PredicateMethod:
Enabled: false
@@ -81,7 +93,7 @@ RSpec/AnyInstance:
Enabled: false
Metrics/BlockNesting:
Max: 5
Max: 6
Rails/I18nLocaleTexts:
Enabled: false
@@ -97,3 +109,16 @@ Rails/ApplicationController:
Rails/Output:
Enabled: false
Rails/StrongParametersExpect:
Enabled: false
Rails/RedirectBackOrTo:
Enabled: false
Rails/UnknownEnv:
Environments:
- development
- test
- production
- local
+44 -36
View File
@@ -1,22 +1,23 @@
FROM ruby:3.4.2-alpine AS download
FROM ruby:4.0.5-alpine AS download
WORKDIR /fonts
RUN apk --no-cache add fontforge wget && \
RUN apk --no-cache add wget unzip && \
wget https://github.com/satbyy/go-noto-universal/releases/download/v7.0/GoNotoKurrent-Regular.ttf && \
wget https://github.com/satbyy/go-noto-universal/releases/download/v7.0/GoNotoKurrent-Bold.ttf && \
wget https://github.com/impallari/DancingScript/raw/master/fonts/DancingScript-Regular.otf && \
wget https://cdn.jsdelivr.net/gh/notofonts/notofonts.github.io/fonts/NotoSansSymbols2/hinted/ttf/NotoSansSymbols2-Regular.ttf && \
wget https://github.com/Maxattax97/gnu-freefont/raw/master/ttf/FreeSans.ttf && \
wget https://github.com/impallari/DancingScript/raw/master/OFL.txt && \
wget https://raw.githubusercontent.com/impallari/DancingScript/master/OFL.txt && \
wget https://raw.githubusercontent.com/notofonts/noto-fonts/refs/heads/main/LICENSE && \
wget -O /model.onnx "https://github.com/docusealco/fields-detection/releases/download/2.0.0/model_704_int8.onnx" && \
wget -O pdfium-linux.tgz "https://github.com/docusealco/pdfium-binaries/releases/latest/download/pdfium-linux-$(uname -m | sed 's/x86_64/x64/;s/aarch64/arm64/').tgz" && \
wget -O pdfium-linux.zip "https://github.com/docusealco/pdfium-binaries/releases/download/20260920/pdfium-musl-$(uname -m).zip" && \
case "$(uname -m)" in \
x86_64) echo "bbca8a648dbd1ba81f9c6c223f21dc394cf1ad381c6c17d60c6dcc4a7d3a2ae0 pdfium-linux.zip" ;; \
aarch64) echo "73b72ed79b9bfdd494dee7e863482c24971a8d4b11f6cd1289aa170d31304a1f pdfium-linux.zip" ;; \
esac | sha256sum -c - && \
mkdir -p /pdfium-linux && \
tar -xzf pdfium-linux.tgz -C /pdfium-linux
unzip -q pdfium-linux.zip -d /pdfium-linux
RUN fontforge -lang=py -c 'font1 = fontforge.open("FreeSans.ttf"); font2 = fontforge.open("NotoSansSymbols2-Regular.ttf"); font1.mergeFonts(font2); font1.generate("FreeSans.ttf")'
FROM ruby:3.4.2-alpine AS webpack
FROM ruby:4.0.5-alpine AS webpack
ENV RAILS_ENV=production
ENV NODE_ENV=production
@@ -36,22 +37,25 @@ COPY ./config/shakapacker.yml ./config/shakapacker.yml
COPY ./postcss.config.js ./postcss.config.js
COPY ./tailwind.config.js ./tailwind.config.js
COPY ./tailwind.form.config.js ./tailwind.form.config.js
COPY ./tailwind.dynamic.config.js ./tailwind.dynamic.config.js
COPY ./tailwind.application.config.js ./tailwind.application.config.js
COPY ./app/javascript ./app/javascript
COPY ./app/views ./app/views
RUN echo "gem 'shakapacker'" > Gemfile && ./bin/shakapacker
FROM ruby:3.4.2-alpine AS app
FROM ruby:4.0.5-alpine AS app
ENV RAILS_ENV=production
ENV BUNDLE_WITHOUT="development:test"
ENV LD_PRELOAD=/lib/libgcompat.so.0
ENV OPENSSL_CONF=/app/openssl_legacy.cnf
ENV OPENSSL_CONF=/etc/openssl_legacy.cnf
WORKDIR /app
RUN 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
RUN apk add --no-cache libpq vips redis onnxruntime leptonica jemalloc font-liberation font-carlito && \
rm -f /usr/bin/onnx_test_runner /usr/bin/onnxruntime_test
RUN addgroup -g 2000 docuseal && adduser -u 2000 -G docuseal -s /bin/sh -D -h /home/docuseal docuseal
RUN echo $'.include = /etc/ssl/openssl.cnf\n\
\n\
@@ -63,37 +67,41 @@ legacy = legacy_sect\n\
activate = 1\n\
\n\
[legacy_sect]\n\
activate = 1' >> /app/openssl_legacy.cnf
activate = 1' >> /etc/openssl_legacy.cnf
COPY ./Gemfile ./Gemfile.lock ./
COPY --chown=docuseal:docuseal ./Gemfile ./Gemfile.lock ./
RUN apk add --no-cache build-base && bundle install && apk del --no-cache build-base && rm -rf ~/.bundle /usr/local/bundle/cache && ruby -e "puts Dir['/usr/local/bundle/**/{spec,rdoc,resources/shared,resources/collation,resources/locales}']" | xargs rm -rf && ln -sf /usr/lib/libonnxruntime.so.1 $(ruby -e "print Dir[Gem::Specification.find_by_name('onnxruntime').gem_dir + '/vendor/*.so'].first")
RUN apk add --no-cache build-base git libpq-dev yaml-dev && bundle install && apk del --no-cache build-base git libpq-dev yaml-dev && rm -rf ~/.bundle /usr/local/bundle/cache && ruby -e "puts Dir['/usr/local/bundle/**/{spec,rdoc,resources/shared,resources/collation,resources/locales,resources/unicode_data/properties}'] + Dir['/usr/local/bundle/gems/*/{test,tests,examples,sample,misc,doc,docs}'] + Dir['/usr/local/bundle/gems/*/ext/**/*.{c,h,o,S}']" | xargs rm -rf && ln -sf /usr/lib/libonnxruntime.so.1 $(ruby -e "print Dir[Gem::Specification.find_by_name('onnxruntime').gem_dir + '/vendor/*.so'].first")
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 LICENSE_ADDITIONAL_TERMS README.md Rakefile config.ru .version ./
COPY --chown=docuseal:docuseal .version ./public/version
COPY ./bin ./bin
COPY ./app ./app
COPY ./config ./config
COPY ./db/migrate ./db/migrate
COPY ./log ./log
COPY ./lib ./lib
COPY ./public ./public
COPY ./tmp ./tmp
COPY LICENSE README.md Rakefile config.ru .version ./
COPY .version ./public/version
COPY --from=download /fonts/GoNotoKurrent-Regular.ttf /fonts/GoNotoKurrent-Bold.ttf /fonts/DancingScript-Regular.otf /fonts/OFL.txt /fonts
COPY --from=download /fonts/FreeSans.ttf /usr/share/fonts/freefont
COPY --chown=docuseal:docuseal --from=download /fonts/GoNotoKurrent-Regular.ttf /fonts/GoNotoKurrent-Bold.ttf /fonts/DancingScript-Regular.otf /fonts/OFL.txt /fonts/LICENSE /fonts/
COPY --from=download /pdfium-linux/lib/libpdfium.so /usr/lib/libpdfium.so
COPY --from=download /pdfium-linux/licenses/pdfium.txt /usr/lib/libpdfium-LICENSE.txt
COPY --from=download /model.onnx /app/tmp/model.onnx
COPY --from=webpack /app/public/packs ./public/packs
COPY --from=download /pdfium-linux/licenses/ /usr/lib/libpdfium-licenses/
COPY --chown=docuseal:docuseal --from=download /model.onnx /app/tmp/model.onnx
COPY --chown=docuseal:docuseal --from=webpack /app/public/packs ./public/packs
RUN ln -s /fonts /app/public/fonts
RUN bundle exec bootsnap precompile -j 1 --gemfile app/ lib/
RUN mkdir -p /app/public/fonts && ln -s /fonts/DancingScript-Regular.otf /app/public/fonts/ && \
mkdir -p /usr/share/fonts/noto/bold && ln -s /fonts/GoNotoKurrent-Regular.ttf /usr/share/fonts/noto/ && ln -s /fonts/GoNotoKurrent-Bold.ttf /usr/share/fonts/noto/bold/ && fc-cache -f && \
bundle exec bootsnap precompile -j 1 --gemfile app/ lib/ && \
chown -R docuseal:docuseal /app/tmp/cache
WORKDIR /data/docuseal
ENV HOME=/home/docuseal
ENV WORKDIR=/data/docuseal
ENV VIPS_MAX_COORD=17000
ENV VIPS_BLOCK_UNTRUSTED=1
# ENV LD_PRELOAD=/usr/lib/libjemalloc.so.2
ENV MALLOC_CONF=dirty_decay_ms:1000,narenas:2,background_thread:true
EXPOSE 3000
CMD ["/app/bin/bundle", "exec", "puma", "-C", "/app/config/puma.rb", "--dir", "/app"]
+14 -15
View File
@@ -2,16 +2,17 @@
source 'https://rubygems.org'
ruby '3.4.2'
ruby '4.0.5'
gem 'arabic-letter-connector', require: 'arabic-letter-connector/logic'
gem 'addressable'
gem 'arabic-letter-connector', require: false
gem 'aws-sdk-s3', require: false
gem 'aws-sdk-secretsmanager', require: false
gem 'azure-storage-blob', require: false
gem 'azure-blob', require: false
gem 'bootsnap', require: false
gem 'cancancan'
gem 'csv'
gem 'csv-safe'
gem 'csv', require: false
gem 'csv-safe', require: false
gem 'devise'
gem 'devise-two-factor'
gem 'dotenv', require: false
@@ -20,34 +21,32 @@ gem 'faraday'
gem 'faraday-follow_redirects'
gem 'google-cloud-storage', require: false
gem 'hexapdf'
gem 'image_processing'
gem 'jwt'
gem 'jwt', require: false
gem 'lograge'
gem 'mysql2', require: false
gem 'numo-narray'
gem 'oj'
gem 'onnxruntime'
gem 'numo-narray-alt', require: false
gem 'onnxruntime', require: false
gem 'pagy'
gem 'pg', require: false
gem 'premailer-rails'
gem 'pretender'
gem 'puma', require: false
gem 'rack'
gem 'rails'
gem 'rails_autolink'
gem 'rails-i18n'
gem 'rotp'
gem 'rouge', require: false
gem 'rqrcode'
gem 'rqrcode', require: false
gem 'ruby-vips'
gem 'rubyXL'
gem 'rubyzip', require: false
gem 'shakapacker'
gem 'sidekiq'
gem 'sqlite3', require: false
gem 'strip_attributes'
gem 'trilogy', require: false
gem 'turbo-rails'
gem 'twitter_cldr', require: false
gem 'tzinfo-data'
gem 'wordcraft', require: false, group: :ce
gem 'xlsxtream', require: false
group :development, :test do
gem 'better_html'
+310 -331
View File
@@ -1,29 +1,31 @@
GEM
remote: https://rubygems.org/
specs:
actioncable (8.0.2.1)
actionpack (= 8.0.2.1)
activesupport (= 8.0.2.1)
action_text-trix (2.1.19)
railties
actioncable (8.1.3.1)
actionpack (= 8.1.3.1)
activesupport (= 8.1.3.1)
nio4r (~> 2.0)
websocket-driver (>= 0.6.1)
zeitwerk (~> 2.6)
actionmailbox (8.0.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)
actionmailbox (8.1.3.1)
actionpack (= 8.1.3.1)
activejob (= 8.1.3.1)
activerecord (= 8.1.3.1)
activestorage (= 8.1.3.1)
activesupport (= 8.1.3.1)
mail (>= 2.8.0)
actionmailer (8.0.2.1)
actionpack (= 8.0.2.1)
actionview (= 8.0.2.1)
activejob (= 8.0.2.1)
activesupport (= 8.0.2.1)
actionmailer (8.1.3.1)
actionpack (= 8.1.3.1)
actionview (= 8.1.3.1)
activejob (= 8.1.3.1)
activesupport (= 8.1.3.1)
mail (>= 2.8.0)
rails-dom-testing (~> 2.2)
actionpack (8.0.2.1)
actionview (= 8.0.2.1)
activesupport (= 8.0.2.1)
actionpack (8.1.3.1)
actionview (= 8.1.3.1)
activesupport (= 8.1.3.1)
nokogiri (>= 1.8.5)
rack (>= 2.2.4)
rack-session (>= 1.0.1)
@@ -31,55 +33,58 @@ GEM
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)
actiontext (8.1.3.1)
action_text-trix (~> 2.1.15)
actionpack (= 8.1.3.1)
activerecord (= 8.1.3.1)
activestorage (= 8.1.3.1)
activesupport (= 8.1.3.1)
globalid (>= 0.6.0)
nokogiri (>= 1.8.5)
actionview (8.0.2.1)
activesupport (= 8.0.2.1)
actionview (8.1.3.1)
activesupport (= 8.1.3.1)
builder (~> 3.1)
erubi (~> 1.11)
rails-dom-testing (~> 2.2)
rails-html-sanitizer (~> 1.6)
activejob (8.0.2.1)
activesupport (= 8.0.2.1)
activejob (8.1.3.1)
activesupport (= 8.1.3.1)
globalid (>= 0.3.6)
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)
activemodel (8.1.3.1)
activesupport (= 8.1.3.1)
activerecord (8.1.3.1)
activemodel (= 8.1.3.1)
activesupport (= 8.1.3.1)
timeout (>= 0.4.0)
activestorage (8.0.2.1)
actionpack (= 8.0.2.1)
activejob (= 8.0.2.1)
activerecord (= 8.0.2.1)
activesupport (= 8.0.2.1)
activestorage (8.1.3.1)
actionpack (= 8.1.3.1)
activejob (= 8.1.3.1)
activerecord (= 8.1.3.1)
activesupport (= 8.1.3.1)
marcel (~> 1.0)
activesupport (8.0.2.1)
activesupport (8.1.3.1)
base64
benchmark (>= 0.3)
bigdecimal
concurrent-ruby (~> 1.0, >= 1.3.1)
connection_pool (>= 2.2.5)
drb
i18n (>= 1.6, < 2)
json
logger (>= 1.4.2)
minitest (>= 5.1)
securerandom (>= 0.3)
tzinfo (~> 2.0, >= 2.0.5)
uri (>= 0.13.1)
addressable (2.8.7)
public_suffix (>= 2.0.2, < 7.0)
annotaterb (4.14.0)
addressable (2.9.0)
public_suffix (>= 2.0.2, < 8.0)
annotaterb (4.22.0)
activerecord (>= 6.0.0)
activesupport (>= 6.0.0)
arabic-letter-connector (0.1.1)
ast (2.4.3)
aws-eventstream (1.4.0)
aws-partitions (1.1197.0)
aws-sdk-core (3.240.0)
aws-partitions (1.1233.0)
aws-sdk-core (3.244.0)
aws-eventstream (~> 1, >= 1.3.0)
aws-partitions (~> 1, >= 1.992.0)
aws-sigv4 (~> 1.9)
@@ -87,44 +92,38 @@ GEM
bigdecimal
jmespath (~> 1, >= 1.6.1)
logger
aws-sdk-kms (1.118.0)
aws-sdk-core (~> 3, >= 3.239.1)
aws-sdk-kms (1.123.0)
aws-sdk-core (~> 3, >= 3.244.0)
aws-sigv4 (~> 1.5)
aws-sdk-s3 (1.208.0)
aws-sdk-core (~> 3, >= 3.234.0)
aws-sdk-s3 (1.218.0)
aws-sdk-core (~> 3, >= 3.244.0)
aws-sdk-kms (~> 1)
aws-sigv4 (~> 1.5)
aws-sdk-secretsmanager (1.110.0)
aws-sdk-core (~> 3, >= 3.210.0)
aws-sdk-secretsmanager (1.129.0)
aws-sdk-core (~> 3, >= 3.244.0)
aws-sigv4 (~> 1.5)
aws-sigv4 (1.12.1)
aws-eventstream (~> 1, >= 1.0.2)
azure-storage-blob (2.0.3)
azure-storage-common (~> 2.0)
nokogiri (~> 1, >= 1.10.8)
azure-storage-common (2.0.4)
faraday (~> 1.0)
faraday_middleware (~> 1.0, >= 1.0.0.rc1)
net-http-persistent (~> 4.0)
nokogiri (~> 1, >= 1.10.8)
azure-blob (0.8.0)
cgi
rexml
base64 (0.3.0)
bcrypt (3.1.20)
benchmark (0.4.1)
better_html (2.1.1)
actionview (>= 6.0)
activesupport (>= 6.0)
bcrypt (3.1.22)
better_html (2.2.0)
actionview (>= 7.0)
activesupport (>= 7.0)
ast (~> 2.0)
erubi (~> 1.4)
parser (>= 2.4)
smart_properties
bigdecimal (3.2.2)
bigdecimal (4.1.2)
bindex (0.8.1)
bootsnap (1.18.4)
bootsnap (1.23.0)
msgpack (~> 1.2)
brakeman (7.0.0)
brakeman (8.0.4)
racc
builder (3.3.0)
bullet (8.0.0)
bullet (8.1.0)
activesupport (>= 3.0.0)
uniform_notifier (~> 1.11)
camertron-eprun (1.1.1)
@@ -138,51 +137,50 @@ GEM
rack-test (>= 0.6.3)
regexp_parser (>= 1.5, < 3.0)
xpath (~> 3.2)
cgi (0.5.1)
childprocess (5.1.0)
logger (~> 1.5)
chunky_png (1.4.0)
cldr-plurals-runtime-rb (1.1.0)
cmdparse (3.0.7)
coderay (1.1.3)
concurrent-ruby (1.3.5)
connection_pool (2.5.3)
crack (1.0.0)
concurrent-ruby (1.3.8)
connection_pool (3.0.2)
crack (1.0.1)
bigdecimal
rexml
crass (1.0.6)
css_parser (1.21.0)
addressable
csv (3.3.2)
crass (1.0.7)
csv (3.3.5)
csv-safe (3.3.1)
csv (~> 3.0)
cuprite (0.15.1)
cuprite (0.17)
capybara (~> 3.0)
ferrum (~> 0.15.0)
date (3.4.1)
debug (1.10.0)
ferrum (~> 0.17.0)
date (3.5.1)
debug (1.11.1)
irb (~> 1.10)
reline (>= 0.3.8)
declarative (0.0.20)
devise (4.9.4)
devise (5.0.4)
bcrypt (~> 3.0)
orm_adapter (~> 0.1)
railties (>= 4.1.0)
railties (>= 7.0)
responders
warden (~> 1.2.3)
devise-two-factor (6.1.0)
activesupport (>= 7.0, < 8.1)
devise (~> 4.0)
railties (>= 7.0, < 8.1)
devise-two-factor (6.4.0)
activesupport (>= 7.2, < 8.2)
devise (>= 4.0, < 6.0)
railties (>= 7.2, < 8.2)
rotp (~> 6.0)
diff-lcs (1.5.1)
digest-crc (0.6.5)
diff-lcs (1.6.2)
digest-crc (0.7.0)
rake (>= 12.0.0, < 14.0.0)
docile (1.4.1)
dotenv (3.1.7)
dotenv (3.2.0)
drb (2.2.3)
email_typo (0.2.3)
erb (5.0.2)
erb_lint (0.7.0)
erb (6.0.6)
erb_lint (0.9.0)
activesupport
better_html (>= 2.0.1)
parser (>= 2.7.1.4)
@@ -190,117 +188,97 @@ GEM
rubocop (>= 1)
smart_properties
erubi (1.13.1)
factory_bot (6.5.0)
activesupport (>= 5.0.0)
factory_bot_rails (6.4.4)
factory_bot (6.5.6)
activesupport (>= 6.1.0)
factory_bot_rails (6.5.1)
factory_bot (~> 6.5)
railties (>= 5.0.0)
faker (3.5.1)
railties (>= 6.1.0)
faker (3.6.1)
i18n (>= 1.8.11, < 2)
faraday (1.10.4)
faraday-em_http (~> 1.0)
faraday-em_synchrony (~> 1.0)
faraday-excon (~> 1.1)
faraday-httpclient (~> 1.0)
faraday-multipart (~> 1.0)
faraday-net_http (~> 1.0)
faraday-net_http_persistent (~> 1.0)
faraday-patron (~> 1.0)
faraday-rack (~> 1.0)
faraday-retry (~> 1.0)
ruby2_keywords (>= 0.0.4)
faraday-em_http (1.0.0)
faraday-em_synchrony (1.0.0)
faraday-excon (1.1.0)
faraday-follow_redirects (0.3.0)
faraday (2.14.3)
faraday-net_http (>= 2.0, < 3.5)
json
logger
faraday-follow_redirects (0.5.0)
faraday (>= 1, < 3)
faraday-httpclient (1.0.1)
faraday-multipart (1.1.0)
multipart-post (~> 2.0)
faraday-net_http (1.0.2)
faraday-net_http_persistent (1.2.0)
faraday-patron (1.0.0)
faraday-rack (1.0.0)
faraday-retry (1.0.3)
faraday_middleware (1.2.1)
faraday (~> 1.0)
ferrum (0.15)
faraday-net_http (3.4.4)
net-http (~> 0.5)
ferrum (0.17.2)
addressable (~> 2.5)
base64 (~> 0.2)
concurrent-ruby (~> 1.1)
webrick (~> 1.7)
websocket-driver (~> 0.7)
ffi (1.17.1)
ffi (1.17.1-aarch64-linux-gnu)
ffi (1.17.1-aarch64-linux-musl)
ffi (1.17.1-arm64-darwin)
ffi (1.17.1-x86_64-linux-gnu)
ffi (1.17.1-x86_64-linux-musl)
foreman (0.88.1)
ffi (1.17.4-aarch64-linux-gnu)
ffi (1.17.4-aarch64-linux-musl)
ffi (1.17.4-arm64-darwin)
ffi (1.17.4-x86_64-linux-gnu)
ffi (1.17.4-x86_64-linux-musl)
foreman (0.90.0)
thor (~> 1.4)
geom2d (0.4.1)
globalid (1.2.1)
globalid (1.4.0)
activesupport (>= 6.1)
google-apis-core (0.15.1)
addressable (~> 2.5, >= 2.5.1)
googleauth (~> 1.9)
httpclient (>= 2.8.3, < 3.a)
mini_mime (~> 1.0)
mutex_m
google-apis-core (1.0.2)
addressable (~> 2.8, >= 2.8.7)
faraday (~> 2.13)
faraday-follow_redirects (~> 0.3)
googleauth (~> 1.14)
mini_mime (~> 1.1)
representable (~> 3.0)
retriable (>= 2.0, < 4.a)
google-apis-iamcredentials_v1 (0.22.0)
retriable (~> 3.1)
google-apis-iamcredentials_v1 (0.26.0)
google-apis-core (>= 0.15.0, < 2.a)
google-apis-storage_v1 (0.49.0)
google-apis-storage_v1 (0.61.0)
google-apis-core (>= 0.15.0, < 2.a)
google-cloud-core (1.7.1)
google-cloud-core (1.8.0)
google-cloud-env (>= 1.0, < 3.a)
google-cloud-errors (~> 1.0)
google-cloud-env (2.2.1)
google-cloud-env (2.3.1)
base64 (~> 0.2)
faraday (>= 1.0, < 3.a)
google-cloud-errors (1.4.0)
google-cloud-storage (1.54.0)
google-cloud-errors (1.6.0)
google-cloud-storage (1.59.0)
addressable (~> 2.8)
digest-crc (~> 0.4)
google-apis-core (~> 0.13)
google-apis-core (>= 0.18, < 2)
google-apis-iamcredentials_v1 (~> 0.18)
google-apis-storage_v1 (~> 0.38)
google-apis-storage_v1 (>= 0.42)
google-cloud-core (~> 1.6)
googleauth (~> 1.9)
mini_mime (~> 1.0)
google-logging-utils (0.1.0)
googleauth (1.12.2)
google-logging-utils (0.2.0)
googleauth (1.16.2)
faraday (>= 1.0, < 3.a)
google-cloud-env (~> 2.2)
google-logging-utils (~> 0.1)
jwt (>= 1.4, < 3.0)
jwt (>= 1.4, < 4.0)
multi_json (~> 1.11)
os (>= 0.9, < 2.0)
signet (>= 0.16, < 2.a)
hashdiff (1.1.2)
hexapdf (1.4.0)
hashdiff (1.2.1)
hexapdf (1.7.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.7)
i18n (1.15.2)
concurrent-ruby (~> 1.0)
image_processing (1.13.0)
mini_magick (>= 4.9.5, < 5)
ruby-vips (>= 2.0.17, < 3)
io-console (0.8.1)
irb (1.15.2)
io-console (0.8.2)
irb (1.18.0)
pp (>= 0.6.0)
prism (>= 1.3.0)
rdoc (>= 4.0.0)
reline (>= 0.4.2)
jmespath (1.6.2)
json (2.15.0)
jwt (2.9.3)
json (2.21.2)
jwt (3.2.0)
base64
language_server-protocol (3.17.0.5)
launchy (3.0.1)
launchy (3.1.1)
addressable (~> 2.8)
childprocess (~> 5.0)
logger (~> 1.6)
letter_opener (1.10.0)
launchy (>= 2.2, < 4)
letter_opener_web (3.0.0)
@@ -315,29 +293,27 @@ GEM
activesupport (>= 4)
railties (>= 4)
request_store (~> 1.0)
loofah (2.24.1)
loofah (2.25.2)
crass (~> 1.0.2)
nokogiri (>= 1.12.0)
mail (2.8.1)
mail (2.9.1)
logger
mini_mime (>= 0.1.1)
net-imap
net-pop
net-smtp
marcel (1.0.4)
matrix (0.4.2)
marcel (1.2.1)
matrix (0.4.3)
method_source (1.1.0)
mini_magick (4.13.2)
mini_mime (1.1.5)
mini_portile2 (2.8.9)
minitest (5.25.5)
msgpack (1.7.5)
multi_json (1.15.0)
multipart-post (2.4.1)
mutex_m (0.3.0)
mysql2 (0.5.6)
net-http-persistent (4.0.5)
connection_pool (~> 2.2)
net-imap (0.5.9)
minitest (6.0.6)
drb (~> 2.0)
prism (~> 1.5)
msgpack (1.8.4)
multi_json (1.19.1)
net-http (0.9.1)
uri (>= 0.11.1)
net-imap (0.6.6)
date
net-protocol
net-pop (0.1.2)
@@ -346,124 +322,115 @@ GEM
timeout
net-smtp (0.5.1)
net-protocol
nio4r (2.7.4)
nokogiri (1.18.9)
mini_portile2 (~> 2.8.2)
nio4r (2.7.5)
nokogiri (1.19.4-aarch64-linux-gnu)
racc (~> 1.4)
nokogiri (1.18.9-aarch64-linux-gnu)
nokogiri (1.19.4-aarch64-linux-musl)
racc (~> 1.4)
nokogiri (1.18.9-aarch64-linux-musl)
nokogiri (1.19.4-arm64-darwin)
racc (~> 1.4)
nokogiri (1.18.9-arm64-darwin)
nokogiri (1.19.4-x86_64-linux-gnu)
racc (~> 1.4)
nokogiri (1.18.9-x86_64-linux-gnu)
nokogiri (1.19.4-x86_64-linux-musl)
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
numo-narray-alt (0.10.3)
onnxruntime (0.10.1-aarch64-linux)
ffi
onnxruntime (0.10.1-arm64-darwin)
ffi
onnxruntime (0.10.1-x86_64-linux)
ffi
openssl (3.3.0)
openssl (4.0.1)
orm_adapter (0.5.0)
os (1.1.4)
ostruct (0.6.3)
package_json (0.1.0)
pagy (9.3.3)
parallel (1.27.0)
parser (3.3.9.0)
package_json (0.2.0)
pagy (43.6.1)
json
uri
yaml
parallel (1.28.0)
parser (3.3.11.1)
ast (~> 2.4.1)
racc
pg (1.5.9)
pp (0.6.2)
pg (1.6.3-aarch64-linux)
pg (1.6.3-aarch64-linux-musl)
pg (1.6.3-arm64-darwin)
pg (1.6.3-x86_64-linux)
pg (1.6.3-x86_64-linux-musl)
pp (0.6.4)
prettyprint
premailer (1.27.0)
addressable
css_parser (>= 1.19.0)
htmlentities (>= 4.0.0)
premailer-rails (1.12.0)
actionmailer (>= 3)
net-smtp
premailer (~> 1.7, >= 1.7.9)
pretender (0.5.0)
actionpack (>= 6.1)
pretender (1.0.0)
actionpack (>= 7.2)
prettyprint (0.2.0)
prism (1.5.1)
pry (0.15.0)
prism (1.9.0)
pry (0.16.0)
coderay (~> 1.1)
method_source (~> 1.0)
reline (>= 0.6.0)
pry-rails (0.3.11)
pry (>= 0.13.0)
psych (5.2.6)
date
stringio
public_suffix (6.0.1)
puma (6.5.0)
public_suffix (7.0.5)
puma (8.0.2)
nio4r (~> 2.0)
racc (1.8.1)
rack (3.2.3)
rack (3.2.6)
rack-proxy (0.7.7)
rack
rack-session (2.1.1)
rack-session (2.1.2)
base64 (>= 0.1.0)
rack (>= 3.0.0)
rack-test (2.2.0)
rack (>= 1.3)
rackup (2.2.1)
rackup (2.3.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)
rails (8.1.3.1)
actioncable (= 8.1.3.1)
actionmailbox (= 8.1.3.1)
actionmailer (= 8.1.3.1)
actionpack (= 8.1.3.1)
actiontext (= 8.1.3.1)
actionview (= 8.1.3.1)
activejob (= 8.1.3.1)
activemodel (= 8.1.3.1)
activerecord (= 8.1.3.1)
activestorage (= 8.1.3.1)
activesupport (= 8.1.3.1)
bundler (>= 1.15.0)
railties (= 8.0.2.1)
railties (= 8.1.3.1)
rails-dom-testing (2.3.0)
activesupport (>= 5.0.0)
minitest
nokogiri (>= 1.6)
rails-html-sanitizer (1.6.2)
loofah (~> 2.21)
rails-html-sanitizer (1.7.1)
loofah (~> 2.25, >= 2.25.2)
nokogiri (>= 1.15.7, != 1.16.7, != 1.16.6, != 1.16.5, != 1.16.4, != 1.16.3, != 1.16.2, != 1.16.1, != 1.16.0.rc1, != 1.16.0)
rails-i18n (8.0.1)
rails-i18n (8.1.0)
i18n (>= 0.7, < 2)
railties (>= 8.0.0, < 9)
rails_autolink (1.1.8)
actionview (> 3.1)
activesupport (> 3.1)
railties (> 3.1)
railties (8.0.2.1)
actionpack (= 8.0.2.1)
activesupport (= 8.0.2.1)
railties (8.1.3.1)
actionpack (= 8.1.3.1)
activesupport (= 8.1.3.1)
irb (~> 1.13)
rackup (>= 1.0.0)
rake (>= 12.2)
thor (~> 1.0, >= 1.2.2)
tsort (>= 0.2)
zeitwerk (~> 2.6)
rainbow (3.1.1)
rake (13.3.0)
rdoc (6.14.2)
rake (13.4.2)
rbs (4.1.0)
logger
prism (>= 1.6.0)
tsort
rdoc (8.0.0)
erb
psych (>= 4.0.0)
redis-client (0.23.0)
prism (>= 1.6.0)
rbs (>= 4.0.0)
tsort
redis-client (0.29.0)
connection_pool
regexp_parser (2.11.3)
reline (0.6.2)
reline (0.6.3)
io-console (~> 0.5)
representable (3.2.0)
declarative (< 0.1.0)
@@ -471,35 +438,35 @@ GEM
uber (< 0.2.0)
request_store (1.7.0)
rack (>= 1.4)
responders (3.1.1)
actionpack (>= 5.2)
railties (>= 5.2)
retriable (3.1.2)
responders (3.2.0)
actionpack (>= 7.0)
railties (>= 7.0)
retriable (3.4.1)
rexml (3.4.4)
rotp (6.3.0)
rouge (4.5.2)
rqrcode (2.2.0)
rouge (4.7.0)
rqrcode (3.2.0)
chunky_png (~> 1.0)
rqrcode_core (~> 1.0)
rqrcode_core (1.2.0)
rspec-core (3.13.2)
rqrcode_core (~> 2.0)
rqrcode_core (2.1.0)
rspec-core (3.13.6)
rspec-support (~> 3.13.0)
rspec-expectations (3.13.3)
rspec-expectations (3.13.5)
diff-lcs (>= 1.2.0, < 2.0)
rspec-support (~> 3.13.0)
rspec-mocks (3.13.2)
rspec-mocks (3.13.8)
diff-lcs (>= 1.2.0, < 2.0)
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)
rspec-rails (8.0.4)
actionpack (>= 7.2)
activesupport (>= 7.2)
railties (>= 7.2)
rspec-core (>= 3.13.0, < 5.0.0)
rspec-expectations (>= 3.13.0, < 5.0.0)
rspec-mocks (>= 3.13.0, < 5.0.0)
rspec-support (>= 3.13.0, < 5.0.0)
rspec-support (3.13.7)
rubocop (1.86.0)
json (~> 2.3)
language_server-protocol (~> 3.17.0.2)
lint_roller (~> 1.1.0)
@@ -507,122 +474,136 @@ GEM
parser (>= 3.3.0.2)
rainbow (>= 2.2.2, < 4.0)
regexp_parser (>= 2.9.3, < 3.0)
rubocop-ast (>= 1.47.1, < 2.0)
rubocop-ast (>= 1.49.0, < 2.0)
ruby-progressbar (~> 1.7)
unicode-display_width (>= 2.4.0, < 4.0)
rubocop-ast (1.47.1)
rubocop-ast (1.49.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)
prism (~> 1.7)
rubocop-performance (1.26.1)
lint_roller (~> 1.1)
rubocop (>= 1.75.0, < 2.0)
rubocop-ast (>= 1.47.1, < 2.0)
rubocop-rails (2.34.3)
activesupport (>= 4.2.0)
lint_roller (~> 1.1)
rack (>= 1.1)
rubocop (>= 1.52.0, < 2.0)
rubocop-ast (>= 1.31.1, < 2.0)
rubocop-rspec (3.3.0)
rubocop (~> 1.61)
rubocop (>= 1.75.0, < 2.0)
rubocop-ast (>= 1.44.0, < 2.0)
rubocop-rspec (3.9.0)
lint_roller (~> 1.1)
rubocop (~> 1.81)
ruby-progressbar (1.13.0)
ruby-vips (2.2.2)
ruby-vips (2.3.0)
ffi (~> 1.12)
logger
ruby2_keywords (0.0.5)
rubyXL (3.4.33)
nokogiri (>= 1.10.8)
rubyzip (>= 1.3.0)
rubyzip (2.3.2)
rubyzip (3.7.0)
securerandom (0.4.1)
semantic_range (3.1.0)
shakapacker (8.0.2)
semantic_range (3.1.1)
shakapacker (9.7.0)
activesupport (>= 5.2)
package_json
rack-proxy (>= 0.6.1)
railties (>= 5.2)
semantic_range (>= 2.3.0)
sidekiq (7.3.7)
connection_pool (>= 2.3.0)
logger
rack (>= 2.2.4)
redis-client (>= 0.22.2)
signet (0.19.0)
sidekiq (8.1.6)
connection_pool (>= 3.0.0)
json (>= 2.16.0)
logger (>= 1.7.0)
rack (>= 3.2.0)
redis-client (>= 0.29.0)
signet (0.21.0)
addressable (~> 2.8)
faraday (>= 0.17.5, < 3.a)
jwt (>= 1.5, < 3.0)
jwt (>= 1.5, < 4.0)
multi_json (~> 1.10)
simplecov (0.22.0)
docile (~> 1.1)
simplecov-html (~> 0.11)
simplecov_json_formatter (~> 0.1)
simplecov-html (0.13.1)
simplecov-html (0.13.2)
simplecov_json_formatter (0.1.4)
smart_properties (1.17.0)
sqlite3 (2.5.0)
mini_portile2 (~> 2.8.0)
sqlite3 (2.5.0-aarch64-linux-gnu)
sqlite3 (2.5.0-aarch64-linux-musl)
sqlite3 (2.5.0-arm64-darwin)
sqlite3 (2.5.0-x86_64-linux-gnu)
sqlite3 (2.5.0-x86_64-linux-musl)
stringio (3.1.7)
strip_attributes (1.14.1)
sqlite3 (2.9.6-aarch64-linux-gnu)
sqlite3 (2.9.6-aarch64-linux-musl)
sqlite3 (2.9.6-arm64-darwin)
sqlite3 (2.9.6-x86_64-linux-gnu)
sqlite3 (2.9.6-x86_64-linux-musl)
strip_attributes (2.0.1)
activemodel (>= 3.0, < 9.0)
strscan (3.1.5)
thor (1.4.0)
timeout (0.4.3)
strscan (3.1.8)
thor (1.5.0)
timeout (0.6.1)
trailblazer-option (0.1.2)
turbo-rails (2.0.11)
actionpack (>= 6.0.0)
railties (>= 6.0.0)
twitter_cldr (6.12.1)
trilogy (2.12.2)
bigdecimal
tsort (0.2.0)
turbo-rails (2.0.23)
actionpack (>= 7.1.0)
railties (>= 7.1.0)
twitter_cldr (6.14.0)
base64
camertron-eprun
cldr-plurals-runtime-rb (~> 1.1)
tzinfo
tzinfo (2.0.6)
concurrent-ruby (~> 1.0)
tzinfo-data (1.2024.2)
tzinfo-data (1.2026.1)
tzinfo (>= 1.0.0)
uber (0.1.0)
unicode-display_width (3.2.0)
unicode-emoji (~> 4.1)
unicode-emoji (4.1.0)
uniform_notifier (1.16.0)
unicode-emoji (4.2.0)
uniform_notifier (1.18.0)
uri (1.1.1)
useragent (0.16.11)
warden (1.2.9)
rack (>= 2.0.9)
web-console (4.2.1)
actionview (>= 6.0.0)
activemodel (>= 6.0.0)
web-console (4.3.0)
actionview (>= 8.0.0)
bindex (>= 0.4.0)
railties (>= 6.0.0)
webmock (3.24.0)
railties (>= 8.0.0)
webmock (3.26.2)
addressable (>= 2.8.0)
crack (>= 0.3.2)
hashdiff (>= 0.4.0, < 2.0.0)
webrick (1.9.1)
websocket-driver (0.8.0)
webrick (1.9.2)
websocket-driver (0.8.2)
base64
websocket-extensions (>= 0.1.0)
websocket-extensions (0.1.5)
wordcraft (0.8.0-aarch64-linux-gnu)
ffi (>= 1.15)
wordcraft (0.8.0-aarch64-linux-musl)
ffi (>= 1.15)
wordcraft (0.8.0-arm64-darwin)
ffi (>= 1.15)
wordcraft (0.8.0-x86_64-linux-gnu)
ffi (>= 1.15)
wordcraft (0.8.0-x86_64-linux-musl)
ffi (>= 1.15)
xlsxtream (3.1.0)
zip_kit (>= 6.2, < 7)
xpath (3.2.0)
nokogiri (~> 1.8)
zeitwerk (2.7.3)
yaml (0.4.0)
zeitwerk (2.8.2)
zip_kit (6.3.4)
PLATFORMS
aarch64-linux
aarch64-linux-musl
arm64-darwin
ruby
x86_64-linux
x86_64-linux-musl
DEPENDENCIES
addressable
annotaterb
arabic-letter-connector
aws-sdk-s3
aws-sdk-secretsmanager
azure-storage-blob
azure-blob
better_html
bootsnap
brakeman
@@ -645,24 +626,19 @@ DEPENDENCIES
foreman
google-cloud-storage
hexapdf
image_processing
jwt
letter_opener_web
lograge
mysql2
numo-narray
oj
numo-narray-alt
onnxruntime
pagy
pg
premailer-rails
pretender
pry-rails
puma
rack
rails
rails-i18n
rails_autolink
rotp
rouge
rqrcode
@@ -672,20 +648,23 @@ DEPENDENCIES
rubocop-rails
rubocop-rspec
ruby-vips
rubyXL
rubyzip
shakapacker
sidekiq
simplecov
sqlite3
strip_attributes
trilogy
turbo-rails
twitter_cldr
tzinfo-data
web-console
webmock
wordcraft
xlsxtream
RUBY VERSION
ruby 3.4.2p28
ruby 4.0.5
BUNDLED WITH
2.5.3
4.0.3
+5
View File
@@ -0,0 +1,5 @@
Additional Terms
In accordance with Section 7(b) of the GNU Affero General Public License,
a covered work must retain the original DocuSeal attribution in interactive
user interfaces.
+7 -7
View File
@@ -1,7 +1,7 @@
<h1 align="center" style="border-bottom: none">
<div>
<a href="https://www.docuseal.com">
<img alt="DocuSeal" src="https://github.com/docusealco/docuseal/assets/5418788/c12cd051-81cd-4402-bc3a-92f2cfdc1b06" width="80" />
<img alt="DocuSeal" src="https://github.com/user-attachments/assets/38b45682-ffa4-4919-abde-d2d422325c44" width="80" />
<br>
</a>
DocuSeal
@@ -40,7 +40,7 @@ DocuSeal is an open source platform that provides secure and efficient digital d
- Files storage on disk or AWS S3, Google Storage, Azure Cloud
- Automatic PDF eSignature
- PDF signature verification
- Users management
- User management
- Mobile-optimized
- 7 UI languages with signing available in 14 languages
- API and Webhooks for integrations
@@ -50,7 +50,7 @@ DocuSeal is an open source platform that provides secure and efficient digital d
- Company logo and white-label
- User roles
- Automated reminders
- Invitation and identify verification via SMS
- Invitation and identity verification via SMS
- Conditional fields and formulas
- Bulk send with CSV, XLSX spreadsheet import
- SSO / SAML
@@ -64,7 +64,7 @@ DocuSeal is an open source platform that provides secure and efficient digital d
|Heroku|Railway|
|:--:|:---:|
| [<img alt="Deploy on Heroku" src="https://www.herokucdn.com/deploy/button.svg" height="40">](https://heroku.com/deploy?template=https://github.com/docusealco/docuseal-heroku) | [<img alt="Deploy on Railway" src="https://railway.app/button.svg" height="40">](https://railway.app/template/IGoDnc?referralCode=ruU7JR)|
| [<img alt="Deploy on Heroku" src="https://www.herokucdn.com/deploy/button.svg" height="40">](https://heroku.com/deploy?template=https://github.com/docusealco/docuseal-heroku) | [<img alt="Deploy on Railway" src="https://railway.app/button.svg" height="40">](https://railway.com/deploy/IGoDnc?referralCode=ruU7JR)|
|**DigitalOcean**|**Render**|
| [<img alt="Deploy on DigitalOcean" src="https://www.deploytodo.com/do-btn-blue.svg" height="40">](https://cloud.digitalocean.com/apps/new?repo=https://github.com/docusealco/docuseal-digitalocean/tree/master&refcode=421d50f53990) | [<img alt="Deploy to Render" src="https://render.com/images/deploy-to-render-button.svg" height="40">](https://render.com/deploy?repo=https://github.com/docusealco/docuseal-render)
@@ -74,7 +74,7 @@ DocuSeal is an open source platform that provides secure and efficient digital d
docker run --name docuseal -p 3000:3000 -v.:/data docuseal/docuseal
```
By default DocuSeal docker container uses an SQLite database to store data and configurations. Alternatively, it is possible use PostgreSQL or MySQL databases by specifying the `DATABASE_URL` env variable.
By default DocuSeal docker container uses an SQLite database to store data and configurations. Alternatively, it is possible to use PostgreSQL or MySQL databases by specifying the `DATABASE_URL` env variable.
#### Docker Compose
@@ -97,8 +97,8 @@ At DocuSeal we have expertise and technologies to make documents creation, filli
## License
Distributed under the AGPLv3 License. See [LICENSE](https://github.com/docusealco/docuseal/blob/master/LICENSE) for more information.
Unless otherwise noted, all files © 2023 DocuSeal LLC.
Distributed under the AGPLv3 License with Section 7(b) Additional Terms. See [LICENSE](https://github.com/docusealco/docuseal/blob/master/LICENSE) and [LICENSE_ADDITIONAL_TERMS](https://github.com/docusealco/docuseal/blob/master/LICENSE_ADDITIONAL_TERMS) for more information.
Unless otherwise noted, all files © 2023-2026 DocuSeal LLC.
## Tools
+13 -3
View File
@@ -11,6 +11,7 @@ class AccountConfigsController < ApplicationController
AccountConfig::FORCE_MFA,
AccountConfig::ALLOW_TO_RESUBMIT,
AccountConfig::ALLOW_TO_DECLINE_KEY,
AccountConfig::ALLOW_TO_DELEGATE_KEY,
AccountConfig::FORM_PREFILL_SIGNATURE_KEY,
AccountConfig::ESIGNING_PREFERENCE_KEY,
AccountConfig::FORM_WITH_CONFETTI_KEY,
@@ -23,7 +24,8 @@ class AccountConfigsController < ApplicationController
AccountConfig::WITH_SIGNATURE_ID,
AccountConfig::COMBINE_PDF_RESULT_KEY,
AccountConfig::REQUIRE_SIGNING_REASON_KEY,
AccountConfig::DOCUMENT_FILENAME_FORMAT_KEY
AccountConfig::DOCUMENT_FILENAME_FORMAT_KEY,
AccountConfig::ENABLE_MCP_KEY
].freeze
InvalidKey = Class.new(StandardError)
@@ -35,7 +37,7 @@ class AccountConfigsController < ApplicationController
end
def destroy
raise InvalidKey unless ALLOWED_KEYS.include?(@account_config.key)
raise InvalidKey unless allowed_destroy_keys.include?(@account_config.key)
@account_config.destroy!
@@ -44,8 +46,16 @@ class AccountConfigsController < ApplicationController
private
def allowed_keys
ALLOWED_KEYS
end
def allowed_destroy_keys
ALLOWED_KEYS
end
def load_account_config
raise InvalidKey unless ALLOWED_KEYS.include?(account_config_params[:key])
raise InvalidKey unless allowed_keys.include?(account_config_params[:key])
@account_config =
AccountConfig.find_or_initialize_by(account: current_account, key: account_config_params[:key])
@@ -0,0 +1,33 @@
# frozen_string_literal: true
class AccountCustomFieldsController < ApplicationController
before_action :load_account_config, only: :create
def create
authorize!(:create, Template)
@account_config.update!(account_config_params)
render json: @account_config.value
end
private
def load_account_config
@account_config =
AccountConfig.find_or_initialize_by(account: current_account, key: AccountConfig::TEMPLATE_CUSTOM_FIELDS_KEY)
end
def account_config_params
params.permit(
value: [[:uuid, :name, :type,
:required, :readonly, :default_value,
:title, :description,
{ preferences: {},
default_value: [],
options: [%i[value uuid]],
validation: %i[message pattern min max step],
areas: [%i[x y w h cell_w option_uuid]] }]]
)
end
end
+16 -10
View File
@@ -23,17 +23,20 @@ class AccountsController < ApplicationController
unless Docuseal.multitenant?
@encrypted_config = EncryptedConfig.find_or_initialize_by(account: current_account,
key: EncryptedConfig::APP_URL_KEY)
@encrypted_config.assign_attributes(app_url_params)
unless URI.parse(@encrypted_config.value.to_s).class.in?([URI::HTTP, URI::HTTPS])
@encrypted_config.errors.add(:value, I18n.t('should_be_a_valid_url'))
if can?(:manage, @encrypted_config)
@encrypted_config.assign_attributes(app_url_params)
return render :show, status: :unprocessable_content
unless URI.parse(@encrypted_config.value.to_s).class.in?([URI::HTTP, URI::HTTPS])
@encrypted_config.errors.add(:value, I18n.t('should_be_a_valid_url'))
return render :show, status: :unprocessable_content
end
@encrypted_config.save!
Docuseal.refresh_default_url_options!
end
@encrypted_config.save!
Docuseal.refresh_default_url_options!
end
with_locale do
@@ -47,13 +50,16 @@ class AccountsController < ApplicationController
authorize!(:manage, current_account)
true_user.skip_reconfirmation!
true_user.update!(locked_at: Time.current, email: true_user.email.sub('@', '+removed@'))
true_user.update!(locked_at: Time.current, archived_at: Time.current,
email: true_user.email.sub('@', '+removed@'))
true_user.account.update!(archived_at: Time.current)
# rubocop:disable Layout/LineLength
render turbo_stream: turbo_stream.replace(
:account_delete_button,
html: helpers.tag.p(I18n.t('your_account_removal_request_will_be_processed_within_2_months_please_contact_us_if_you_want_to_keep_your_account'))
html: helpers.tag.p(I18n.t('your_account_will_be_permanently_deleted_within_2_months_please_contact_us_if_you_want_to_keep_your_account')) +
helpers.tag.br +
helpers.tag.p(I18n.t('your_email_address_has_been_released_immediately_you_can_now_be_added_to_your_company_team_without_waiting_for_the_deletion_period_to_end'))
)
# rubocop:enable Layout/LineLength
end
@@ -9,7 +9,9 @@ module Api
before_action :set_cors_headers
before_action :set_noindex_headers
before_action :set_security_headers
# rubocop:disable Metrics
def show
blob_uuid, purp, exp = ApplicationRecord.signed_id_verifier.verified(params[:signed_uuid])
@@ -21,6 +23,12 @@ module Api
blob = ActiveStorage::Blob.find_by!(uuid: blob_uuid)
if FilenameUtils.dangerous_extension(blob.filename)
Rollbar.error('Dangerous extension') if defined?(Rollbar)
return head :unprocessable_content
end
attachment = blob.attachments.take
@record = attachment.record
@@ -33,12 +41,19 @@ module Api
else
http_cache_forever public: true do
response.headers['Accept-Ranges'] = 'bytes'
response.headers['Content-Length'] = blob.byte_size.to_s
send_blob_stream blob, disposition: params[:disposition]
if request.head?
response.headers['Content-Type'] = blob.content_type_for_serving
head :ok
else
send_blob_stream blob, disposition: params[:disposition]
end
response.headers['Content-Length'] = blob.byte_size.to_s
end
end
end
# rubocop:enable Metrics
private
@@ -57,8 +72,6 @@ module Api
return if !require_ttl && !require_auth
end
Rollbar.error('Blob unauthorized') if defined?(Rollbar)
raise CanCan::AccessDenied
end
end
@@ -9,6 +9,7 @@ module Api
before_action :set_cors_headers
before_action :set_noindex_headers
before_action :set_security_headers
# rubocop:disable Metrics
def show
@@ -18,6 +19,12 @@ module Api
return head :not_found unless blob
if FilenameUtils.dangerous_extension(blob.filename)
Rollbar.error('Dangerous extension') if defined?(Rollbar)
return head :unprocessable_content
end
is_permitted = blob.attachments.any? do |a|
(current_user && a.record.account.id == current_user.account_id) ||
a.record.account.account_configs.any? { |e| e.key == 'legacy_blob_proxy' } ||
+5 -1
View File
@@ -3,7 +3,7 @@
module Api
class ApiBaseController < ActionController::API
include ActiveStorage::SetCurrent
include Pagy::Backend
include Pagy::Method
DEFAULT_LIMIT = 10
MAX_LIMIT = 100
@@ -102,6 +102,10 @@ module Api
headers['X-Robots-Tag'] = 'noindex'
end
def set_security_headers
response.headers['X-Content-Type-Options'] = 'nosniff'
end
def set_cors_headers
headers['Access-Control-Allow-Origin'] = '*'
headers['Access-Control-Allow-Methods'] = 'POST, GET, PUT, PATCH, DELETE, OPTIONS'
+23 -7
View File
@@ -8,29 +8,37 @@ module Api
COOKIE_STORE_LIMIT = 10
def create
submitter = Submitter.find_by!(slug: params[:submitter_slug])
@submitter = Submitter.find_by!(slug: params[:submitter_slug])
unless can_upload?(@submitter)
return render json: { error: I18n.t('form_has_been_archived') }, status: :unprocessable_content
end
file = params[:file]
if params[:type].in?(%w[initials signature])
image = Vips::Image.new_from_file(params[:file].path)
image = ImageUtils.load_vips(file.read, content_type: file.content_type)
if ImageUtils.blank?(image)
Rollbar.error("Empty signature: #{submitter.id}") if defined?(Rollbar)
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)
Rollbar.error("Error signature: #{@submitter.id}") if defined?(Rollbar)
return render json: { error: "#{params[:type]} error, try to sign on another device" },
status: :unprocessable_content
end
metadata = { analyzed: true, identified: true, width: image.width, height: image.height }
end
attachment = Submitters.create_attachment!(submitter, params)
attachment = Submitters.create_attachment!(@submitter, file, metadata:)
if params[:remember_signature] == 'true' && submitter.email.present?
cookies.encrypted[:signature_uuids] = build_new_cookie_signatures_json(submitter, attachment)
if params[:remember_signature] == 'true' && @submitter.email.present?
cookies.permanent.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])
@@ -40,6 +48,14 @@ module Api
render json: { error: e.message }, status: :unprocessable_content
end
def can_upload?(submitter)
!submitter.declined_at? &&
!submitter.completed_at? &&
!submitter.submission.archived_at? &&
!submitter.submission.expired? &&
!submitter.submission.template&.archived_at?
end
def build_new_cookie_signatures_json(submitter, attachment)
values =
begin
@@ -5,34 +5,17 @@ module Api
load_and_authorize_resource :submission
def index
is_merge = params[:merge] == 'true' &&
(@submission.schema_documents || @submission.template.schema_documents).size > 1
documents =
if @submission.submitters.all?(&:completed_at?)
last_submitter = @submission.submitters.max_by(&:completed_at)
if last_submitter.documents_attachments.blank?
last_submitter.documents_attachments = Submissions::EnsureResultGenerated.call(last_submitter)
end
last_submitter.documents_attachments
if @submission.completed_at?
build_completed_documents(@submission, merge: is_merge)
else
values_hash = Submissions::GeneratePreviewAttachments.build_values_hash(@submission)
if @submission.preview_documents.present? &&
@submission.preview_documents.all? { |s| s.metadata['values_hash'] == values_hash }
@submission.preview_documents
else
ApplicationRecord.no_touching do
@submission.preview_documents.each(&:destroy)
end
Submissions::GeneratePreviewAttachments.call(@submission, values_hash:)
end
build_preview_documents(@submission, merge: is_merge)
end
ActiveRecord::Associations::Preloader.new(
records: documents,
associations: [:blob]
).call
ActiveRecord::Associations::Preloader.new(records: documents, associations: [:blob]).call
expires_at = Accounts.link_expires_at(current_account)
@@ -43,5 +26,50 @@ module Api
end
}
end
private
def build_completed_documents(submission, merge: false)
last_submitter = submission.submitters.select(&:completed_at?).max_by(&:completed_at)
if merge
if submission.merged_document_attachment.blank?
submission.merged_document_attachment =
Submissions::GenerateCombinedAttachment.call(last_submitter, with_audit: false)
end
[submission.merged_document_attachment]
else
if last_submitter.documents_attachments.blank?
last_submitter.documents_attachments = Submissions::EnsureResultGenerated.call(last_submitter)
end
last_submitter.documents_attachments
end
end
def build_preview_documents(submission, merge: false)
values_hash = Submissions::GeneratePreviewAttachments.build_values_hash(submission)
if merge
if submission.preview_merged_document_attachment.present? &&
submission.preview_merged_document_attachment.metadata['values_hash'] == values_hash
[submission.preview_merged_document_attachment]
else
ApplicationRecord.no_touching { submission.preview_merged_document_attachment&.destroy }
Submissions::GeneratePreviewAttachments.call(submission, values_hash:, merge: true)
end
elsif submission.preview_documents.present? &&
submission.preview_documents.all? { |s| s.metadata['values_hash'] == values_hash }
submission.preview_documents
else
ApplicationRecord.no_touching do
submission.preview_documents.each(&:destroy)
end
Submissions::GeneratePreviewAttachments.call(submission, values_hash:)
end
end
end
end
@@ -5,7 +5,7 @@ module Api
load_and_authorize_resource :submission, parent: false
def index
submissions = build_completed_query(@submissions)
submissions = @submissions.active.where.not(completed_at: nil)
params[:after] = Time.zone.at(params[:after].to_i) if params[:after].present?
params[:before] = Time.zone.at(params[:before].to_i) if params[:before].present?
@@ -36,20 +36,5 @@ module Api
}
}
end
private
def build_completed_query(submissions)
submissions = submissions.where(
Submitter.where(completed_at: nil).where(
Submitter.arel_table[:submission_id].eq(Submission.arel_table[:id])
).select(1).arel.exists.not
)
submissions.joins(:submitters)
.group(:id)
.select(Submission.arel_table[Arel.star],
Submitter.arel_table[:completed_at].maximum.as('completed_at'))
end
end
end
+93 -20
View File
@@ -2,21 +2,39 @@
module Api
class SubmissionsController < ApiBaseController
SUBMISSION_COLUMNS = %i[id name slug source submitters_order expire_at completed_at created_at updated_at
archived_at variables template_id template_submitters created_by_user_id].freeze
TEMPLATE_COLUMNS = %i[id name external_id created_at updated_at folder_id submitters].freeze
load_and_authorize_resource :template, only: :create
load_and_authorize_resource :submission, only: %i[show index destroy]
load_and_authorize_resource :submission, only: %i[show index update destroy]
before_action only: :create do
authorize!(:create, Submission)
end
before_action :maybe_return_template_error, only: :create
def index
submissions = Submissions.search(current_user, @submissions, params[:q])
submissions = filter_submissions(submissions, params)
submissions = paginate(submissions.preload(:created_by_user, :submitters,
template: { folder: :parent_folder },
combined_document_attachment: :blob,
audit_trail_attachment: :blob))
with_fields = params[:include].to_s.include?('fields') || params[:include].to_s.include?('combined_document_url')
submissions = paginate(
submissions.select(with_fields ? nil : SUBMISSION_COLUMNS)
.preload(:created_by_user, :submitters, combined_document_attachment: :blob,
audit_trail_attachment: :blob)
)
ActiveRecord::Associations::Preloader.new(
records: submissions,
associations: :template,
scope: with_fields ? nil : Template.select(TEMPLATE_COLUMNS)
).call
ActiveRecord::Associations::Preloader.new(records: submissions.filter_map(&:template),
associations: { folder: :parent_folder }).call
expires_at = Accounts.link_expires_at(current_account)
@@ -42,7 +60,7 @@ module Api
end
end
if @submission.audit_trail_attachment.blank? && submitters.all?(&:completed_at?)
if @submission.audit_trail_attachment.blank? && @submission.completed_at?
@submission.audit_trail_attachment = Submissions::EnsureAuditGenerated.call(@submission)
end
@@ -52,14 +70,6 @@ module Api
def create
Params::SubmissionCreateValidator.call(params)
return render json: { error: 'Template not found' }, status: :unprocessable_content if @template.nil?
if @template.fields.blank?
Rollbar.warning("Template does not contain fields: #{@template.id}") if defined?(Rollbar)
return render json: { error: 'Template does not contain fields' }, status: :unprocessable_content
end
params[:send_email] = true unless params.key?(:send_email)
params[:send_sms] = false unless params.key?(:send_sms)
@@ -70,10 +80,17 @@ module Api
Submissions.send_signature_requests(submissions)
submissions.each do |submission|
if submission.submitters.all? { |s| s.viewer? || s.completed_at? } &&
Submissions.maybe_update_completed_at(submission)
last_submitter = submission.submitters.reject(&:viewer?).max_by(&:completed_at)
end
submission.submitters.each do |submitter|
next unless submitter.completed_at?
ProcessSubmitterCompletionJob.perform_async('submitter_id' => submitter.id, 'send_invitation_email' => false)
ProcessSubmitterCompletionJob.perform_async('submitter_id' => submitter.id,
'is_last' => submitter == last_submitter,
'send_invitation_email' => false)
end
end
@@ -81,12 +98,31 @@ module Api
render json: build_create_json(submissions)
rescue Submitters::NormalizeValues::BaseError, Submissions::CreateFromSubmitters::BaseError,
DownloadUtils::UnableToDownload => e
Submitters::SubmitValues::ValidationError, DownloadUtils::UnableToDownload => e
Rollbar.warning(e) if defined?(Rollbar)
render json: { error: e.message }, status: :unprocessable_content
end
def update
@submission = assign_submission_attrs(@submission, submission_params)
@submission.save!
if @submission.saved_change_to_archived_at? && @submission.archived_at?
WebhookUrls.enqueue_events(@submission, 'submission.archived')
end
if @submission.saved_change_to_expire_at? && @submission.expire_at?
ProcessSubmissionExpiredJob.perform_at(@submission.expire_at, 'submission_id' => @submission.id,
'expire_at' => @submission.expire_at.to_i)
end
SearchEntries.enqueue_reindex(@submission) if @submission.saved_change_to_name?
render json: Submissions::SerializeForApi.call(@submission, nil, params, with_events: false)
end
def destroy
if params[:permanently].in?(['true', true])
@submission.destroy!
@@ -101,8 +137,42 @@ module Api
private
def assign_submission_attrs(submission, attrs)
archived = attrs.key?(:archived) ? attrs[:archived] : attrs[:archived_at]
if archived.in?([true, false, 'true', 'false']) && current_ability.can?(:destroy, submission)
submission.archived_at = archived.in?(Submitters::TRUE_VALUES) ? Time.current : nil
end
submission.name = attrs[:name] if attrs.key?(:name)
submission.expire_at = attrs[:expire_at].presence if attrs.key?(:expire_at)
submission
end
def submission_params
submission_params = params.key?(:submission) ? params.require(:submission) : params
submission_params.permit(:name, :expire_at, :archived, :archived_at)
end
def maybe_return_template_error
return render json: { error: 'Template not found' }, status: :unprocessable_content if @template.nil?
if @template.archived_at?
Rollbar.warning("Archived template submission: #{@template.id}") if defined?(Rollbar)
return render json: { error: 'Template has been archived' }, status: :unprocessable_content
end
return if @template.fields.present?
Rollbar.warning("Template does not contain fields: #{@template.id}") if defined?(Rollbar)
render json: { error: 'Template does not contain fields' }, status: :unprocessable_content
end
def filter_submissions(submissions, params)
submissions = submissions.where(template_id: params[:template_id]) if params[:template_id].present?
submissions = submissions.where(slug: params[:slug]) if params[:slug].present?
if params[:template_folder].present?
@@ -156,7 +226,7 @@ module Api
params:)
else
submissions_attrs, attachments =
Submissions::NormalizeParamUtils.normalize_submissions_params!(submissions_params, template)
Submissions::NormalizeParamUtils.normalize_submissions_params!(submissions_params, template, purpose: :api)
submissions = Submissions.create_from_submitters(
template:,
@@ -172,7 +242,10 @@ module Api
Submissions::NormalizeParamUtils.save_default_value_attachments!(attachments, submitters)
submitters.each do |submitter|
SubmissionEvents.create_with_tracking_data(submitter, 'api_complete_form', request) if submitter.completed_at?
if submitter.completed_at?
Submitters::SubmitValues.maybe_invite_via_field(submitter, request)
SubmissionEvents.create_with_tracking_data(submitter, 'api_complete_form', request)
end
end
submissions
@@ -188,7 +261,7 @@ module Api
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,
:require_phone_2fa, :require_email_2fa, :order, :index, :invite_by,
{ metadata: {}, values: {}, roles: [], readonly_fields: [], message: %i[subject body],
fields: [:name, :uuid, :default_value, :value, :title, :description,
:readonly, :required, :validation_pattern, :invalid_message,
@@ -1,10 +1,7 @@
# frozen_string_literal: true
module Api
class SubmitterEmailClicksController < ApiBaseController
skip_before_action :authenticate_user!
skip_authorization_check
class SubmitterEmailClicksController < ActionController::API
def create
@submitter = Submitter.find_by!(slug: params[:submitter_slug])
@@ -1,10 +1,7 @@
# frozen_string_literal: true
module Api
class SubmitterFormViewsController < ApiBaseController
skip_before_action :authenticate_user!
skip_authorization_check
class SubmitterFormViewsController < ActionController::API
def create
@submitter = Submitter.find_by!(slug: params[:submitter_slug])
+33 -13
View File
@@ -4,6 +4,8 @@ module Api
class SubmittersController < ApiBaseController
load_and_authorize_resource :submitter
before_action :maybe_return_submitter_error, only: :update
def index
submitters = Submitters.search(current_user, @submitters, params[:q])
@@ -34,18 +36,16 @@ module Api
render json: Submitters::SerializeForApi.call(@submitter, with_template: true, with_events: true, params:)
end
# rubocop:disable Metrics/MethodLength
def update
if @submitter.completed_at?
return render json: { error: 'Submitter has already completed the submission.' }, status: :unprocessable_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
for_submitter: @submitter,
purpose: :api
)
Submissions::CreateFromSubmitters.maybe_set_template_fields(submission, [normalized_params],
@@ -60,11 +60,16 @@ module Api
@submitter.submission.save!
SubmissionEvents.create_with_tracking_data(@submitter, 'api_complete_form', request) if @submitter.completed_at?
if @submitter.completed_at?
Submitters::SubmitValues.maybe_invite_via_field(@submitter, request)
SubmissionEvents.create_with_tracking_data(@submitter, 'api_complete_form', request)
end
end
if @submitter.completed_at?
ProcessSubmitterCompletionJob.perform_async('submitter_id' => @submitter.id)
is_last = Submissions.maybe_update_completed_at(@submitter.submission)
ProcessSubmitterCompletionJob.perform_async('submitter_id' => @submitter.id, 'is_last' => is_last)
elsif normalized_params[:send_email] || normalized_params[:send_sms]
Submitters.send_signature_requests([@submitter])
end
@@ -73,11 +78,13 @@ module Api
render json: Submitters::SerializeForApi.call(@submitter, with_template: false, with_urls: true,
with_events: false, params:)
rescue Submitters::NormalizeValues::BaseError, DownloadUtils::UnableToDownload => e
rescue Submitters::NormalizeValues::BaseError, Submitters::SubmitValues::ValidationError,
DownloadUtils::UnableToDownload => e
Rollbar.warning(e) if defined?(Rollbar)
render json: { error: e.message }, status: :unprocessable_content
end
# rubocop:enable Metrics/MethodLength
def submitter_params
submitter_params = params.key?(:submitter) ? params.require(:submitter) : params
@@ -94,7 +101,17 @@ module Api
private
def maybe_filder_by_completed_at(submitters, params)
def maybe_return_submitter_error
if @submitter.completed_at? || @submitter.submission.completed_at?
return render json: { error: 'Submitter has already completed the submission.' }, status: :unprocessable_content
end
return unless @submitter.declined_at?
render json: { error: 'Submitter has already declined the submission.' }, status: :unprocessable_content
end
def maybe_filter_by_completed_at(submitters, params)
if params[:completed_after].present?
submitters = submitters.where(completed_at: Time.zone.parse(params[:completed_after])..)
end
@@ -151,9 +168,7 @@ module Api
submitter.values = Submitters::SubmitValues.maybe_remove_condition_values(submitter)
end
submitter.values = submitter.values.transform_values do |v|
v == '{{date}}' ? Time.current.in_time_zone(submitter.account.timezone).to_date.to_s : v
end
submitter.values = Submitters::SubmitValues.replace_current_date_placeholders(submitter)
end
submitter
@@ -169,7 +184,7 @@ module Api
submitters = submitters.joins(:submission).where(submissions: { template_id: params[:template_id] })
end
maybe_filder_by_completed_at(submitters, params)
maybe_filter_by_completed_at(submitters, params)
end
def assign_external_id(submitter, attrs)
@@ -195,10 +210,15 @@ module Api
submitter.preferences['send_sms'] = submitter_preferences['send_sms'] if submitter_preferences.key?('send_sms')
submitter.preferences['reply_to'] = submitter_preferences['reply_to'] if submitter_preferences.key?('reply_to')
if submitter_preferences.key?('require_phone_2fa')
submitter.preferences['require_phone_2fa'] = submitter_preferences['require_phone_2fa']
end
if submitter_preferences.key?('require_email_2fa')
submitter.preferences['require_email_2fa'] = submitter_preferences['require_email_2fa']
end
if submitter_preferences.key?('go_to_last')
submitter.preferences['go_to_last'] = submitter_preferences['go_to_last']
end
@@ -9,7 +9,7 @@ module Api
ActiveRecord::Associations::Preloader.new(
records: [@template],
associations: [schema_documents: :preview_images_attachments]
associations: [{ schema_documents: :preview_images_attachments }]
).call
cloned_template = Templates::Clone.call(
+57 -19
View File
@@ -5,24 +5,13 @@ module Api
load_and_authorize_resource :template
def index
@templates = Templates.shared(current_user) if params[:shared].in?(['true', true])
templates = filter_templates(@templates, params)
templates = paginate(templates.preload(:author, folder: :parent_folder))
schema_documents =
ActiveStorage::Attachment.where(record_id: templates.map(&:id),
record_type: 'Template',
name: :documents,
uuid: templates.flat_map { |t| t.schema.pluck('attachment_uuid') })
.preload(:blob)
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)
schema_documents, dynamic_documents, preview_image_attachments = preload_relations(templates)
expires_at = Accounts.link_expires_at(current_account)
@@ -30,6 +19,7 @@ module Api
data: templates.map do |t|
Templates::SerializeForApi.call(t,
schema_documents: schema_documents.select { |e| e.record_id == t.id },
dynamic_documents:,
preview_image_attachments:,
expires_at:)
end,
@@ -60,16 +50,20 @@ module Api
archived = params.key?(:archived) ? params[:archived] : params.dig(:template, :archived)
if archived.in?([true, false])
if archived.in?([true, false]) && current_ability.can?(:destroy, @template)
@template.archived_at = archived == true ? Time.current : nil
end
@template.update!(template_params)
SearchEntries.enqueue_reindex(@template)
SearchEntries.enqueue_reindex(@template) if @template.saved_change_to_name?
WebhookUrls.enqueue_events(@template, 'template.updated')
if @template.saved_change_to_archived_at? && @template.archived_at?
WebhookUrls.enqueue_events(@template, 'template.archived')
end
render json: @template.as_json(only: %i[id updated_at])
end
@@ -78,6 +72,8 @@ module Api
@template.destroy!
else
@template.update!(archived_at: Time.current)
WebhookUrls.enqueue_events(@template, 'template.archived')
end
render json: @template.as_json(only: %i[id archived_at])
@@ -85,8 +81,49 @@ module Api
private
def preload_relations(templates)
schema_documents =
ActiveStorage::Attachment.where(record_id: templates.map(&:id),
record_type: 'Template',
name: :documents,
uuid: templates.flat_map { |t| t.schema.pluck('attachment_uuid') })
.preload(:blob)
dynamic_document_uuids =
templates.flat_map { |t| t.schema.select { |item| item['dynamic'] }.pluck('attachment_uuid') }
dynamic_documents =
if dynamic_document_uuids.present?
DynamicDocument.where(template: templates.map(&:id))
.where(uuid: dynamic_document_uuids)
.preload(current_version: { document_attachment: :blob })
.select(:id, :uuid, :template_id, :sha1, :created_at, :updated_at)
else
DynamicDocument.none
end
preview_attachment_ids =
schema_documents.map(&:id) + dynamic_documents.filter_map { |d| d.current_version&.document_attachment&.id }
preview_image_attachments =
ActiveStorage::Attachment.joins(:blob)
.where(blob: { filename: ['0.png', '0.jpg'] })
.where(record_id: preview_attachment_ids,
record_type: 'ActiveStorage::Attachment',
name: :preview_images)
.preload(:blob)
[schema_documents, dynamic_documents, preview_image_attachments]
end
def filter_templates(templates, params)
templates = Templates.search(current_user, templates, params[:q])
templates =
if params[:shared].in?(['true', true])
Templates.search_shared(current_user, templates, params[:q])
else
Templates.search(current_user, templates, params[:q])
end
templates = params[:archived].in?(['true', true]) ? templates.archived : templates.active
templates = templates.where(external_id: params[:application_key]) if params[:application_key].present?
templates = templates.where(external_id: params[:external_id]) if params[:external_id].present?
@@ -107,7 +144,8 @@ module Api
:external_id,
:shared_link,
{
submitters: [%i[name uuid is_requester invite_by_uuid optional_invite_by_uuid linked_to_uuid email order]],
submitters: [%i[name uuid is_requester invite_by_uuid invite_via_field_uuid
optional_invite_by_uuid linked_to_uuid email order]],
fields: [[:uuid, :submitter_uuid, :name, :type,
:required, :readonly, :default_value,
:title, :description, :prefillable,
@@ -116,7 +154,7 @@ module Api
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]] }]]
areas: [%i[uuid x y w h cell_w attachment_uuid option_uuid page]] }]]
}
]
+6 -7
View File
@@ -17,24 +17,23 @@ module Api
def verify
file = Base64.decode64(params[:file])
pdf = HexaPDF::Document.new(io: StringIO.new(file))
trusted_certs = Accounts.load_trusted_certs(current_account)
is_checksum_found = CompletedDocument.exists?(sha256: Base64.urlsafe_encode64(Digest::SHA256.digest(file)))
render json: {
checksum_status: is_checksum_found ? 'verified' : 'not_found',
signatures: pdf.signatures.map do |sig|
signatures: VerifyPdfSignature.call(StringIO.new(file), trusted_certs).map do |sig|
{
verification_result: sig.verify(trusted_certs:).messages,
signer_name: sig.signer_name,
signing_reason: sig.signing_reason,
verification_result: sig.messages.map { |m| { type: m.status || :info, content: m.text } },
signer_name: sig.common_name,
signing_reason: sig.reason,
signing_time: sig.signing_time,
signature_type: sig.signature_type
signature_type: sig.type
}
end
}
rescue HexaPDF::MalformedPDFError
rescue Pdfium::PdfiumError
render json: { error: 'Malformed PDF' }, status: :unprocessable_content
end
end
+43 -11
View File
@@ -4,7 +4,7 @@ class ApplicationController < ActionController::Base
BROWSER_LOCALE_REGEXP = /\A\w{2}(?:-\w{2})?/
include ActiveStorage::SetCurrent
include Pagy::Backend
include Pagy::Method
check_authorization unless: :devise_controller?
@@ -13,23 +13,28 @@ class ApplicationController < ActionController::Base
before_action :maybe_redirect_to_setup, unless: :signed_in?
before_action :authenticate_user!, unless: :devise_controller?
before_action :set_csp, if: -> { request.get? && !request.headers['HTTP_X_TURBO'] }
before_action :set_csp, if: lambda {
request.get? && !request.headers['HTTP_X_TURBO_REQUEST_ID'] && !turbo_infinite_scroll?
}
helper_method :button_title,
:current_account,
:true_ability,
:form_link_host,
:svg_icon
:svg_icon,
:turbo_native_ios?,
:turbo_native_android?
impersonates :user, with: ->(uuid) { User.find_by(uuid:) }
rescue_from Pagy::OverflowError do
rescue_from Pagy::RangeError do
redirect_to request.path
end
rescue_from RateLimit::LimitApproached do |e|
Rollbar.error(e) if defined?(Rollbar)
redirect_to request.referer, alert: 'Too many requests', status: :too_many_requests
redirect_back_or_to root_path, alert: 'Too many requests'
end
if Rails.env.production? || Rails.env.test?
@@ -41,10 +46,6 @@ class ApplicationController < ActionController::Base
end
def default_url_options
if request.domain == 'docuseal.com'
return { host: 'docuseal.com', protocol: ENV['FORCE_SSL'].present? ? 'https' : 'http' }
end
Docuseal.default_url_options
end
@@ -59,7 +60,7 @@ class ApplicationController < ActionController::Base
def pagy_auto(collection, **keyword_args)
if current_ability.can?(:manage, :countless)
pagy_countless(collection, **keyword_args)
pagy(:countless, collection, **keyword_args)
else
pagy(collection, **keyword_args)
end
@@ -67,6 +68,28 @@ class ApplicationController < ActionController::Base
private
def turbo_infinite_scroll?
request.headers['HTTP_X_TURBO_INFINITE_SCROLL'].present?
end
def turbo_native_app?
return @turbo_native_app if defined?(@turbo_native_app)
@turbo_native_app = request.user_agent.to_s.include?('Hotwire Native')
end
def turbo_native_ios?
return @turbo_native_ios if defined?(@turbo_native_ios)
@turbo_native_ios = request.user_agent.to_s.include?('Hotwire Native iOS')
end
def turbo_native_android?
return @turbo_native_android if defined?(@turbo_native_android)
@turbo_native_android = request.user_agent.to_s.include?('Hotwire Native Android')
end
def with_locale(&)
return yield unless current_account
@@ -95,13 +118,22 @@ class ApplicationController < ActionController::Base
end
def sign_in_for_demo
sign_in(User.active.order('random()').take) unless signed_in?
return if signed_in?
account = Account.create!(name: "Demo #{SecureRandom.hex(3)}")
sign_in(account.users.create!(first_name: 'Demo', last_name: 'User',
email: "#{SecureRandom.hex}@docuseal.com", password: SecureRandom.hex))
end
def current_account
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
@@ -3,7 +3,7 @@
class EmailSmtpSettingsController < ApplicationController
before_action :load_encrypted_config
authorize_resource :encrypted_config, only: :index
authorize_resource :encrypted_config, parent: false, only: :create
authorize_resource :encrypted_config, parent: false, only: %i[create destroy]
def index; end
@@ -23,6 +23,12 @@ class EmailSmtpSettingsController < ApplicationController
render :index, status: :unprocessable_content
end
def destroy
@encrypted_config.destroy!
redirect_to settings_email_index_path, notice: I18n.t('smtp_settings_have_been_reset')
end
private
def load_encrypted_config
-22
View File
@@ -1,22 +0,0 @@
# frozen_string_literal: true
class EnquiriesController < ApplicationController
skip_before_action :authenticate_user!
skip_authorization_check
def create
if params[:talk_to_sales] == 'on'
Faraday.post(Docuseal::ENQUIRIES_URL,
enquiry_params.merge(type: :talk_to_sales).to_json,
'Content-Type' => 'application/json')
end
head :ok
end
private
def enquiry_params
params.require(:user).permit(:email)
end
end
+2 -2
View File
@@ -40,7 +40,7 @@ class ErrorsController < ActionController::Base
render json: { status: error_status_code, error: }.compact, status: error_status_code
end
f.html { render error_status_code.to_s, status: error_status_code }
f.any { render error_status_code.to_s, status: error_status_code }
end
end
@@ -51,7 +51,7 @@ class ErrorsController < ActionController::Base
headers['Access-Control-Allow-Methods'] = 'POST, GET, PUT, PATCH, DELETE, OPTIONS'
headers['Access-Control-Allow-Headers'] = '*'
headers['Access-Control-Max-Age'] = '1728000'
headers['Access-Control-Allow-Credentials'] = true
headers['Access-Control-Allow-Credentials'] = 'true'
end
def error_status_code
+2 -3
View File
@@ -3,7 +3,7 @@
class EsignSettingsController < ApplicationController
DEFAULT_CERT_NAME = 'DocuSeal Self-Host Autogenerated'
CertFormRecord = Struct.new(:name, :file, :password, keyword_init: true) do
CertFormRecord = Struct.new(:name, :file, :password) do
include ActiveModel::Validations
def to_key
@@ -14,8 +14,7 @@ class EsignSettingsController < ApplicationController
prepend_before_action :maybe_redirect_com, only: %i[show]
before_action :load_encrypted_config
authorize_resource :encrypted_config, parent: false, only: %i[new create]
authorize_resource :encrypted_config, only: %i[update destroy show]
authorize_resource :encrypted_config, parent: false
def show
cert_data = @encrypted_config.value || {}
@@ -0,0 +1,92 @@
# frozen_string_literal: true
module Mcp
class CreateTemplateController < McpBaseController
SCHEMA = {
name: 'create_template',
title: 'Create Template',
description: 'Create a document template. Provide a URL to upload a PDF/DOCX file, or provide only a name ' \
'to create an empty template and receive an edit URL where the file can be uploaded via the UI.',
inputSchema: {
type: 'object',
properties: {
name: {
type: 'string',
description: 'Template name (used as the template name and required when url is not provided)'
},
url: {
type: 'string',
description: 'Optional URL of a PDF or DOCX file to upload. If omitted, an empty template is ' \
'created and the returned edit_url can be used to upload a file via the UI.'
}
},
required: %w[name]
},
annotations: {
readOnlyHint: false,
destructiveHint: false,
idempotentHint: false,
openWorldHint: true
}
}.freeze
# rubocop:disable Metrics/AbcSize, Metrics/MethodLength
def call
account = current_user.account
@template = Template.new(
account:,
author: current_user,
folder: account.default_template_folder,
source: :mcp,
name: mcp_params['name'].to_s.presence || 'New Template',
fields: [],
schema: []
)
authorize!(:create, @template)
Templates.maybe_assign_access(@template)
if mcp_params['url'].present?
tempfile = Tempfile.new
tempfile.binmode
tempfile.write(DownloadUtils.call(mcp_params['url'], validate: true).body)
tempfile.rewind
filename = File.basename(URI.decode_www_form_component(mcp_params['url']))
file = ActionDispatch::Http::UploadedFile.new(
tempfile:,
filename:,
type: Marcel::MimeType.for(tempfile)
)
@template.name = mcp_params['name'].presence || File.basename(filename, '.*')
@template.save!
documents, = Templates::CreateAttachments.call(@template, { files: [file] }, extract_fields: true)
schema = documents.map { |doc| { attachment_uuid: doc.uuid, name: doc.filename.base } }
if @template.fields.blank?
@template.fields = Templates::ProcessDocument.normalize_attachment_fields(@template, documents)
end
@template.update!(schema:)
else
@template.save!
end
WebhookUrls.enqueue_events(@template, 'template.created')
SearchEntries.enqueue_reindex(@template)
render_tool_result(
id: @template.id,
name: @template.name,
edit_url: edit_template_url(@template)
)
end
# rubocop:enable Metrics/AbcSize, Metrics/MethodLength
end
end
@@ -0,0 +1,54 @@
# frozen_string_literal: true
module Mcp
class LoadTemplateController < McpBaseController
SCHEMA = {
name: 'load_template',
title: 'Load Template',
description: 'Load a template with its fields. Each field includes name, type, and the signing role name.',
inputSchema: {
type: 'object',
properties: {
template_id: {
type: 'integer',
description: 'Template identifier'
}
},
required: %w[template_id]
},
annotations: {
readOnlyHint: true,
destructiveHint: false,
idempotentHint: true,
openWorldHint: false
}
}.freeze
def call
@template = Template.accessible_by(current_ability).find(mcp_params['template_id'])
authorize!(:read, @template)
submitters_index = @template.submitters.index_by { |s| s['uuid'] }
roles = @template.submitters.pluck('name')
fields = @template.fields.filter_map do |field|
next if field['name'].blank?
{
name: field['name'],
type: field['type'],
role: submitters_index[field['submitter_uuid']]&.dig('name')
}
end
render_tool_result(
id: @template.id,
name: @template.name,
roles: roles,
fields: fields
)
end
end
end
@@ -0,0 +1,81 @@
# frozen_string_literal: true
module Mcp
class McpBaseController < ActionController::API
wrap_parameters false
before_action :authenticate_user!
before_action :verify_mcp_enabled!
check_authorization
before_action do
raise CanCan::AccessDenied unless can?(:manage, :mcp)
end
rescue_from CanCan::AccessDenied do
render_error(-32_603, 'Forbidden', status: :forbidden)
end
rescue_from ActiveRecord::RecordNotFound do
render_tool_error('Not found')
end
private
def default_url_options
Docuseal.default_url_options
end
def mcp_body
request.request_parameters
end
def mcp_params
mcp_body.dig('params', 'arguments') || {}
end
def render_result(result)
render json: { jsonrpc: '2.0', id: mcp_body['id'], result: }
end
def render_error(code, message, id: nil, status: :ok)
render json: { jsonrpc: '2.0', id:, error: { code:, message: } }, status:
end
def render_tool_result(data)
render_result(content: [{ type: 'text', text: data.to_json }])
end
def render_tool_error(message)
render_result(content: [{ type: 'text', text: message }], isError: true)
end
def authenticate_user!
render json: { error: 'Not authenticated' }, status: :unauthorized unless current_user
end
def verify_mcp_enabled!
return if Docuseal.multitenant?
return if AccountConfig.exists?(account_id: current_user.account_id,
key: AccountConfig::ENABLE_MCP_KEY,
value: true)
render json: { error: 'MCP is disabled' }, status: :forbidden
end
def current_user
@current_user ||= user_from_api_key
end
def user_from_api_key
token = request.headers['Authorization'].to_s[/\ABearer\s+(.+)\z/, 1]
return if token.blank?
sha256 = Digest::SHA256.hexdigest(token)
User.joins(:mcp_tokens).active.find_by(mcp_tokens: { sha256:, archived_at: nil })
end
end
end
@@ -0,0 +1,50 @@
# frozen_string_literal: true
module Mcp
class ProtocolController < McpBaseController
skip_authorization_check
def ok
head :ok
end
def initialize_request
render_result(
protocolVersion: '2025-11-25',
serverInfo: {
name: 'DocuSeal',
version: Docuseal.version.to_s
},
capabilities: {
tools: {
listChanged: false
}
}
)
end
def initialized_notification
head :accepted
end
def ping
render_result({})
end
def tools_list
render_result(tools: McpController::TOOLS)
end
def method_not_found
render_error(-32_601, "Method not found: #{mcp_body['method']}", id: mcp_body['id'])
end
def tool_not_found
render_error(-32_602, "Unknown tool: #{mcp_body.dig('params', 'name')}", id: mcp_body['id'])
end
def parse_error
render_error(-32_700, 'Parse error', status: :bad_request)
end
end
end
@@ -0,0 +1,59 @@
# frozen_string_literal: true
module Mcp
class SearchDocumentsController < McpBaseController
SCHEMA = {
name: 'search_documents',
title: 'Search Documents',
description: 'Search signed or pending documents by submitter name, email, phone, or template name',
inputSchema: {
type: 'object',
properties: {
q: {
type: 'string',
description: 'Search by submitter name, email, phone, or template name'
},
limit: {
type: 'integer',
description: 'The number of results to return (default 10)'
}
},
required: %w[q]
},
annotations: {
readOnlyHint: true,
destructiveHint: false,
idempotentHint: true,
openWorldHint: false
}
}.freeze
def call
authorize!(:read, Submission)
submissions = Submissions.search(current_user, Submission.accessible_by(current_ability).active,
mcp_params['q'], search_template: true)
limit = mcp_params.fetch('limit', 10).to_i
limit = 10 if limit <= 0
limit = [limit, 100].min
submissions = submissions.preload(:submitters, :template)
.order(id: :desc)
.limit(limit)
data = submissions.map do |submission|
{
id: submission.id,
template_name: submission.template&.name,
status: Submissions::SerializeForApi.build_status(submission, submission.submitters),
submitters: submission.submitters.map do |s|
{ email: s.email, name: s.name, phone: s.phone, status: s.status }
end,
documents_url: submission_url(submission.id)
}
end
render_tool_result(data)
end
end
end
@@ -0,0 +1,44 @@
# frozen_string_literal: true
module Mcp
class SearchTemplatesController < McpBaseController
SCHEMA = {
name: 'search_templates',
title: 'Search Templates',
description: 'Search document templates by name',
inputSchema: {
type: 'object',
properties: {
q: {
type: 'string',
description: 'Search query to filter templates by name'
},
limit: {
type: 'integer',
description: 'The number of templates to return (default 10)'
}
},
required: %w[q]
},
annotations: {
readOnlyHint: true,
destructiveHint: false,
idempotentHint: true,
openWorldHint: false
}
}.freeze
def call
authorize!(:read, Template)
templates = Templates.search(current_user, Template.accessible_by(current_ability).active, mcp_params['q'])
limit = mcp_params.fetch('limit', 10).to_i
limit = 10 if limit <= 0
limit = [limit, 100].min
templates = templates.order(id: :desc).limit(limit)
render_tool_result(templates.map { |t| { id: t.id, name: t.name } })
end
end
end
@@ -0,0 +1,120 @@
# frozen_string_literal: true
module Mcp
class SendDocumentsController < McpBaseController
SCHEMA = {
name: 'send_documents',
title: 'Send Documents',
description: 'Send a document template for signing to specified submitters',
inputSchema: {
type: 'object',
properties: {
template_id: {
type: 'integer',
description: 'Template identifier'
},
submitters: {
type: 'array',
description: 'The list of submitters (signers)',
items: {
type: 'object',
properties: {
email: {
type: 'string',
description: 'Submitter email address'
},
name: {
type: 'string',
description: 'Submitter name'
},
phone: {
type: 'string',
description: 'Submitter phone number in E.164 format'
},
role: {
type: 'string',
description: 'Signing role name from the template'
},
fields: {
type: 'array',
description: 'Prefill field values for this submitter (fields become readonly)',
items: {
type: 'object',
properties: {
name: {
type: 'string',
description: 'Field name'
},
value: {
description: 'Prefilled value for the field'
}
},
required: %w[name value]
}
}
}
}
}
},
required: %w[template_id submitters]
},
annotations: {
readOnlyHint: false,
destructiveHint: true,
idempotentHint: false,
openWorldHint: true
}
}.freeze
# rubocop:disable Metrics
def call
@template = Template.accessible_by(current_ability).find(mcp_params['template_id'])
authorize!(:read, @template)
return render_tool_error('Template has been archived') if @template.archived_at?
authorize!(:create, Submission.new(template: @template, account_id: current_user.account_id))
return render_tool_error('Template has no fields') if @template.fields.blank?
submitters = (mcp_params['submitters'] || []).map do |s|
attrs = s.slice('email', 'name', 'role', 'phone').compact_blank
fields = Array.wrap(s['fields']).filter_map do |f|
next if f['name'].blank?
{ 'name' => f['name'], 'default_value' => f['value'], 'readonly' => true }
end
attrs['fields'] = fields if fields.present?
attrs.with_indifferent_access
end
submissions = Submissions.create_from_submitters(
template: @template,
user: current_user,
source: :mcp,
submitters_order: @template.preferences['submitters_order'].presence || 'random',
submissions_attrs: { submitters: },
params: { 'send_email' => true, 'submitters' => submitters }
)
return render_tool_error('No valid submitters provided') if submissions.blank?
WebhookUrls.enqueue_events(submissions, 'submission.created')
Submissions.send_signature_requests(submissions)
SearchEntries.enqueue_reindex(submissions)
submission = submissions.first
render_tool_result(id: submission.id, status: 'pending')
rescue Submissions::CreateFromSubmitters::BaseError => e
render_tool_error(e.message)
end
# rubocop:enable Metrics
end
end
+44
View File
@@ -0,0 +1,44 @@
# frozen_string_literal: true
class McpController < ActionController::Metal
TOOL_CONTROLLERS = {
'search_templates' => Mcp::SearchTemplatesController,
'load_template' => Mcp::LoadTemplateController,
'create_template' => Mcp::CreateTemplateController,
'send_documents' => Mcp::SendDocumentsController,
'search_documents' => Mcp::SearchDocumentsController
}.freeze
TOOLS = TOOL_CONTROLLERS.map { |_, controller| controller::SCHEMA }.freeze
def call
return Mcp::ProtocolController.dispatch(:ok, request, response) if request.raw_post.blank?
body = JSON.parse(request.raw_post)
body = nil unless body.is_a?(Hash)
request.request_parameters = body || {}
action =
case body&.dig('method')
when 'initialize' then :initialize_request
when 'notifications/initialized' then :initialized_notification
when 'ping' then :ping
when 'tools/list' then :tools_list
when 'tools/call'
tool = TOOL_CONTROLLERS[body.dig('params', 'name')]
return tool.dispatch(:call, request, response) if tool
:tool_not_found
else
:method_not_found
end
Mcp::ProtocolController.dispatch(action, request, response)
rescue JSON::ParserError
request.request_parameters = {}
Mcp::ProtocolController.dispatch(:parse_error, request, response)
end
end
@@ -0,0 +1,37 @@
# frozen_string_literal: true
class McpSettingsController < ApplicationController
load_and_authorize_resource :mcp_token, parent: false
before_action do
authorize!(:manage, :mcp)
end
def index
@mcp_tokens = @mcp_tokens.active.order(id: :desc)
end
def create
@mcp_token = current_user.mcp_tokens.new(mcp_token_params)
if @mcp_token.save
@mcp_tokens = [@mcp_token]
render :index, status: :created
else
render turbo_stream: turbo_stream.replace(:modal, template: 'mcp_settings/new'), status: :unprocessable_content
end
end
def destroy
@mcp_token.update!(archived_at: Time.current)
redirect_back fallback_location: settings_mcp_index_path, notice: I18n.t('mcp_token_has_been_removed')
end
private
def mcp_token_params
params.require(:mcp_token).permit(:name)
end
end
+5 -1
View File
@@ -14,6 +14,8 @@ class MfaSetupController < ApplicationController
def edit; end
def create
RateLimit.call("mfa-setup-otp-#{current_user.id}", limit: 5, ttl: 5.minutes, enabled: true)
if current_user.validate_and_consume_otp!(params[:otp_attempt])
current_user.otp_required_for_login = true
current_user.save!
@@ -29,7 +31,9 @@ class MfaSetupController < ApplicationController
end
def destroy
if current_user.validate_and_consume_otp!(params[:otp_attempt])
RateLimit.call("mfa-setup-otp-#{current_user.id}", limit: 5, ttl: 5.minutes, enabled: true)
if current_user.valid_for_authentication? { current_user.validate_and_consume_otp!(params[:otp_attempt]) }
current_user.update!(otp_required_for_login: false, otp_secret: nil)
redirect_to settings_profile_index_path, notice: I18n.t('2fa_has_been_removed')
@@ -8,6 +8,7 @@ class NotificationsSettingsController < ApplicationController
before_action :build_account_config, only: :create
authorize_resource :account_config, only: :create
before_action :authorize_email_reminders!, only: :create
def index; end
@@ -21,6 +22,14 @@ class NotificationsSettingsController < ApplicationController
private
def authorize_email_reminders!
return unless Docuseal.multitenant?
return if @account_config.key != AccountConfig::SUBMITTER_REMINDERS
return if can?(:manage, :email_reminders)
redirect_back fallback_location: settings_notifications_path, alert: I18n.t('unlock_with_docuseal_pro')
end
def build_account_config
@account_config =
AccountConfig.find_or_initialize_by(account: current_account, key: email_config_params[:key])
@@ -5,48 +5,93 @@ class PreviewDocumentPageController < ActionController::API
FORMAT = Templates::ProcessDocument::FORMAT
def show
attachment_uuid = ApplicationRecord.signed_id_verifier.verified(params[:signed_uuid], purpose: :attachment)
TMPFILE_PREFIX = 'attachment-'
TMPFILE_TTL = 5.minutes
TMPFILE_MAX_TOTAL_SIZE = Docuseal.multitenant? ? 400.megabytes : 1.gigabyte
attachment = ActiveStorage::Attachment.find_by(uuid: attachment_uuid) if attachment_uuid
def show
result_data =
ApplicationRecord.signed_id_verifier.verified(params[:signed_key], purpose: :attachment)
attachment =
if result_data.is_a?(Array) && result_data.compact_blank.size == 2
attachment_id, attachment_uuid = result_data
ActiveStorage::Attachment.find_by(id: attachment_id, uuid: attachment_uuid)
elsif result_data
ActiveStorage::Attachment.find_by(uuid: result_data)
end
return head :not_found unless attachment
@template = attachment.record
preview_image = attachment.preview_images.joins(:blob)
.find_by(blob: { filename: ["#{params[:id]}.png", "#{params[:id]}.jpg"] })
return redirect_to preview_image.url, allow_other_host: true if preview_image
file_path =
if attachment.service.name == :disk
ActiveStorage::Blob.service.path_for(attachment.key)
else
find_or_create_document_tempfile_path(attachment)
end
preview_image =
Templates::ProcessDocument.generate_pdf_preview_from_file(attachment, file_path, params[:id].to_i)
attachment.preview_images.joins(:blob)
.find_by(blob: { filename: ["#{params[:id].to_i}.png", "#{params[:id].to_i}.jpg"] })
redirect_to preview_image.url, allow_other_host: true
end
def find_or_create_document_tempfile_path(attachment)
file_path = "#{Dir.tmpdir}/#{attachment.uuid}"
File.open(file_path, File::RDWR | File::CREAT, 0o644) do |f|
f.flock(File::LOCK_EX)
# rubocop:disable Style/ZeroLengthPredicate
if f.size.zero?
f.binmode
f.write(attachment.download)
end
# rubocop:enable Style/ZeroLengthPredicate
if preview_image
return redirect_to preview_image.url(time: ActiveStorage::Attachment.service_url_time),
allow_other_host: true
end
file_path
preview_image =
open_attachment_io(attachment) do |io|
Templates::ProcessDocument.generate_pdf_preview_from_io(attachment, io, params[:id].to_i)
end
redirect_to preview_image.url(time: ActiveStorage::Attachment.service_url_time), allow_other_host: true
end
def open_attachment_io(attachment, &)
return File.open(ActiveStorage::Blob.service.path_for(attachment.key), 'rb', &) if attachment.service.name == :disk
file_name = "#{TMPFILE_PREFIX}#{Digest::SHA1.hexdigest("#{attachment.id}-#{attachment.uuid}")}"
file_path = File.join(Dir.tmpdir, file_name)
File.open(file_path, File::RDWR | File::CREAT, 0o644) do |file|
file.flock(File::LOCK_EX)
# rubocop:disable Style/ZeroLengthPredicate
if file.size.zero?
cleanup_stale_tempfiles
file.binmode
file.write(attachment.download)
else
FileUtils.touch(file_path)
end
# rubocop:enable Style/ZeroLengthPredicate
file.flock(File::LOCK_UN)
yield file
end
end
def cleanup_stale_tempfiles
entries =
Dir.glob(File.join(Dir.tmpdir, "#{TMPFILE_PREFIX}*")).filter_map do |path|
stat = File.stat(path)
[path, stat.mtime, stat.size]
rescue Errno::ENOENT
nil
end
total_size = entries.sum(&:last)
stale_time = TMPFILE_TTL.ago
entries.sort_by(&:second).each do |path, mtime, size|
break if mtime > stale_time && total_size <= TMPFILE_MAX_TOTAL_SIZE
File.unlink(path)
total_size -= size
rescue Errno::ENOENT
nil
end
end
end
@@ -1,6 +1,14 @@
# frozen_string_literal: true
class RevealAccessTokenController < ApplicationController
rate_limit to: 4, within: 1.minute, only: %i[create], by: -> { current_user.id }, with: lambda {
Rollbar.error('Rate limit api key') if defined?(Rollbar)
render turbo_stream: turbo_stream.replace(:modal, template: 'reveal_access_token/show',
locals: { error_message: I18n.t(:too_many_attempts) }),
status: :unprocessable_content
}
def show
authorize!(:manage, current_user.access_token)
end
+132
View File
@@ -0,0 +1,132 @@
# frozen_string_literal: true
class SearchController < ApplicationController
load_and_authorize_resource :template_folder, parent: false
load_and_authorize_resource :template, parent: false
load_and_authorize_resource :submission, parent: false
load_and_authorize_resource :template, instance_name: :filter_template, id_param: :template_id, parent: true
FOLDERS_LIMIT = 4
LIMIT = 12
def index
@query = params[:q].to_s.squish
return render_infinite_scroll(@template_folders, @templates, @submissions, @query) if turbo_infinite_scroll?
if @query.blank?
@template_folders = @templates = @submissions = []
elsif params[:scope] == 'folders'
@folders_pagy, @template_folders =
pagy(:countless, filter_folders(@template_folders, @templates, @query), limit: LIMIT)
@templates = @submissions = []
else
folders = filter_folders(@template_folders, @templates, @query)
@pagy, @template_folders, @templates, @submissions =
if params[:scope] == 'submissions'
load_submissions_page(folders, @templates, @submissions, @query)
else
load_templates_page(folders, @templates, @submissions, @query)
end
if @templates.blank? && @submissions.blank? && params[:template_id].blank?
@folders_pagy, @template_folders = pagy(:countless, folders, limit: LIMIT)
end
end
end
private
def load_submissions_page(folders, templates, submissions, query)
pagy, submissions = pagy(:countless, filter_submissions(submissions, query), limit: LIMIT)
return [pagy, [], [], submissions] if pagy.next || params[:template_id].present?
[pagy, folders.limit(FOLDERS_LIMIT).to_a, filter_templates(templates, query).limit(LIMIT).to_a, submissions]
end
def load_templates_page(folders, templates, submissions, query)
pagy, templates = pagy(:countless, filter_templates(templates, query), limit: LIMIT)
submissions = pagy.next ? [] : filter_submissions(submissions, query).limit(LIMIT).to_a
[pagy, folders.limit(FOLDERS_LIMIT).to_a, templates, submissions]
end
def render_infinite_scroll(folders, templates, submissions, query)
target, partial, collection =
case params[:scope]
when 'folders'
['search_folders', 'search/folder', filter_folders(folders, templates, query)]
when 'submissions'
['search_submissions', 'search/submission', filter_submissions(submissions, query)]
else
['search_templates', 'search/template', filter_templates(templates, query)]
end
pagy, records = pagy(:countless, collection, limit: LIMIT)
render turbo_stream: [
turbo_stream.append(target, partial:, collection: records),
turbo_stream.replace('infinite_scroll', partial: 'shared/infinite_scroll', locals: { pagy: })
]
end
def filter_folders(folders, templates, query)
return folders.none if params[:archived] == 'true'
folders = folders.where.not(id: current_account.default_template_folder.id)
folders = folders.where(parent_folder_id: filter_folder_ids(params[:folder])) if params[:folder].present?
folders = folders.where(author_id: filter_author_id(params[:author])) if params[:author].present?
folders = TemplateFolders.filter_active_folders(folders, templates)
folders = TemplateFolders.search(folders, query)
folders.preload(:parent_folder).order(:name)
end
def filter_templates(templates, query)
templates = params[:archived] == 'true' ? templates.where.not(archived_at: nil) : templates.active
templates = templates.select_for_list.preload(:author, :template_accesses, folder: :parent_folder)
templates = templates.where(folder_id: filter_folder_ids(params[:folder])) if params[:folder].present?
templates = templates.where(author_id: filter_author_id(params[:author])) if params[:author].present?
Templates.search(current_user, templates, query).order(id: :desc)
end
def filter_folder_ids(folder_name)
TemplateFolders.filter_by_full_name(current_account.template_folders, folder_name)
.preload(:subfolders)
.flat_map { |folder| [folder.id, *folder.subfolders.map(&:id)] }
end
def filter_author_id(email)
current_account.users.find_by(email:)&.id || -1
end
def filter_submissions(submissions, query)
submissions = submissions.left_joins(:template)
submissions =
if params[:archived] == 'true'
submissions.where.not(archived_at: nil).or(submissions.where.not(templates: { archived_at: nil }))
else
submissions.where(archived_at: nil).where(templates: { archived_at: nil })
end
submissions = Submissions.search(current_user, submissions, query, search_template: true)
submissions = Submissions::Filter.call(submissions, current_user, params)
submissions =
if params[:status] == 'completed' || params[:completed_at_from].present? || params[:completed_at_to].present?
submissions.order(completed_at: :desc)
else
submissions.order(id: :desc)
end
submissions.select_for_list.preload(:submitters, :template)
end
end
@@ -14,22 +14,22 @@ class SendSubmissionEmailController < ApplicationController
template = Template.find_by!(slug: params[:template_slug])
@submitter =
Submitter.completed.where(submission: template.submissions).find_by!(email: params[:email].to_s.downcase)
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])
submission = Submission.find_by!(slug: params[:submission_slug])
if submission
@submitter = Submitter.completed.find_by(submission: submission, email: params[:email].to_s.downcase)
end
@submitter = submission.submitters.order(:completed_at).find_by(email: params[:email].to_s.downcase)
return redirect_to submissions_preview_completed_path(params[:submission_slug], status: :error) unless @submitter
else
@submitter = Submitter.completed.find_by!(slug: params[:submitter_slug])
@submitter = Submitter.find_by!(slug: params[:submitter_slug])
end
RateLimit.call("send-email-#{@submitter.id}", limit: 2, ttl: 5.minutes)
if @submitter && completed_submitter?(@submitter)
RateLimit.call("send-email-#{@submitter.id}", limit: 2, ttl: 5.minutes)
SubmitterMailer.documents_copy_email(@submitter, sig: true).deliver_later! if can_send?(@submitter)
SubmitterMailer.documents_copy_email(@submitter, sig: true).deliver_later! if can_send?(@submitter)
end
respond_to do |f|
f.html { render :success }
@@ -39,6 +39,10 @@ class SendSubmissionEmailController < ApplicationController
private
def completed_submitter?(submitter)
submitter.completed_at? || (submitter.viewer? && submitter.submission.completed_at?)
end
def can_send?(submitter)
return false if submitter.account.archived_at?
return false if EmailEvent.exists?(tag: :submitter_documents_copy, email: submitter.email, emailable: submitter,
+21 -3
View File
@@ -3,6 +3,10 @@
class SessionsController < Devise::SessionsController
before_action :configure_permitted_parameters
skip_before_action :verify_authenticity_token, if: -> { request.xhr? && request.format.json? }
respond_to :json, only: :create
around_action :with_browser_locale
def create
@@ -11,12 +15,26 @@ class SessionsController < Devise::SessionsController
if Docuseal.multitenant? && !User.exists?(email:)
Rollbar.warning('Sign in new user') if defined?(Rollbar)
return redirect_to new_registration_path(sign_up: true, user: sign_in_params.slice(:email)),
notice: I18n.t('create_a_new_account')
respond_to do |format|
format.html do
redirect_to new_registration_path(sign_up: true, user: sign_in_params.slice(:email)),
notice: t('create_a_new_account')
end
format.json do
render json: { sign_up: true, notice: t('create_a_new_account') }, status: :unprocessable_content
end
end
return
end
if User.exists?(email:, otp_required_for_login: true) && sign_in_params[:otp_attempt].blank?
return render :otp, locals: { resource: User.new(sign_in_params) }, status: :unprocessable_content
respond_to do |format|
format.html { render :otp, locals: { resource: User.new(sign_in_params) }, status: :unprocessable_content }
format.json { render json: { otp_required: true, email_otp: false }, status: :unprocessable_content }
end
return
end
super
+7
View File
@@ -0,0 +1,7 @@
# frozen_string_literal: true
class SettingsController < ApplicationController
skip_authorization_check
def index; end
end
+51 -119
View File
@@ -8,13 +8,9 @@ class StartFormController < ApplicationController
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
if @template.preferences['require_phone_2fa'] || @template.preferences['require_email_2fa']
raise ActionController::RoutingError, I18n.t('not_found')
@@ -23,8 +19,7 @@ class StartFormController < ApplicationController
if @template.shared_link?
@submitter = @template.submissions.new(account_id: @template.account_id)
.submitters.new(account_id: @template.account_id,
uuid: (filter_undefined_submitters(@template).first ||
@template.submitters.first)['uuid'])
uuid: Submitters::StartForm.first_submitter_uuid(@template))
render :email_verification if params[:email_verification]
else
Rollbar.warning("Not shared template: #{@template.id}") if defined?(Rollbar)
@@ -41,24 +36,28 @@ class StartFormController < ApplicationController
if @submitter.completed_at?
redirect_to start_form_completed_path(@template.slug, submitter_params.compact_blank)
else
if filter_undefined_submitters(@template).size > 1 && @submitter.new_record?
if Templates.filter_undefined_submitters(@template.submitters).size > 1 && @submitter.new_record?
@error_message = multiple_submitters_error_message
return render :show, status: :unprocessable_content
end
if (is_new_record = @submitter.new_record?)
assign_submission_attributes(@submitter, @template)
Submitters::StartForm.assign_submission_attributes(
@submitter, @template, ip: request.remote_ip, user_agent: request.user_agent
)
Submissions::AssignDefinedSubmitters.call(@submitter.submission)
else
@submitter.assign_attributes(ip: request.remote_ip, ua: request.user_agent)
end
if @template.preferences['shared_link_2fa'] == true
handle_require_2fa(@submitter, is_new_record:)
if require_link_2fa?(@template, @submitter)
handle_require_2fa(@template, @submitter)
elsif @submitter.errors.blank? && @submitter.save
enqueue_new_submitter_jobs(@submitter) if is_new_record
Submitters::StartForm.enqueue_new_submitter_jobs(@submitter) if is_new_record
Submitters::StartForm.assign_start_form_cookie(@submitter, request)
redirect_to submit_form_path(@submitter.slug)
else
@@ -88,27 +87,40 @@ class StartFormController < ApplicationController
private
def enqueue_new_submitter_jobs(submitter)
WebhookUrls.enqueue_events(submitter.submission, 'submission.created')
def find_or_initialize_submitter(template, submitter_params)
required_fields = template.preferences.fetch('link_form_fields', ['email'])
SearchEntries.enqueue_reindex(submitter)
blank_fields = required_fields.select { |key| submitter_params[key].blank? }
return unless submitter.submission.expire_at?
submitter =
if blank_fields.present?
Submitter.new(submitter_params)
else
Submitters::StartForm.find_or_initialize_submitter(
template, submitter_params, exclude_completed: params[:resubmit].present?,
request:, current_user:
)
end
ProcessSubmissionExpiredJob.perform_at(submitter.submission.expire_at, 'submission_id' => submitter.submission_id)
blank_fields.each { |key| submitter.errors.add(key.to_sym, :blank) }
submitter
end
def load_resubmit_submitter
@resubmit_submitter =
if params[:resubmit].present? && !params[:resubmit].in?([true, 'true'])
Submitter.find_by(slug: params[:resubmit])
end
def require_link_2fa?(template, submitter)
return true if template.preferences['shared_link_2fa'] == true
return false if cookies.encrypted[:start_form_slug] == submitter.slug
return false if current_user && submitter.email == current_user.email &&
current_user.account_id == submitter.account_id
!submitter.new_record?
end
def authorize_start!
return redirect_to start_form_path(@template.slug) if @template.archived_at?
is_archived = @template.archived_at? || @template.account.archived_at?
return redirect_to start_form_path(@template.slug) if is_archived
return if @resubmit_submitter
return if @template.shared_link? || (current_user && current_ability.can?(:read, @template))
Rollbar.warning("Not shared template: #{@template.id}") if defined?(Rollbar)
@@ -116,87 +128,14 @@ class StartFormController < ApplicationController
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
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
@template =
if @resubmit_submitter
@resubmit_submitter.template
else
Template.find_by!(slug: params[:slug] || params[:start_form_slug])
end
@template = Template.find_by!(slug: params[:slug] || params[:start_form_slug])
end
def multiple_submitters_error_message
@@ -207,38 +146,31 @@ class StartFormController < ApplicationController
end
end
def handle_require_2fa(submitter, is_new_record:)
def handle_require_2fa(template, submitter)
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
if Submitters::StartForm.verify_2fa_and_save_submitter(submitter, request, is_new_record: submitter.new_record?)
redirect_to submit_form_path(submitter.slug)
else
if defined?(Rollbar) && template.preferences['shared_link_2fa'] != true
Rollbar.info("2FA link requested: #{submitter.id}")
end
Submitters.send_shared_link_email_verification_code(submitter, request:)
render :email_verification
end
rescue Submitters::StartForm::NotSaved
render :show, status: :unprocessable_content
rescue Submitters::BouncedEmail => e
@error_message = e.message
render :error, status: :unprocessable_content
rescue Submitters::UnableToSendCode, Submitters::InvalidOtp => e
redirect_to start_form_path(submitter.submission.template.slug,
params: submitter_params.merge(email_verification: true)),
redirect_to start_form_path(template.slug, params: submitter_params.merge(email_verification: true)),
alert: e.message
rescue RateLimit::LimitApproached
redirect_to start_form_path(submitter.submission.template.slug,
params: submitter_params.merge(email_verification: true)),
redirect_to start_form_path(template.slug, params: submitter_params.merge(email_verification: true)),
alert: I18n.t(:too_many_attempts)
end
end
@@ -1,23 +1,34 @@
# frozen_string_literal: true
class StartFormEmail2faSendController < ApplicationController
layout 'form'
around_action :with_browser_locale
skip_before_action :authenticate_user!
skip_authorization_check
def create
@template = Template.find_by!(slug: params[:slug])
before_action :load_template
before_action :authorize_start!
def create
@submitter = @template.submissions.new(account_id: @template.account_id)
.submitters.new(**submitter_params, account_id: @template.account_id)
if @submitter.email.blank?
return redirect_to start_form_path(@template.slug), alert: I18n.t(:provide_your_email_to_start)
end
Submitters.send_shared_link_email_verification_code(@submitter, request:)
redir_params = { notice: I18n.t(:code_has_been_resent) } if params[:resend]
redirect_to start_form_path(@template.slug, params: submitter_params.merge(email_verification: true)),
**redir_params
rescue Submitters::BouncedEmail => e
@error_message = e.message
render 'start_form/error', status: :unprocessable_content
rescue Submitters::UnableToSendCode => e
redirect_to start_form_path(@template.slug, params: submitter_params.merge(email_verification: true)),
alert: e.message
@@ -25,7 +36,34 @@ class StartFormEmail2faSendController < ApplicationController
private
def load_template
@template = Template.find_by!(slug: params[:slug])
end
def authorize_start!
is_archived = @template.archived_at? || @template.account.archived_at?
return redirect_to start_form_path(@template.slug) if is_archived
return if (@template.shared_link? || (current_user && current_ability.can?(:read, @template))) &&
(@template.preferences['shared_link_2fa'] == true || submitter_exists?(@template, params))
Rollbar.warning("Not 2FA shared template: #{@template.id}") if defined?(Rollbar)
redirect_to start_form_path(@template.slug)
end
def submitter_exists?(template, params)
email = Submissions.normalize_email(params.dig(:submitter, :email))
return false if email.blank?
Submitters::StartForm.build_submitters_scope(template, exclude_completed: true).exists?(email:)
end
def submitter_params
params.require(:submitter).permit(:name, :email, :phone)
params.require(:submitter).permit(:name, :email, :phone).tap do |attrs|
attrs[:email] = Submissions.normalize_email(attrs[:email])
end
end
end
@@ -0,0 +1,103 @@
# frozen_string_literal: true
class StartFormResubmitController < ApplicationController
layout 'form'
skip_before_action :authenticate_user!
skip_authorization_check
around_action :with_browser_locale
before_action :load_resubmit_submitter
before_action :load_template
before_action :authorize_start!
def update
@submitter = find_or_initialize_submitter(@template, @resubmit_submitter)
if Templates.filter_undefined_submitters(@template.submitters).size > 1 && @submitter.new_record?
@error_message = multiple_submitters_error_message
return render 'start_form/show', status: :unprocessable_content
end
if (is_new_record = @submitter.new_record?)
Submitters::StartForm.assign_submission_attributes(
@submitter, @template,
ip: request.remote_ip, user_agent: request.user_agent, resubmit_submitter: @resubmit_submitter,
source: @resubmit_submitter.submission.source_self? ? :self : :link
)
Submissions::AssignDefinedSubmitters.call(@submitter.submission)
else
@submitter.assign_attributes(ip: request.remote_ip, ua: request.user_agent)
end
if @template.preferences['shared_link_2fa'] == true
handle_require_2fa(@submitter)
elsif @submitter.save
Submitters::StartForm.enqueue_new_submitter_jobs(@submitter) if is_new_record
Submitters::StartForm.assign_start_form_cookie(@submitter, request)
redirect_to submit_form_path(@submitter.slug)
else
render 'start_form/show', status: :unprocessable_content
end
end
private
def find_or_initialize_submitter(template, resubmit_submitter)
Submitters::StartForm.build_submitters_scope(template, exclude_completed: true)
.find_by(slug: cookies.encrypted[:start_form_slug],
email: resubmit_submitter.email) || Submitter.new
end
def load_resubmit_submitter
@resubmit_submitter = Submitter.find_by(slug: params[:resubmit])
raise ActiveRecord::RecordNotFound if @resubmit_submitter.blank? ||
!Submitters::StartForm.can_resubmit?(@resubmit_submitter)
end
def load_template
@template = @resubmit_submitter.template
end
def authorize_start!
redirect_to submit_form_path(@resubmit_submitter.slug) if @template.archived_at? || @template.account.archived_at?
end
def multiple_submitters_error_message
if current_user&.account_id == @template.account_id
helpers.t('this_submission_has_multiple_signers_which_prevents_the_use_of_a_sharing_link_html')
else
I18n.t('not_found')
end
end
def handle_require_2fa(submitter)
if Submitters::StartForm.verify_2fa_and_save_submitter(submitter, request,
is_new_record: submitter.new_record?)
redirect_to submit_form_path(submitter.slug)
else
Submitters.send_shared_link_email_verification_code(submitter, request:)
render 'start_form/email_verification'
end
rescue Submitters::StartForm::NotSaved
render 'start_form/show', status: :unprocessable_content
rescue Submitters::BouncedEmail => e
@error_message = e.message
render 'start_form/error', status: :unprocessable_content
rescue Submitters::UnableToSendCode, Submitters::InvalidOtp => e
flash.now[:alert] = e.message
render 'start_form/email_verification'
rescue RateLimit::LimitApproached
flash.now[:alert] = I18n.t(:too_many_attempts)
render 'start_form/email_verification'
end
end
@@ -0,0 +1,50 @@
# frozen_string_literal: true
class StartFormSelfController < ApplicationController
load_resource :template, parent: false
around_action :with_browser_locale
before_action :authorize_start!
def update
@submitter = Submitters::StartForm.build_submitters_scope(@template, exclude_completed: true, source: :self)
.find_or_initialize_by(email: current_user.email)
@submitter = Submitter.new(email: current_user.email) if @submitter.persisted? &&
outdated_template_fields?(@submitter, @template)
if (is_new_record = @submitter.new_record?)
@submitter.name = current_user.full_name
Submitters::StartForm.assign_submission_attributes(
@submitter, @template, ip: request.remote_ip, user_agent: request.user_agent, source: :self
)
Submissions::AssignDefinedSubmitters.call(@submitter.submission)
else
@submitter.assign_attributes(ip: request.remote_ip, ua: request.user_agent)
end
@submitter.save!
Submitters::StartForm.enqueue_new_submitter_jobs(@submitter) if is_new_record
Submitters::StartForm.assign_start_form_cookie(@submitter, request)
redirect_to submit_form_path(@submitter.slug)
end
private
def outdated_template_fields?(submitter, template)
template_fields = submitter.submission.template_fields
template_fields.present? && template_fields != template.fields
end
def authorize_start!
authorize!(:read, @template)
redirect_to template_path(@template) if @template.archived_at?
end
end
@@ -16,6 +16,7 @@ class SubmissionEventsController < ApplicationController
'email_verified' => 'email_check',
'click_sms' => 'hand_click',
'decline_form' => 'x',
'delegate_form' => 'user_share',
'start_verification' => 'player_play',
'complete_verification' => 'check',
'invite_party' => 'user_plus'
@@ -23,5 +24,7 @@ class SubmissionEventsController < ApplicationController
load_and_authorize_resource :submission
def index; end
def index
render :index, layout: 'plain'
end
end
@@ -7,17 +7,39 @@ class SubmissionsArchivedController < ApplicationController
@submissions = @submissions.left_joins(:template)
@submissions = @submissions.where.not(archived_at: nil)
.or(@submissions.where.not(templates: { archived_at: nil }))
.preload(:template_accesses, :created_by_user, template: :author)
.preload(:template_accesses, :created_by_user)
@submissions = Submissions.search(current_user, @submissions, params[:q], search_template: true)
@submissions = Submissions::Filter.call(@submissions, current_user, params)
@submissions = if params[:completed_at_from].present? || params[:completed_at_to].present?
@submissions.order(Submitter.arel_table[:completed_at].maximum.desc)
else
@submissions.order(id: :desc)
end
@submissions =
if params[:status] == 'completed' || params[:completed_at_from].present? || params[:completed_at_to].present?
@submissions.order(completed_at: :desc)
else
@submissions.order(id: :desc)
end
@pagy, @submissions = pagy_auto(@submissions.preload(submitters: :start_form_submission_events))
@pagy, @submissions = pagy_auto(@submissions.select_for_list.preload(submitters: :start_form_submission_events))
template_scope = @submissions.all?(&:template_submitters) ? Template.select_for_list : nil
ActiveRecord::Associations::Preloader.new(records: @submissions,
associations: :template,
scope: template_scope).call
ActiveRecord::Associations::Preloader.new(records: @submissions.filter_map(&:template),
associations: :author).call
render_infinite_scroll(@pagy, @submissions) if turbo_infinite_scroll?
end
private
def render_infinite_scroll(pagy, submissions)
render turbo_stream: [
turbo_stream.before('infinite_scroll', partial: 'templates/submission', collection: submissions,
locals: { with_template: true, archived: true }),
turbo_stream.replace('infinite_scroll', partial: 'shared/infinite_scroll', locals: { pagy: })
]
end
end
+63 -21
View File
@@ -1,9 +1,7 @@
# frozen_string_literal: true
class SubmissionsController < ApplicationController
before_action :load_template, only: %i[new create]
authorize_resource :template, only: %i[new create]
load_and_authorize_resource :template, only: %i[new create]
load_and_authorize_resource :submission, only: %i[show destroy]
prepend_before_action :maybe_redirect_com, only: %i[show]
@@ -20,13 +18,23 @@ class SubmissionsController < ApplicationController
'payment' => 'credit_card', 'verification' => 'id', 'kba' => 'user_scan'
}.freeze
STATUS_BADGES = {
'awaiting' => 'badge-info', 'sent' => 'badge-info', 'completed' => 'badge-success',
'opened' => 'badge-warning', 'declined' => 'badge-error'
}.freeze
STATUS_ICONS = {
'awaiting' => 'clock', 'sent' => 'send', 'completed' => 'circle_check',
'opened' => 'mail_opened', 'declined' => 'x_circle'
}.freeze
def show
@submission = Submissions.preload_with_pages(@submission)
unless @submission.submitters.all?(&:completed_at?)
unless @submission.completed_at?
ActiveRecord::Associations::Preloader.new(
records: [@submission],
associations: [submitters: :start_form_submission_events]
associations: [{ submitters: :start_form_submission_events }]
).call
end
@@ -35,10 +43,14 @@ class SubmissionsController < ApplicationController
def new
authorize!(:new, Submission)
render :new, layout: 'plain'
end
def create
save_template_message(@template, params) if params[:save_message] == '1'
return redirect_to template_path(@template), alert: I18n.t('template_has_been_archived') if @template.archived_at?
save_template_message(@template, params) if params[:save_message] == '1' && can?(:update, @template)
[params.delete(:subject), params.delete(:body)] if params[:is_custom_message] != '1'
@@ -51,20 +63,11 @@ class SubmissionsController < ApplicationController
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',
submissions_attrs:,
new_fields:,
params: params.merge('send_completed_email' => true))
create_submissions(@template, submissions_params, params)
end
raise Submissions::CreateFromSubmitters::BaseError, 'Invalid email address' if submissions.blank?
WebhookUrls.enqueue_events(submissions, 'submission.created')
Submissions.send_signature_requests(submissions)
@@ -92,11 +95,35 @@ class SubmissionsController < ApplicationController
I18n.t('submission_has_been_archived')
end
redirect_back(fallback_location: @submission.template_id ? template_path(@submission.template) : root_path, notice:)
if turbo_native_app? && request.format.turbo_stream?
flash.now[:notice] = notice
render turbo_stream: [turbo_stream.remove(@submission), turbo_stream.append_all('body', partial: 'shared/flash')]
else
redirect_back(fallback_location: @submission.template_id ? template_path(@submission.template) : root_path,
notice:)
end
end
private
def create_submissions(template, submissions_params, params)
normalize_message_submitter_uuids!(params)
submissions_attrs = submissions_params[:submission].to_h.values
submissions_attrs, _, new_fields =
Submissions::NormalizeParamUtils.normalize_submissions_params!(submissions_attrs, template, add_fields: true)
Submissions.create_from_submitters(template: template,
user: current_user,
source: :invite,
submitters_order: params[:preserve_order] == '1' ? 'preserved' : 'random',
submissions_attrs:,
new_fields:,
params: params.merge('send_completed_email' => true))
end
def save_template_message(template, params)
template.preferences['request_email_subject'] = params[:subject] if params[:subject].present?
template.preferences['request_email_body'] = params[:body] if params[:body].present?
@@ -108,7 +135,22 @@ class SubmissionsController < ApplicationController
params.permit(submission: { submitters: [:uuid, :email, :phone, :name, { values: {} }] })
end
def load_template
@template = Template.accessible_by(current_ability).find(params[:template_id])
def normalize_message_submitter_uuids!(params)
return if params[:request_email_per_submitter] == '1'
uuids = params[:email_message_submitter_uuids]
return if uuids.blank?
return if params[:subject].blank? && params[:body].blank?
params[:submitter_preferences] =
Array.wrap(uuids).index_with { { 'subject' => params[:subject], 'body' => params[:body] } }
params[:request_email_per_submitter] = '1'
params.delete(:subject)
params.delete(:body)
params
end
end
@@ -8,17 +8,39 @@ class SubmissionsDashboardController < ApplicationController
@submissions = @submissions.where(archived_at: nil)
.where(templates: { archived_at: nil })
.preload(:template_accesses, :created_by_user, template: :author)
.preload(:template_accesses, :created_by_user)
@submissions = Submissions.search(current_user, @submissions, params[:q], search_template: true)
@submissions = Submissions::Filter.call(@submissions, current_user, params)
@submissions = if params[:completed_at_from].present? || params[:completed_at_to].present?
@submissions.order(Submitter.arel_table[:completed_at].maximum.desc)
else
@submissions.order(id: :desc)
end
@submissions =
if params[:status] == 'completed' || params[:completed_at_from].present? || params[:completed_at_to].present?
@submissions.order(completed_at: :desc)
else
@submissions.order(id: :desc)
end
@pagy, @submissions = pagy_auto(@submissions.preload(submitters: :start_form_submission_events))
@pagy, @submissions = pagy_auto(@submissions.select_for_list.preload(submitters: :start_form_submission_events))
template_scope = @submissions.all?(&:template_submitters) ? Template.select_for_list : nil
ActiveRecord::Associations::Preloader.new(records: @submissions,
associations: :template,
scope: template_scope).call
ActiveRecord::Associations::Preloader.new(records: @submissions.filter_map(&:template),
associations: :author).call
render_infinite_scroll(@pagy, @submissions) if turbo_infinite_scroll?
end
private
def render_infinite_scroll(pagy, submissions)
render turbo_stream: [
turbo_stream.before('infinite_scroll', partial: 'templates/submission', collection: submissions,
locals: { with_template: true }),
turbo_stream.replace('infinite_scroll', partial: 'shared/infinite_scroll', locals: { pagy: })
]
end
end
@@ -9,7 +9,7 @@ class SubmissionsDebugController < ApplicationController
def index
@submitter = Submitter.preload({ attachments_attachments: :blob },
submission: { template: { documents_attachments: :blob } })
.find_by(slug: params[:submitter_slug])
.find_by(slug: params[:submit_form_slug])
respond_to do |f|
f.html do
@@ -1,40 +1,17 @@
# frozen_string_literal: true
class SubmissionsDownloadController < ApplicationController
skip_before_action :authenticate_user!
skip_authorization_check
TTL = 40.minutes
FILES_TTL = 5.minutes
load_and_authorize_resource :submission
def index
@submitter = Submitter.find_signed(params[:sig], purpose: :download_completed) if params[:sig].present?
signature_valid =
if @submitter&.slug == params[:submitter_slug]
true
else
@submitter = nil
end
@submitter ||= Submitter.find_by!(slug: params[:submitter_slug])
Submissions::EnsureResultGenerated.call(@submitter)
last_submitter = @submitter.submission.submitters.where.not(completed_at: nil).order(:completed_at).last
last_submitter = @submission.submitters.where.not(completed_at: nil).order(:completed_at).last
return head :not_found unless last_submitter
Submissions::EnsureResultGenerated.call(last_submitter)
if last_submitter.completed_at < TTL.ago && !signature_valid && !current_user_submitter?(last_submitter)
Rollbar.info("TTL: #{last_submitter.id}") if defined?(Rollbar)
return head :not_found
end
if params[:combined] == 'true'
url = build_combined_url(@submitter)
url = Submitters.build_combined_url(last_submitter)
if url
render json: [url]
@@ -42,43 +19,7 @@ class SubmissionsDownloadController < ApplicationController
head :not_found
end
else
render json: build_urls(last_submitter)
render json: Submitters.build_document_urls(last_submitter)
end
end
private
def current_user_submitter?(submitter)
current_user && current_user.account.submitters.exists?(id: submitter.id)
end
def build_urls(submitter)
filename_format = AccountConfig.find_or_initialize_by(account_id: submitter.account_id,
key: AccountConfig::DOCUMENT_FILENAME_FORMAT_KEY)&.value
Submitters.select_attachments_for_download(submitter).map do |attachment|
ActiveStorage::Blob.proxy_url(
attachment.blob,
expires_at: FILES_TTL.from_now.to_i,
filename: Submitters.build_document_filename(submitter, attachment.blob, filename_format)
)
end
end
def build_combined_url(submitter)
return if submitter.submission.submitters.exists?(completed_at: nil)
return if submitter.submission.submitters.order(:completed_at).last != submitter
attachment = submitter.submission.combined_document_attachment
attachment ||= Submissions::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
@@ -5,10 +5,11 @@ class SubmissionsExportController < ApplicationController
load_and_authorize_resource :submission, through: :template, parent: false, only: :index
def index
submissions = @submissions.active
.preload(submitters: { documents_attachments: :blob,
attachments_attachments: :blob })
.order(id: :asc)
submissions = params[:archived] == 'true' ? @submissions.archived : @submissions.active
submissions = submissions.preload(submitters: { documents_attachments: :blob,
attachments_attachments: :blob })
.order(id: :asc)
submissions = Submissions.search(current_user, submissions, params[:q], search_values: true)
submissions = Submissions::Filter.call(submissions, current_user, params)
@@ -10,20 +10,22 @@ class SubmissionsPreviewController < ApplicationController
TTL = 40.minutes
def show
submitter = Submitter.find_signed(params[:sig], purpose: :download_completed) if params[:sig].present?
@sig_submitter = Submitter.find_signed(params[:sig], purpose: :download_completed) if params[:sig].present?
signature_valid =
if submitter && submitter.submission.slug == params[:slug]
@submission = submitter.submission
if @sig_submitter && @sig_submitter.submission.slug == params[:slug]
@submission = @sig_submitter.submission
true
else
@sig_submitter = nil
end
@submission ||= Submission.find_by!(slug: params[:slug])
raise ActionController::RoutingError, I18n.t('not_found') if @submission.account.archived_at?
if !@submission.submitters.all?(&:completed_at?) && !signature_valid &&
if !@submission.completed_at? && !signature_valid &&
(!current_user || !current_ability.can?(:read, @submission))
raise ActionController::RoutingError, I18n.t('not_found')
end
@@ -36,7 +38,7 @@ class SubmissionsPreviewController < ApplicationController
@submission = Submissions.preload_with_pages(@submission)
render 'submissions/show', layout: 'plain'
render 'submissions/show', layout: 'plain', locals: { is_preview: true }
end
def completed
@@ -0,0 +1,64 @@
# frozen_string_literal: true
class SubmissionsPreviewDownloadController < ApplicationController
skip_before_action :authenticate_user!
skip_authorization_check
TTL = 40.minutes
def index
@submission = Submission.find_by!(slug: params[:submission_slug] || params[:submissions_preview_slug])
last_submitter = @submission.submitters.where.not(completed_at: nil).order(:completed_at).last
return head :not_found unless last_submitter
Submissions::EnsureResultGenerated.call(last_submitter)
unless current_user_submission?(@submission)
if use_2fa?(@submission)
Rollbar.info("2FA download error: #{last_submitter.id}") if defined?(Rollbar)
return head :not_found
end
if last_submitter.completed_at < TTL.ago
Rollbar.info("TTL: #{last_submitter.id}") if defined?(Rollbar)
return head :not_found
end
end
if params[:combined] == 'true'
respond_with_combined(last_submitter)
else
render json: Submitters.build_document_urls(last_submitter)
end
end
private
def respond_with_combined(submitter)
url = Submitters.build_combined_url(submitter)
if url
render json: [url]
else
head :not_found
end
end
def current_user_submission?(submission)
current_user && current_ability.can?(:read, submission)
end
def use_2fa?(submission)
return true if submission.submitters.any? do |e|
e.preferences['require_phone_2fa'] || e.preferences['require_email_2fa']
end
return true if submission.template&.preferences&.dig('require_phone_2fa')
return true if submission.template&.preferences&.dig('require_email_2fa')
false
end
end
@@ -0,0 +1,39 @@
# frozen_string_literal: true
class SubmissionsResendEmailController < ApplicationController
load_and_authorize_resource :submission
before_action do
authorize!(:manage, :resend_all)
authorize!(:update, @submission)
end
def create
submitters = @submission.submitters.reject(&:completed_at?).select { |s| s.email.present? && !s.declined_at? }
if Docuseal.multitenant?
recent_submitter_ids =
SubmissionEvent.where(submitter_id: submitters.map(&:id),
event_type: 'send_email',
created_at: 10.hours.ago..Time.current).pluck(:submitter_id).to_set
submitters = submitters.reject { |s| recent_submitter_ids.include?(s.id) }
end
submitters.each do |submitter|
SendSubmitterInvitationEmailJob.perform_async('submitter_id' => submitter.id)
submitter.sent_at ||= Time.current
submitter.save!
end
notice =
if submitters.empty?
I18n.t('email_has_been_sent_already')
else
I18n.t('emails_have_been_sent_to_n_recipients', count: submitters.size)
end
redirect_back(fallback_location: submission_path(@submission), notice:)
end
end
@@ -4,6 +4,8 @@ class SubmissionsUnarchiveController < ApplicationController
load_and_authorize_resource :submission
def create
authorize!(:destroy, @submission)
@submission.update!(archived_at: nil)
redirect_to submission_path(@submission), notice: I18n.t('submission_has_been_unarchived')
@@ -0,0 +1,13 @@
# frozen_string_literal: true
class SubmitFormClickEmailController < ActionController::API
def create
@submitter = Submitter.find_by!(slug: params[:submit_form_slug])
if params[:t].present? && params[:t] == SubmissionEvents.build_tracking_param(@submitter, 'click_email')
SubmissionEvents.create_with_tracking_data(@submitter, 'click_email', request)
end
render json: {}
end
end
@@ -0,0 +1,73 @@
# frozen_string_literal: true
class SubmitFormCompletedDownloadController < ApplicationController
skip_before_action :authenticate_user!
skip_authorization_check
TTL = 40.minutes
FILES_TTL = 5.minutes
def index
@submitter = Submitter.find_signed(params[:sig], purpose: :download_completed) if params[:sig].present?
signature_valid = @submitter&.slug == submitter_slug
@submitter = Submitter.find_by!(slug: submitter_slug) unless signature_valid
unless completed_submitter?(@submitter)
Rollbar.error("Not completed: #{@submitter.id}") if defined?(Rollbar)
return head :not_found
end
Submissions::EnsureResultGenerated.call(@submitter) if @submitter.completed_at?
last_submitter = @submitter.submission.submitters.completed.order(:completed_at).last
return head :not_found unless last_submitter
Submissions::EnsureResultGenerated.call(last_submitter)
if !signature_valid && !current_user_submitter?(last_submitter)
unless Submitters::AuthorizedForForm.call(@submitter, current_user, request, with_order: true)
return head :not_found
end
if last_submitter.completed_at < TTL.ago
Rollbar.info("TTL: #{last_submitter.id}") if defined?(Rollbar)
return head :not_found
end
end
if params[:combined] == 'true'
respond_with_combined(last_submitter)
else
render json: Submitters.build_document_urls(last_submitter)
end
end
private
def submitter_slug
params[:submit_form_slug] || params[:submitter_slug]
end
def respond_with_combined(submitter)
url = Submitters.build_combined_url(submitter)
if url
render json: [url]
else
head :not_found
end
end
def completed_submitter?(submitter)
submitter.completed_at? || (submitter.viewer? && submitter.submission.completed_at?)
end
def current_user_submitter?(submitter)
current_user && current_ability.can?(:read, submitter)
end
end
+51 -32
View File
@@ -3,21 +3,25 @@
class SubmitFormController < ApplicationController
layout 'form'
around_action :with_browser_locale, only: %i[show completed success]
around_action :with_browser_locale, only: %i[show completed success delegated]
skip_before_action :authenticate_user!
skip_authorization_check
before_action :load_submitter, only: %i[show update completed]
before_action :maybe_redirect_delegated, only: %i[show completed]
before_action :maybe_render_locked_page, only: :show
before_action :maybe_require_link_2fa, only: %i[show update]
before_action :maybe_require_link_2fa, only: %i[show]
CONFIG_KEYS = [].freeze
def show
submission = @submitter.submission
return redirect_to submit_form_completed_path(@submitter.slug) if @submitter.completed_at?
return render :email_2fa if require_email_2fa?(@submitter)
return render :email_2fa unless Submitters::AuthorizedForForm.pass_email_2fa?(@submitter, request)
if @submitter.completed_at? || submission.completed_at?
return redirect_to submit_form_completed_path(@submitter.slug)
end
@form_configs = Submitters::FormConfigs.call(@submitter, CONFIG_KEYS)
@@ -27,28 +31,26 @@ class SubmitFormController < ApplicationController
Submissions.preload_with_pages(submission)
Submitters::MaybeUpdateDefaultValues.call(@submitter, current_user)
@attachments_index = build_attachments_index(submission)
if current_user && current_user.email == @submitter.email
@current_user_data = current_user.as_json(only: %i[first_name last_name], methods: %i[full_name])
initials = UserConfigs.load_initials(current_user)
@prefill_initials = Submitters.build_prefill_attachment_data(@submitter, initials) if initials
end
return unless @form_configs[:prefill_signature]
if (user_signature = UserConfigs.load_signature(current_user))
@signature_attachment = ActiveStorage::Attachment.find_or_create_by!(
blob_id: user_signature.blob_id,
name: 'attachments',
record: @submitter
)
end
signature = UserConfigs.load_signature(current_user) ||
Submitters::FindRememberedSignature.call(@submitter, params, cookies)
@signature_attachment ||=
Submitters::MaybeAssignDefaultBrowserSignature.call(@submitter, params, cookies, @attachments_index.values)
@attachments_index[@signature_attachment.uuid] = @signature_attachment if @signature_attachment
@prefill_signature = Submitters.build_prefill_attachment_data(@submitter, signature) if signature
end
def update
if require_email_2fa?(@submitter)
unless Submitters::AuthorizedForForm.call(@submitter, current_user, request)
return render json: { error: I18n.t('verification_required_refresh_the_page_and_pass_2fa') },
status: :unprocessable_content
end
@@ -70,6 +72,12 @@ class SubmitFormController < ApplicationController
status: :unprocessable_content
end
if @submitter.viewer?
Rollbar.warning("Submit viewer: #{@submitter.id}") if defined?(Rollbar)
return render json: { error: I18n.t('form_is_view_only') }, status: :unprocessable_content
end
Submitters::SubmitValues.call(@submitter, params, request)
head :ok
@@ -78,24 +86,33 @@ class SubmitFormController < ApplicationController
render json: { field_uuid: e.message }, status: :unprocessable_content
rescue Submitters::SubmitValues::ValidationError => e
Rollbar.warning("Validation error #{@submitter.id}: #{e.message}") if defined?(Rollbar)
render json: { error: e.message }, status: :unprocessable_content
end
def completed
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)
return if Submitters::AuthorizedForForm.call(@submitter, current_user, request)
redirect_to submit_form_path(params[:submit_form_slug])
end
def success; end
def delegated
submitter_version = SubmitterVersion.find_by!(slug: params[:slug] || params[:submit_form_slug])
@submitter = submitter_version.submitter
maybe_render_locked_page
end
private
def maybe_require_link_2fa
return if @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
return if Submitters::AuthorizedForForm.pass_link_2fa?(@submitter, current_user, request)
redirect_to start_form_path(@submitter.submission.template.slug)
end
@@ -109,20 +126,22 @@ class SubmitFormController < ApplicationController
render :declined if @submitter.declined_at?
end
def maybe_redirect_delegated
return if @submitter
submitter_version = SubmitterVersion.find_by!(slug: params[:slug] || params[:submit_form_slug])
submitter_version.submitter.submission_events.find_by!(event_type: :delegate_form)
redirect_to submit_form_delegated_path(submitter_version.slug)
end
def load_submitter
@submitter = Submitter.find_by!(slug: params[:slug] || params[:submit_form_slug])
@submitter = Submitter.find_by(slug: params[:slug] || params[:submit_form_slug])
end
def build_attachments_index(submission)
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
@@ -4,29 +4,45 @@ class SubmitFormDeclineController < ApplicationController
skip_before_action :authenticate_user!
skip_authorization_check
def create
submitter = Submitter.find_by!(slug: params[:submit_form_slug])
before_action :load_submitter
return redirect_to submit_form_path(submitter.slug) if submitter.declined_at? ||
submitter.completed_at? ||
submitter.submission.archived_at? ||
submitter.submission.expired? ||
submitter.submission.template&.archived_at?
def create
return redirect_to submit_form_path(@submitter.slug) if declining_disabled? ||
@submitter.declined_at? ||
@submitter.completed_at? ||
@submitter.submission.archived_at? ||
@submitter.submission.expired? ||
@submitter.submission.template&.archived_at? ||
@submitter.viewer? ||
!Submitters::AuthorizedForForm.call(@submitter,
current_user,
request)
ApplicationRecord.transaction do
submitter.update!(declined_at: Time.current)
@submitter.update!(declined_at: Time.current, values: @submitter.preferences['default_values'] || {})
SubmissionEvents.create_with_tracking_data(submitter, 'decline_form', request, { reason: params[:reason] })
SubmissionEvents.create_with_tracking_data(@submitter, 'decline_form', request, { reason: params[:reason] })
end
user = submitter.submission.created_by_user || submitter.template.author
user = @submitter.submission.created_by_user || @submitter.template.author
if user.user_configs.find_by(key: UserConfig::RECEIVE_DECLINED_EMAIL)&.value != false
SubmitterMailer.declined_email(submitter, user).deliver_later!
if Users.send_emails?(user) &&
user.user_configs.find_by(key: UserConfig::RECEIVE_DECLINED_EMAIL)&.value != false
SubmitterMailer.declined_email(@submitter, user).deliver_later!
end
WebhookUrls.enqueue_events(submitter, 'form.declined')
WebhookUrls.enqueue_events(@submitter, 'form.declined')
redirect_to submit_form_path(submitter.slug)
redirect_to submit_form_path(@submitter.slug)
end
private
def declining_disabled?
@submitter.account.account_configs.find_by(key: AccountConfig::ALLOW_TO_DECLINE_KEY)&.value == false
end
def load_submitter
@submitter = Submitter.find_by!(slug: params[:submit_form_slug])
end
end
@@ -0,0 +1,50 @@
# frozen_string_literal: true
class SubmitFormDelegateController < ApplicationController
skip_before_action :authenticate_user!
skip_authorization_check
before_action :load_submitter
def create
return redirect_to submit_form_path(@submitter.slug) if @submitter.declined_at? ||
@submitter.completed_at? ||
@submitter.submission.archived_at? ||
@submitter.submission.expired? ||
@submitter.submission.template&.archived_at? ||
@submitter.viewer? ||
!Submitters::AuthorizedForForm.call(@submitter,
current_user,
request,
with_order: true)
@submitter.account.account_configs.find_by!(key: AccountConfig::ALLOW_TO_DELEGATE_KEY, value: true)
email = Submissions.normalize_email(params[:email])
return redirect_to submit_form_path(@submitter.slug) if email.blank?
old_slug = @submitter.slug
ApplicationRecord.transaction do
@submitter.submitter_versions.create!(slug: old_slug, email: @submitter.email,
name: @submitter.name, phone: @submitter.phone)
SubmissionEvents.create_with_tracking_data(@submitter, 'delegate_form', request,
{ old_email: @submitter.email, email: })
@submitter.update!(email:, phone: nil, name: nil, slug: SecureRandom.base58(14),
values: Submitters.fetch_values_for_delegate(@submitter))
end
SendSubmitterInvitationEmailJob.perform_async('submitter_id' => @submitter.id)
redirect_to submit_form_delegated_path(old_slug)
end
private
def load_submitter
@submitter = Submitter.find_by!(slug: params[:submit_form_slug])
end
end
@@ -9,15 +9,18 @@ class SubmitFormDownloadController < ApplicationController
def index
@submitter = Submitter.find_by!(slug: params[:submit_form_slug])
return redirect_to submitter_download_index_path(@submitter.slug) if @submitter.completed_at?
return redirect_to submit_form_documents_path(@submitter.slug) if @submitter.completed_at?
return head :unprocessable_content if @submitter.declined_at? ||
@submitter.submission.archived_at? ||
@submitter.submission.expired? ||
@submitter.submission.completed_at? ||
@submitter.submission.template&.archived_at? ||
AccountConfig.exists?(account_id: @submitter.account_id,
key: AccountConfig::ALLOW_TO_PARTIAL_DOWNLOAD_KEY,
value: false)
value: false) ||
!Submitters::AuthorizedForForm.call(@submitter, current_user, request,
with_order: true)
last_completed_submitter = @submitter.submission.submitters
.where.not(id: @submitter.id)
@@ -32,7 +35,7 @@ class SubmitFormDownloadController < ApplicationController
end
urls = attachments.map do |attachment|
ActiveStorage::Blob.proxy_url(attachment.blob, expires_at: FILES_TTL.from_now.to_i)
ActiveStorage::Blob.proxy_path(attachment.blob, expires_at: FILES_TTL.from_now.to_i)
end
render json: urls
@@ -12,7 +12,10 @@ class SubmitFormDrawSignatureController < ApplicationController
return redirect_to submit_form_completed_path(@submitter.slug) if @submitter.completed_at?
if @submitter.submission.template&.archived_at? || @submitter.submission.archived_at?
return redirect_to submit_form_path(@submitter.slug) if @submitter.viewer?
if @submitter.submission.template&.archived_at? || @submitter.submission.archived_at? ||
!Submitters::AuthorizedForForm.call(@submitter, current_user, request, with_order: true)
return redirect_to submit_form_path(@submitter.slug)
end
@@ -8,19 +8,17 @@ class SubmitFormEmail2fasController < ApplicationController
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(':')
value = [@submitter.email.downcase.squish, @submitter.slug].join(':')
if EmailVerificationCodes.verify(params[:one_time_code].to_s.gsub(/\D/, ''), value)
if OtpChallenges.verify(params[:one_time_code].to_s.gsub(/\D/, ''), value, purpose: 'form_email_2fa')
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 }
{ value: @submitter.slug, expires: Submitters::StartForm::COOKIES_TTL.from_now,
**Submitters::StartForm::COOKIES_DEFAULTS }
redirect_to submit_form_path(@submitter.slug)
else
@@ -37,6 +35,12 @@ class SubmitFormEmail2fasController < ApplicationController
RateLimit.call("send-email-code-#{@submitter.id}", limit: 2, ttl: 45.seconds, enabled: true)
if Docuseal.multitenant? && Submitters.email_bounced_recently?(@submitter.email)
Rollbar.warning("Bounced OTP email for submitter: #{@submitter.id}") if defined?(Rollbar)
return redirect_to submit_form_path(@submitter.slug, status: :error), alert: I18n.t(:verification_email_bounced)
end
SendSubmitterVerificationEmailJob.perform_async('submitter_id' => @submitter.id, 'locale' => I18n.locale.to_s)
redir_params = params[:resend] ? { alert: I18n.t(:code_has_been_resent) } : {}
@@ -5,12 +5,12 @@ class SubmitFormInviteController < ApplicationController
skip_authorization_check
def create
submitter = Submitter.find_by!(slug: params[:submit_form_slug])
@submitter = Submitter.find_by!(slug: params[:submit_form_slug])
return head :unprocessable_content unless can_invite?(submitter)
return head :unprocessable_content unless can_invite?(@submitter)
invite_submitters = filter_invite_submitters(submitter, 'invite_by_uuid')
optional_invite_submitters = filter_invite_submitters(submitter, 'optional_invite_by_uuid')
invite_submitters = filter_invite_submitters(@submitter, 'invite_by_uuid')
optional_invite_submitters = filter_invite_submitters(@submitter, 'optional_invite_by_uuid')
ApplicationRecord.transaction do
(invite_submitters + optional_invite_submitters).each do |item|
@@ -19,18 +19,20 @@ class SubmitFormInviteController < ApplicationController
next unless attrs
next if attrs[:email].blank?
submitter.submission.submitters.create!(**attrs, account_id: submitter.account_id)
email = Submissions.normalize_email(attrs[:email])
SubmissionEvents.create_with_tracking_data(submitter, 'invite_party', request, { uuid: submitter.uuid })
@submitter.submission.submitters.create!(uuid: attrs[:uuid], email:, account_id: @submitter.account_id)
SubmissionEvents.create_with_tracking_data(@submitter, 'invite_party', request, { uuid: attrs[:uuid] })
end
submitter.submission.update!(submitters_order: :preserved)
@submitter.submission.update!(submitters_order: :preserved)
end
submitter.submission.submitters.reload
@submitter.submission.submitters.reload
if invite_submitters.all? { |s| submitter.submission.submitters.any? { |e| e.uuid == s['uuid'] } }
Submitters::SubmitValues.call(submitter, ActionController::Parameters.new(completed: 'true'), request)
if invite_submitters.all? { |s| @submitter.submission.submitters.any? { |e| e.uuid == s['uuid'] } }
Submitters::SubmitValues.call(@submitter, ActionController::Parameters.new(completed: 'true'), request)
head :ok
else
@@ -45,7 +47,9 @@ class SubmitFormInviteController < ApplicationController
!submitter.completed_at? &&
!submitter.submission.archived_at? &&
!submitter.submission.expired? &&
!submitter.submission.template&.archived_at?
!submitter.submission.template&.archived_at? &&
!submitter.viewer? &&
Submitters::AuthorizedForForm.call(submitter, current_user, request)
end
def filter_invite_submitters(submitter, key = 'invite_by_uuid')
@@ -0,0 +1,44 @@
# frozen_string_literal: true
class SubmitFormMetadataController < ApplicationController
skip_before_action :authenticate_user!
skip_authorization_check
def index
@submitter = Submitter.find_by!(slug: params[:submit_form_slug])
return head :not_found unless authorized_submitter?(@submitter)
submission = @submitter.submission
values = submission.submitters.reduce({}) { |acc, sub| acc.merge(sub.values) }
schema = Submissions.filtered_conditions_schema(submission, values:, include_submitter_uuid: @submitter.uuid)
documents = schema.filter_map do |item|
submission.schema_documents.find { |a| a.uuid == item['attachment_uuid'] }
end
ActiveRecord::Associations::Preloader.new(records: documents, associations: :blob).call
text_runs = documents.to_h do |document|
[
document.uuid,
DocumentMetadatas.find_or_create_for_document(document, account_id: submission.account_id).text_runs
]
end
render json: { text_runs: }
end
private
def authorized_submitter?(submitter)
!submitter.declined_at? &&
!submitter.completed_at? &&
!submitter.submission.archived_at? &&
!submitter.submission.completed_at? &&
!submitter.submission.expired? &&
!submitter.submission.template&.archived_at? &&
!submitter.account.archived_at? &&
Submitters::AuthorizedForForm.call(submitter, current_user, request, with_order: true)
end
end
@@ -0,0 +1,33 @@
# frozen_string_literal: true
class SubmitFormPrefillAttachmentsController < ActionController::API
include ActiveStorage::SetCurrent
def create
@submitter = Submitter.find_by!(slug: params[:submit_form_slug])
unless can_upload?(@submitter)
return render json: { error: I18n.t('form_has_been_archived') }, status: :unprocessable_content
end
uuid, slug = ApplicationRecord.signed_id_verifier.verify(params[:prefill_token], purpose: :prefill_attachment)
raise ActiveRecord::RecordNotFound if slug != @submitter.slug
source_attachment = ActiveStorage::Attachment.find_by!(uuid:)
attachment = @submitter.attachments_attachments.find_or_create_by!(blob_id: source_attachment.blob_id)
render json: attachment.as_json(only: %i[uuid created_at], methods: %i[url filename content_type])
end
private
def can_upload?(submitter)
!submitter.declined_at? &&
!submitter.completed_at? &&
!submitter.submission.archived_at? &&
!submitter.submission.expired? &&
!submitter.submission.template&.archived_at?
end
end
@@ -0,0 +1,86 @@
# frozen_string_literal: true
class SubmitFormUploadController < ActionController::API
include ActionController::Cookies
include ActiveStorage::SetCurrent
before_action :load_submitter
before_action :authorize_upload!
COOKIE_STORE_LIMIT = 10
def create
file = params[:file]
if params[:type].in?(%w[initials signature])
image = ImageUtils.load_vips(file.read, content_type: file.content_type)
if ImageUtils.blank?(image)
Rollbar.error("Empty signature: #{@submitter.id}") if defined?(Rollbar)
return render json: { error: "#{params[:type]} is empty" }, status: :unprocessable_content
end
if ImageUtils.error?(image)
Rollbar.error("Error signature: #{@submitter.id}") if defined?(Rollbar)
return render json: { error: "#{params[:type]} error, try to sign on another device" },
status: :unprocessable_content
end
metadata = { analyzed: true, identified: true, width: image.width, height: image.height }
end
attachment = Submitters.create_attachment!(@submitter, file, metadata:)
if params[:remember_signature] == 'true' && @submitter.email.present?
cookies.permanent.encrypted[:signature_uuids] = build_new_cookie_signatures_json(@submitter, attachment)
end
render json: attachment.as_json(only: %i[uuid created_at], methods: %i[url filename content_type])
rescue Submitters::MaliciousFileExtension => e
Rollbar.error(e) if defined?(Rollbar)
render json: { error: e.message }, status: :unprocessable_content
end
private
def load_submitter
@submitter = Submitter.find_by!(slug: params[:submit_form_slug])
end
def authorize_upload!
unless can_upload?(@submitter)
return render json: { error: I18n.t('form_has_been_archived') }, status: :unprocessable_content
end
return if Submitters::AuthorizedForForm.call(@submitter, current_user, request)
render json: { error: I18n.t('verification_required_refresh_the_page_and_pass_2fa') },
status: :unprocessable_content
end
def can_upload?(submitter)
!submitter.declined_at? &&
!submitter.completed_at? &&
!submitter.submission.archived_at? &&
!submitter.submission.expired? &&
!submitter.submission.template&.archived_at?
end
def build_new_cookie_signatures_json(submitter, attachment)
values =
begin
JSON.parse(cookies.encrypted[:signature_uuids].presence || '{}')
rescue JSON::ParserError
{}
end
values[submitter.email] = attachment.uuid
values = values.to_a.last(COOKIE_STORE_LIMIT).to_h if values.size > COOKIE_STORE_LIMIT
values.to_json
end
end
@@ -5,19 +5,21 @@ class SubmitFormValuesController < ApplicationController
skip_authorization_check
def index
submitter = Submitter.find_by!(slug: params[:submit_form_slug])
@submitter = Submitter.find_by!(slug: params[:submit_form_slug])
return render json: {} if submitter.completed_at? || submitter.declined_at?
return render json: {} if submitter.submission.template&.archived_at? ||
submitter.submission.archived_at? ||
submitter.submission.expired?
return render json: {} if @submitter.completed_at? ||
@submitter.declined_at? ||
@submitter.submission.template&.archived_at? ||
@submitter.submission.archived_at? ||
@submitter.submission.expired? ||
!Submitters::AuthorizedForForm.call(@submitter, current_user, request)
value = submitter.values[params['field_uuid']]
attachment = submitter.attachments.where(created_at: params[:after]..).find_by(uuid: value) if value.present?
value = @submitter.values[params['field_uuid']]
attachment = @submitter.attachments.where(created_at: params[:after]..).find_by(uuid: value) if value.present?
render json: {
value:,
attachment: attachment&.as_json(only: %i[uuid created_at], methods: %i[url filename content_type])
}, head: :ok
}
end
end
@@ -0,0 +1,18 @@
# frozen_string_literal: true
class SubmitFormViewController < ActionController::API
def create
@submitter = Submitter.find_by!(slug: params[:submit_form_slug])
return head :forbidden if Submitter.signed_id_verifier.verified(params[:v], purpose: :view_form) != @submitter.slug
@submitter.opened_at = Time.current
@submitter.save
SubmissionEvents.create_with_tracking_data(@submitter, 'view_form', request)
WebhookUrls.enqueue_events(@submitter, 'form.viewed')
render json: {}
end
end
@@ -7,27 +7,36 @@ class SubmittersAutocompleteController < ApplicationController
LIMIT = 100
def index
submitters = search_submitters(@submitters)
field = SELECT_COLUMNS.find { |c| c == params[:field] }
submitters = search_submitters(@submitters, field)
arel_columns = SELECT_COLUMNS.map { |col| Submitter.arel_table[col] }
values = submitters.limit(LIMIT).group(arel_columns).pluck(arel_columns)
values =
if field
max_ids = submitters.group(field).limit(LIMIT).select(Submitter.arel_table[:id].maximum)
submitters.where(id: max_ids).order(id: :desc).pluck(arel_columns)
else
submitters.limit(LIMIT).group(arel_columns).pluck(arel_columns)
end
attrs = values.map { |row| SELECT_COLUMNS.zip(row).to_h }
attrs = attrs.uniq { |e| e[params[:field]] } if params[:field].present?
render json: attrs
end
private
def search_submitters(submitters)
if SELECT_COLUMNS.include?(params[:field])
def search_submitters(submitters, field)
if field
if Docuseal.fulltext_search?
Submitters.fulltext_search_field(current_user, submitters, params[:q], params[:field])
Submitters.fulltext_search_field(current_user, submitters, params[:q], field)
else
column = Submitter.arel_table[params[:field].to_sym]
column = Submitter.arel_table[field.to_sym]
term = "#{params[:q].downcase}%"
term = "#{ActiveRecord::Base.sanitize_sql_like(params[:q].downcase)}%"
submitters.where(column.matches(term))
end
+27 -2
View File
@@ -8,12 +8,14 @@ class SubmittersController < ApplicationController
if @submitter.preferences['email_message_uuid'].present?
@submitter.account.email_messages.find_by(uuid: @submitter.preferences['email_message_uuid'])
end
render :edit, layout: 'plain'
end
def update
submission = @submitter.submission
if @submitter.submission_events.exists?(event_type: 'start_form') || submission.archived_at? || submission.expired?
unless submitter_editable?(submission)
return redirect_back fallback_location: submission_path(submission), alert: I18n.t('submitter_cannot_be_updated')
end
@@ -48,11 +50,17 @@ class SubmittersController < ApplicationController
private
def submitter_editable?(submission)
!@submitter.submission_events.exists?(event_type: 'start_form') &&
!@submitter.completed_at? && !@submitter.declined_at? && !submission.completed_at? &&
!submission.archived_at? && !submission.expired? && !submission.template&.archived_at?
end
def maybe_resend_email_sms(submitter, params)
if params[:send_email] == '1' && submitter.email.present?
is_sent_recently = Docuseal.multitenant? &&
EmailEvent.exists?(email: submitter.email,
tag: 'submitter_invitation',
tag: %w[submitter_invitation submitter_view_invitation],
emailable: submitter,
event_type: 'send',
created_at: 4.hours.ago..Time.current)
@@ -73,9 +81,26 @@ class SubmittersController < ApplicationController
submitter.name = attrs[:name] if attrs.key?(:name)
if submitter_changed?(submitter)
submitter.submitter_versions.new(slug: submitter.slug, email: submitter.email_was,
name: submitter.name_was, phone: submitter.phone_was)
submitter.slug = SecureRandom.base58(14)
submitter.values = Submitters.fetch_values_for_delegate(submitter)
end
submitter
end
def submitter_changed?(submitter)
if submitter.email_was.blank? && submitter.phone_was.blank?
submitter.name_was.present? && submitter.name_changed?
else
(submitter.email_was.present? && submitter.email_changed?) ||
(submitter.phone_was.present? && submitter.phone_changed?)
end
end
def submitter_params
params.require(:submitter).permit(:email, :name, :phone).transform_values(&:strip)
end
@@ -0,0 +1,11 @@
# frozen_string_literal: true
class SubmittersDownloadController < ApplicationController
load_and_authorize_resource :submitter
def index
Submissions::EnsureResultGenerated.call(@submitter)
render json: Submitters.build_document_urls(@submitter)
end
end
@@ -5,6 +5,8 @@ class SubmittersResubmitController < ApplicationController
def update
return redirect_to submit_form_path(slug: @submitter.slug) if @submitter.email != current_user.email
return redirect_to submit_form_path(slug: @submitter.slug) if @submitter.completed_at.blank? ||
@submitter.completed_at < 1.month.ago
submission = @submitter.account.submissions.new(created_by_user: current_user,
submitters_order: :preserved,
@@ -1,9 +1,11 @@
# frozen_string_literal: true
class SubmittersSendEmailController < ApplicationController
load_and_authorize_resource :submitter, id_param: :submitter_slug, find_by: :slug
load_and_authorize_resource :submitter
def create
authorize!(:update, @submitter)
if Docuseal.multitenant? && SubmissionEvent.exists?(submitter: @submitter,
event_type: 'send_email',
created_at: 10.hours.ago..Time.current)
@@ -6,17 +6,29 @@ class TemplateDocumentsController < ApplicationController
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) }
documents = @template.schema.map do |item|
if item['dynamic']
dynamic_document = @template.schema_dynamic_documents.find { |e| e.uuid == item['attachment_uuid'] }
DynamicDocuments::EnsureVersionGenerated.call(dynamic_document).document_attachment
else
@template.schema_documents.find { |e| e.uuid == item['attachment_uuid'] }
end
end
render json: documents.map { |d| ActiveStorage::Blob.proxy_path(d.blob, expires_at: FILES_TTL.from_now.to_i) }
end
def create
authorize!(:update, @template)
if params[:blobs].blank? && params[:files].blank?
return render json: { error: I18n.t('file_is_missing') }, status: :unprocessable_content
end
old_fields_hash = @template.fields.hash
documents = Templates::CreateAttachments.call(@template, params, extract_fields: true)
documents, = Templates::CreateAttachments.call(@template, params, extract_fields: true)
schema = documents.map do |doc|
{ attachment_uuid: doc.uuid, name: doc.filename.base }
@@ -27,7 +39,7 @@ class TemplateDocumentsController < ApplicationController
fields: old_fields_hash == @template.fields.hash ? nil : @template.fields,
submitters: old_fields_hash == @template.fields.hash ? nil : @template.submitters,
documents: documents.as_json(
methods: %i[metadata signed_uuid],
methods: %i[metadata signed_key],
include: {
preview_images: { methods: %i[url metadata filename] }
}
@@ -0,0 +1,39 @@
# frozen_string_literal: true
class TemplateDocumentsCropController < ApplicationController
load_and_authorize_resource :template
before_action :load_attachment
rescue_from Leptonica::LeptonicaError do
render json: { error: I18n.t(:unable_to_save) }, status: :unprocessable_content
end
def index
render json: { corners: Leptonica.detect_document_corners(@attachment.download) }
end
def create
authorize!(:update, @template)
document = Templates::CreateDocumentCrop.call(@template, @attachment, crop_params)
render json: {
document: document.as_json(
methods: %i[metadata signed_key],
include: {
preview_images: { methods: %i[url metadata filename] }
}
)
}
end
private
def load_attachment
@attachment = @template.documents_attachments.find_by!(uuid: params[:attachment_uuid])
end
def crop_params
params.permit(:scan, :rotate, :flip_h, :flip_v, corners: [%i[x y]])
end
end
@@ -0,0 +1,32 @@
# frozen_string_literal: true
class TemplateDocumentsModifyController < ApplicationController
load_and_authorize_resource :template
def create
authorize!(:update, @template)
documents_layout =
params.require(:documents).map do |item|
item.permit(:attachment_uuid,
pages: [:attachment_uuid, :page, :rotate,
{ redact: [%i[x y w h color]], replaced_page: %i[attachment_uuid page] }]).to_h
end
Templates::ModifyDocuments.call(@template, documents_layout)
render json: {
schema: @template.schema,
fields: @template.fields,
submitters: @template.submitters,
documents: @template.schema_documents.reload.preload(:blob, preview_images_attachments: :blob).as_json(
methods: %i[metadata signed_key],
include: {
preview_images: { methods: %i[url metadata filename] }
}
)
}
rescue Templates::ModifyDocuments::InvalidLayout
render json: { error: I18n.t(:unable_to_save) }, status: :unprocessable_content
end
end
@@ -0,0 +1,11 @@
# frozen_string_literal: true
class TemplateDocumentsPageObjectsController < ApplicationController
load_and_authorize_resource :template
def index
attachment = @template.documents_attachments.find_by!(uuid: params[:attachment_uuid])
render json: Templates::ModifyDocuments.page_objects(attachment, params[:page].to_i)
end
end
+36 -15
View File
@@ -13,10 +13,7 @@ class TemplateFoldersController < ApplicationController
.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 = filter_folders(@template_folder)
@template_folders = TemplateFolders.sort(@template_folders, current_user, selected_order)
if @templates.exists?
@@ -30,14 +27,18 @@ class TemplateFoldersController < ApplicationController
(@template_folders.size < 7 ? 9 : 6)
end
@pagy, @templates = pagy_auto(@templates, limit:)
@pagy, @templates = pagy_auto(@templates.select_for_list, limit:)
load_related_submissions if params[:q].present? && @templates.blank?
if params[:q].present? && @templates.blank?
@related_submissions_pagy, @related_submissions = load_related_submissions(@template_folder)
end
else
@pagy, @template_folders = pagy(@template_folders, limit: FOLDERS_PER_PAGE)
@templates = @templates.none
end
render_infinite_scroll(@pagy, @templates, @template_folders) if turbo_infinite_scroll?
end
def edit; end
@@ -53,13 +54,33 @@ class TemplateFoldersController < ApplicationController
private
def filter_folders(template_folder)
template_folders =
template_folder.subfolders.where(id: Template.accessible_by(current_ability).active.select(:folder_id))
TemplateFolders.search(template_folders, params[:q])
end
def render_infinite_scroll(pagy, templates, template_folders)
render_params =
if templates.present?
{ partial: 'templates/template', collection: templates }
else
{ partial: 'template_folders/folder', collection: template_folders, as: :folder }
end
render turbo_stream: [
turbo_stream.before('infinite_scroll', **render_params),
turbo_stream.replace('infinite_scroll', partial: 'shared/infinite_scroll', locals: { pagy: })
]
end
def selected_order
@selected_order ||=
if cookies.permanent[:dashboard_templates_order].blank? ||
(cookies.permanent[:dashboard_templates_order] == 'used_at' && can?(:manage, :countless))
if can?(:manage, :countless)
'created_at'
else
cookies.permanent[:dashboard_templates_order]
cookies.permanent[:dashboard_templates_order].presence || 'created_at'
end
end
@@ -67,20 +88,20 @@ class TemplateFoldersController < ApplicationController
params.require(:template_folder).permit(:name)
end
def load_related_submissions
@related_submissions =
def load_related_submissions(template_folder)
related_submissions =
Submission.accessible_by(current_ability)
.where(archived_at: nil)
.where(template_id: current_account.templates.active
.where(folder: [@template_folder, *@template_folder.subfolders])
.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 = Submissions.search(current_user, related_submissions, params[:q])
.order(id: :desc)
@related_submissions_pagy, @related_submissions = pagy_auto(@related_submissions, limit: 5)
pagy_auto(related_submissions.select_for_list, limit: 5)
end
end
@@ -8,7 +8,9 @@ class TemplateSharingsTestingController < ApplicationController
end
def create
testing_account = Accounts.find_or_create_testing_user(true_user.account).account
account = can?(:manage, :tenants_test) ? current_account : true_user.account
testing_account = Accounts.find_or_create_testing_user(account).account
if params[:value] == '1'
TemplateSharing.create!(ability: :manage, account: testing_account, template: @template)
@@ -10,11 +10,27 @@ class TemplatesArchivedController < ApplicationController
@templates = Templates.search(current_user, @templates, params[:q])
@pagy, @templates = pagy_auto(@templates, limit: 12)
@pagy, @templates = pagy_auto(@templates.select_for_list, limit: 12)
return unless params[:q].present? && @templates.blank?
if params[:q].present? && @templates.blank?
@related_submissions_pagy, @related_submissions = load_related_submissions
end
@related_submissions =
render_infinite_scroll(@pagy, @templates) if turbo_infinite_scroll?
end
private
def render_infinite_scroll(pagy, templates)
render turbo_stream: [
turbo_stream.before('infinite_scroll', partial: 'templates/template', collection: templates,
locals: { with_folder: true }),
turbo_stream.replace('infinite_scroll', partial: 'shared/infinite_scroll', locals: { pagy: })
]
end
def load_related_submissions
related_submissions =
Submission.accessible_by(current_ability)
.joins(:template)
.where.not(templates: { archived_at: nil })
@@ -22,9 +38,9 @@ class TemplatesArchivedController < ApplicationController
template: :author,
submitters: :start_form_submission_events)
@related_submissions = Submissions.search(current_user, @related_submissions, params[:q])
.order(id: :desc)
related_submissions = Submissions.search(current_user, related_submissions, params[:q])
.order(id: :desc)
@related_submissions_pagy, @related_submissions = pagy_auto(@related_submissions, limit: 5)
pagy_auto(related_submissions.select_for_list, limit: 5)
end
end
@@ -10,13 +10,25 @@ class TemplatesArchivedSubmissionsController < ApplicationController
@submissions = Submissions::Filter.call(@submissions, current_user, params)
@submissions = if params[:completed_at_from].present? || params[:completed_at_to].present?
@submissions.order(Submitter.arel_table[:completed_at].maximum.desc)
@submissions.order(completed_at: :desc)
else
@submissions.order(id: :desc)
end
@pagy, @submissions = pagy_auto(@submissions.preload(submitters: :start_form_submission_events))
@pagy, @submissions = pagy_auto(@submissions.select_for_list.preload(submitters: :start_form_submission_events))
render_infinite_scroll(@pagy, @submissions, @template) if turbo_infinite_scroll?
rescue ActiveRecord::RecordNotFound
redirect_to root_path
end
private
def render_infinite_scroll(pagy, submissions, template)
render turbo_stream: [
turbo_stream.before('infinite_scroll', partial: 'templates/submission', collection: submissions,
locals: { template:, archived: true }),
turbo_stream.replace('infinite_scroll', partial: 'shared/infinite_scroll', locals: { pagy: })
]
end
end
@@ -8,11 +8,14 @@ class TemplatesCloneAndReplaceController < ApplicationController
ActiveRecord::Associations::Preloader.new(
records: [@template],
associations: [schema_documents: :preview_images_attachments]
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, '.*')
authorize!(:create, cloned_template)
cloned_template.save!
documents = Templates::ReplaceAttachments.call(cloned_template, params, extract_fields: true)
@@ -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.can?(: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
+31 -63
View File
@@ -3,8 +3,6 @@
class TemplatesController < ApplicationController
load_and_authorize_resource :template
before_action :load_base_template, only: %i[new create]
def show
submissions = @template.submissions.accessible_by(current_ability)
submissions = submissions.active if @template.archived_at.blank?
@@ -16,69 +14,42 @@ class TemplatesController < ApplicationController
submissions = Submissions::Filter.filter_by_status(submissions, params)
submissions = if params[:completed_at_from].present? || params[:completed_at_to].present?
submissions.order(Submitter.arel_table[:completed_at].maximum.desc)
submissions.order(completed_at: :desc)
else
submissions.order(id: :desc)
end
@pagy, @submissions = pagy_auto(submissions.preload(:template_accesses, submitters: :start_form_submission_events))
@pagy, @submissions =
pagy_auto(submissions.select_for_list.preload(:template_accesses, submitters: :start_form_submission_events))
render_infinite_scroll(@pagy, @submissions, @template) if turbo_infinite_scroll?
rescue ActiveRecord::RecordNotFound
redirect_to root_path
end
def new
@template.name = "#{@base_template.name} (#{I18n.t('clone')})" if @base_template
render :new, layout: 'plain'
end
def edit
ActiveRecord::Associations::Preloader.new(
records: [@template],
associations: [schema_documents: [:blob, { 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
@template_data = Templates.serialize_for_builder(@template)
render :edit, layout: 'plain'
end
def create
if @base_template
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])
else
@template.author = current_user
@template.folder = TemplateFolders.find_or_create_by_name(current_user, params[:folder_name])
end
if params[:account_id].present? && authorized_clone_account_id?(params[:account_id])
@template.account_id = params[:account_id]
@template.folder = @template.account.default_template_folder if @template.account_id != current_account.id
else
@template.account = current_account
end
@template.author = current_user
@template.folder = TemplateFolders.find_or_create_by_name(current_user, params[:folder_name])
@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)
WebhookUrls.enqueue_events(@template, 'template.created')
maybe_redirect_to_template(@template)
redirect_to(edit_template_path(@template))
else
render turbo_stream: turbo_stream.replace(:modal, template: 'templates/new'), status: :unprocessable_content
end
@@ -107,20 +78,36 @@ class TemplatesController < ApplicationController
else
@template.update!(archived_at: Time.current)
WebhookUrls.enqueue_events(@template, 'template.archived')
I18n.t('template_has_been_archived')
end
redirect_back(fallback_location: root_path, notice:)
if @template.destroyed? && turbo_native_app?
redirect_to templates_archived_index_path, notice:
else
redirect_back(fallback_location: root_path, notice:)
end
end
private
def render_infinite_scroll(pagy, submissions, template)
render turbo_stream: [
turbo_stream.before('infinite_scroll', partial: 'templates/submission', collection: submissions,
locals: { template:, archived: template.archived_at? }),
turbo_stream.replace('infinite_scroll', partial: 'shared/infinite_scroll', locals: { pagy: })
]
end
def template_params
params.require(:template).permit(
:name,
{ schema: [[:attachment_uuid, :google_drive_file_id, :name,
{ schema: [[:attachment_uuid, :google_drive_file_id, :name, :dynamic,
{ 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]],
submitters: [%i[name uuid is_requester linked_to_uuid invite_via_field_uuid
invite_by_uuid optional_invite_by_uuid email order]],
variables_schema: {},
fields: [[:uuid, :submitter_uuid, :name, :type,
:required, :readonly, :default_value,
:title, :description, :prefillable,
@@ -129,26 +116,7 @@ class TemplatesController < ApplicationController
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]] }]] }
areas: [%i[uuid x y w h cell_w attachment_uuid option_uuid page]] }]] }
)
end
def authorized_clone_account_id?(account_id)
true_user.account_id.to_s == account_id.to_s ||
true_user.account.linked_accounts.accessible_by(current_ability).exists?(id: account_id)
end
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
def load_base_template
return if params[:base_template_id].blank?
@base_template = Template.accessible_by(current_ability).find_by(id: params[:base_template_id])
end
end
+105 -49
View File
@@ -11,86 +11,142 @@ class TemplatesDashboardController < ApplicationController
helper_method :selected_order
def index
@default_folder = current_account.default_template_folder
@template_folders =
TemplateFolders.filter_active_folders(@template_folders.where(parent_folder_id: nil), @templates)
@template_folders = @template_folders.where.not(id: @default_folder.id) if params[:q].blank?
@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
)
@shared_templates = Templates.shared(current_user).active
@pagy, @template_folders, @show_default_folder, @show_shared_folder, @show_shared_inline =
load_folders(@template_folders, @templates, @shared_templates)
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)
if @show_shared_inline
@templates = @shared_templates.preload(:template_sharings)
else
@templates = @templates.active
@templates = @templates.where(folder_id: @default_folder.id) if params[:q].blank?
end
limit =
if @template_folders.size < 4
TEMPLATES_PER_PAGE
else
(@template_folders.size < 7 ? 9 : 6)
end
@pagy, @templates = load_templates(@templates.select_for_list, @pagy.count,
show_shared_inline: @show_shared_inline)
@pagy, @templates = pagy_auto(@templates, limit:)
load_related_submissions if params[:q].present? && @templates.blank?
if params[:q].present? && @templates.blank?
@related_submissions_pagy, @related_submissions = load_related_submissions
end
end
render_infinite_scroll(@pagy, @templates, @template_folders) if turbo_infinite_scroll?
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)
)
)
def render_infinite_scroll(pagy, templates, template_folders)
render_params =
if templates.present?
{ partial: 'templates/template', collection: templates }
else
rel = rel.where(folder_id: current_account.default_template_folder.id)
{ partial: 'template_folders/folder', collection: template_folders, as: :folder }
end
end
Templates.search(current_user, rel, params[:q])
render turbo_stream: [
turbo_stream.before('infinite_scroll', **render_params),
turbo_stream.replace('infinite_scroll', partial: 'shared/infinite_scroll', locals: { pagy: })
]
end
def load_templates(templates, folders_count, show_shared_inline: false)
templates = templates.preload(:author, :template_accesses)
templates =
if show_shared_inline
Templates.search_shared(current_user, templates, params[:q])
else
Templates.search(current_user, templates, params[:q])
end
templates = Templates::Order.call(templates, current_user, selected_order)
limit =
if folders_count < 4
TEMPLATES_PER_PAGE
else
(folders_count < 7 ? 9 : 6)
end
pagy_auto(templates, limit:)
end
def load_folders(template_folders, templates, shared_templates)
if params[:q].present?
pagy(template_folders, limit: FOLDERS_PER_PAGE,
page: template_folders.count > SHOW_TEMPLATES_FOLDERS_THRESHOLD ? params[:page] : 1)
else
load_folders_with_pinned(template_folders, templates, shared_templates)
end
end
def load_folders_with_pinned(template_folders, templates, shared_templates)
folders_count = template_folders.count
shared_exists = shared_templates.exists?
default_has_templates = templates.active.exists?(folder_id: current_account.default_template_folder.id)
show_inline_folders =
folders_count + (shared_exists && default_has_templates ? 1 : 0) <= SHOW_TEMPLATES_FOLDERS_THRESHOLD
show_shared_inline = shared_exists && !default_has_templates && show_inline_folders
show_shared_in_grid = shared_exists && !show_shared_inline
show_default_in_grid = !show_inline_folders && default_has_templates
pinned_count = (show_default_in_grid ? 1 : 0) + (show_shared_in_grid ? 1 : 0)
pagy, = pagy(template_folders, count: folders_count + pinned_count,
page: show_inline_folders ? 1 : [params[:page].to_s.to_i, 1].max,
limit: FOLDERS_PER_PAGE,
raise_range_error: true)
show_default_folder = show_default_in_grid && pagy.page == 1
show_shared_folder = show_shared_in_grid && pagy.page == 1
folder_offset = pagy.page == 1 ? 0 : pagy.offset - pinned_count
folder_limit = pagy.page == 1 ? pagy.limit - pinned_count : pagy.limit
template_folders = template_folders.offset(folder_offset).limit(folder_limit)
[pagy, template_folders, show_default_folder, show_shared_folder, show_shared_inline]
end
def selected_order
@selected_order ||=
if cookies.permanent[:dashboard_templates_order].blank? ||
(cookies.permanent[:dashboard_templates_order] == 'used_at' && can?(:manage, :countless))
if can?(:manage, :countless)
'created_at'
else
cookies.permanent[:dashboard_templates_order]
cookies.permanent[:dashboard_templates_order].presence || 'created_at'
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 = 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 = Submissions.search(current_user, related_submissions, params[:q])
.order(id: :desc)
@related_submissions_pagy, @related_submissions = pagy_auto(@related_submissions, limit: 5)
pagy_auto(related_submissions.select_for_list, limit: 5)
end
end
+25 -15
View File
@@ -9,32 +9,35 @@ class TemplatesDebugController < ApplicationController
schema_uuids = @template.schema.index_by { |e| e['attachment_uuid'] }
attachment = @template.documents.find { |a| schema_uuids[a.uuid] }
data = attachment.download
if attachment
data = attachment.download
unless attachment.image?
pdf = HexaPDF::Document.new(io: StringIO.new(data))
unless attachment.image?
fields =
Pdfium::Document.open_io(StringIO.new(data)) do |doc|
Templates::FindPdfiumAcroFields.call(attachment, doc, data)
end
end
fields = Templates::FindAcroFields.call(pdf, attachment, data)
# 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]))
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 }]
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],
methods: %i[metadata signed_key],
include: { preview_images: { methods: %i[url metadata filename] } }
)
).to_json
@@ -58,9 +61,16 @@ class TemplatesDebugController < ApplicationController
params = { files: [file] }
documents = Templates::CreateAttachments.call(@template, params)
documents, dynamic_documents = Templates::CreateAttachments.call(@template, params,
dynamic: DEBUG_FILE.ends_with?('.docx'))
schema = documents.map { |doc| { attachment_uuid: doc.uuid, name: doc.filename.base } }
schema = documents.map do |doc|
{
attachment_uuid: doc.uuid,
name: doc.filename.base,
dynamic: dynamic_documents.find { |e| e.uuid == doc.uuid }.present?
}
end
@template.update!(schema:)
end
@@ -10,12 +10,26 @@ class TemplatesDetectFieldsController < ApplicationController
sse = SSE.new(response.stream)
documents = @template.schema_documents.preload(:blob)
documents =
if params[:attachment_uuid].present?
@template.documents.where(uuid: params[:attachment_uuid])
else
@template.schema_documents
end
documents = documents.preload(:blob)
page_number = params[:page].presence&.to_i
documents.each do |document|
io = StringIO.new(document.download)
io =
if document.image?
StringIO.new(document.preview_images.joins(:blob).find_by(blob: { filename: ['0.png', '0.jpg'] }).download)
else
StringIO.new(document.download)
end
Templates::DetectFields.call(io, attachment: document) do |(attachment_uuid, page, fields)|
Templates::DetectFields.call(io, attachment: document, page_number:) do |(attachment_uuid, page, fields)|
sse.write({ attachment_uuid:, page:, fields: })
end
end
@@ -6,6 +6,8 @@ class TemplatesFoldersController < ApplicationController
def edit; end
def update
authorize!(:update, @template)
name = [params[:parent_name], params[:name]].compact_blank.join(' / ')
@template.folder = TemplateFolders.find_or_create_by_name(current_user, name)
@@ -13,11 +13,13 @@ class TemplatesFormPreviewController < ApplicationController
@submitter.submission.submitters = @template.submitters.map { |item| Submitter.new(uuid: item['uuid']) }
Submissions::CreateFromSubmitters.maybe_set_dynamic_documents(@submitter.submission, preview: true)
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)
@form_configs = Submitters::FormConfigs.call(@submitter, SubmitFormController::CONFIG_KEYS)
end
end
@@ -5,6 +5,8 @@ class TemplatesPreferencesController < ApplicationController
RESETTABLE_PREFERENCE_KEYS = {
AccountConfig::SUBMITTER_INVITATION_EMAIL_KEY => %w[request_email_subject request_email_body submitters],
AccountConfig::SUBMITTER_VIEW_INVITATION_EMAIL_KEY => %w[invitation_view_email_subject
invitation_view_email_body],
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],
@@ -12,7 +14,9 @@ class TemplatesPreferencesController < ApplicationController
completed_notification_email_body]
}.freeze
def show; end
def show
render :show, layout: 'plain'
end
def create
authorize!(:update, @template)
@@ -48,18 +52,18 @@ class TemplatesPreferencesController < ApplicationController
def template_params
params.require(:template).permit(
preferences: %i[bcc_completed request_email_subject request_email_body
invitation_view_email_subject invitation_view_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
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: []]
[{ 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'
@@ -4,18 +4,7 @@ class TemplatesPreviewController < ApplicationController
load_and_authorize_resource :template
def show
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
@template_data = Templates.serialize_for_builder(@template)
render :show, layout: 'plain'
end
@@ -22,7 +22,7 @@ class TemplatesRecipientsController < ApplicationController
def submitters_params
permit_params = { submitters: [%i[name uuid is_requester optional_invite_by_uuid
invite_by_uuid linked_to_uuid email option order]] }
invite_by_uuid invite_via_field_uuid linked_to_uuid email option order]] }
params.require(:template).permit(permit_params).fetch(:submitters, {}).values.filter_map do |s|
next if s[:uuid].blank?
@@ -36,6 +36,7 @@ class TemplatesRecipientsController < ApplicationController
s[:order] = s[:order].to_i if s[:order].present?
s.delete(:invite_by_uuid) if s[:invite_by_uuid].blank?
s.delete(:optional_invite_by_uuid) if s[:optional_invite_by_uuid].blank?
s.delete(:invite_via_field_uuid) if s[:invite_via_field_uuid].blank?
normalize_option_value(s)
end
@@ -53,6 +54,7 @@ class TemplatesRecipientsController < ApplicationController
attrs.delete(:email)
attrs.delete(:linked_to_uuid)
attrs.delete(:invite_by_uuid)
attrs.delete(:invite_via_field_uuid)
attrs.delete(:optional_invite_by_uuid)
when /\Alinked_to_(.*)\z/
attrs[:linked_to_uuid] = ::Regexp.last_match(-1)
Loaded 100 of 683 files, more files were not shown because too many files have changed in this diff. Show more