Compare commits

...

585 Commits

Author SHA1 Message Date
Oleksandr Turchyn 8b035f80b3 fix i18n typos 2024-09-23 11:45:07 +03:00
Pete Matsyburka c717293306 fix form preview 2024-09-23 11:45:07 +03:00
Pete Matsyburka b3f3cf2aa6 fix label 2024-09-23 11:45:07 +03:00
Pete Matsyburka 7406334f77 adjust i18n 2024-09-23 11:45:07 +03:00
Pete Matsyburka 3e1350a221 upgrade puma 2024-09-23 11:45:07 +03:00
Pete Matsyburka 961e0a1c89 fix i18n 2024-09-23 11:45:07 +03:00
Pete Matsyburka 2ce9bbecb9 fix cells field 2024-09-23 11:45:07 +03:00
Pete Matsyburka 704da3a2ec adjust email edit button 2024-09-23 11:45:07 +03:00
Pete Matsyburka 2f530ccfca i18n emails 2024-09-23 11:45:07 +03:00
Pete Matsyburka e6c20f5ff6 adjust submit button 2024-09-23 11:45:07 +03:00
Pete Matsyburka d267978437 set emails copy duration 2024-09-23 11:45:07 +03:00
Oleksandr Turchyn e9d728fa4a add i18n 2024-09-23 11:45:07 +03:00
Pete Matsyburka 3c60fb4ee6 adjust show continue 2024-09-23 11:45:07 +03:00
Oleksandr Turchyn f1e494b669 adjust wording 2024-09-23 11:45:07 +03:00
Pete Matsyburka f7a8e4102b fix scroll 2024-09-23 11:45:07 +03:00
dependabot[bot] 311c2b0e34 Bump devise-two-factor in the bundler group across 1 directory
Bumps the bundler group with 1 update in the / directory: [devise-two-factor](https://github.com/tinfoil/devise-two-factor).


Updates `devise-two-factor` from 5.0.0 to 6.0.0
- [Changelog](https://github.com/devise-two-factor/devise-two-factor/blob/main/CHANGELOG.md)
- [Commits](https://github.com/tinfoil/devise-two-factor/compare/v5.0.0...v6.0.0)

---
updated-dependencies:
- dependency-name: devise-two-factor
  dependency-type: direct:production
  dependency-group: bundler
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-09-23 11:45:07 +03:00
Pete Matsyburka 5dd59f4f7c optimize image 2024-09-23 11:45:07 +03:00
Pete Matsyburka 571f5d8221 set font style 2024-09-23 11:45:07 +03:00
Pete Matsyburka 136b0eb5ed fix typo 2024-09-23 11:45:07 +03:00
Pete Matsyburka 2d813e184b fix padding 2024-09-16 12:11:20 +03:00
Pete Matsyburka fd23674e87 add decline reason in webhook 2024-09-16 12:11:20 +03:00
Pete Matsyburka 3b9d35acf1 fix tld autocorrection 2024-09-16 12:11:20 +03:00
Pete Matsyburka 17fc6e2f1c reduce textarea size 2024-09-16 12:11:20 +03:00
Oleksandr Turchyn 2e132d8b5a update submission modal 2024-09-16 12:11:20 +03:00
Pete Matsyburka 53bef85a63 adjust cert vars 2024-09-16 12:11:20 +03:00
Pete Matsyburka 99ad6e36ac fix preview preload 2024-09-16 12:11:20 +03:00
Pete Matsyburka bfa41ac35f concurrent preview processing 2024-09-16 12:11:20 +03:00
Pete Matsyburka bc4414a368 use png for previews 2024-09-16 12:11:20 +03:00
Pete Matsyburka 26ae5f1d80 update textarea size 2024-09-16 12:11:20 +03:00
Pete Matsyburka be4962a02a adjust form completion 2024-09-16 12:11:20 +03:00
Pete Matsyburka e72090c2e0 add template form preferences 2024-09-16 12:11:20 +03:00
Pete Matsyburka ad8bca8bf4 fix spec 2024-09-16 12:11:20 +03:00
Pete Matsyburka 22be3b37cf fix complete page 2024-09-16 12:11:20 +03:00
Pete Matsyburka cfc32ee522 set textarea max height 2024-09-16 12:11:20 +03:00
Pete Matsyburka 10fbdf6612 allow to invite party 2024-09-16 12:11:20 +03:00
Pete Matsyburka 3604fb6461 add readonly field attr 2024-09-16 12:11:20 +03:00
Pete Matsyburka 24392d0258 remove redacted 2024-09-16 12:11:20 +03:00
Pete Matsyburka 4ab1065773 adjust default recipients 2024-09-16 12:11:20 +03:00
Pete Matsyburka 50b0f7e5ed adjust default recipients 2024-09-16 12:11:20 +03:00
Pete Matsyburka be4a0505bc assign default submitters 2024-09-09 13:57:46 +03:00
Pete Matsyburka bd853c6265 html escape simple format 2024-09-09 13:57:46 +03:00
Pete Matsyburka 5a1efd0e27 add download utils 2024-09-09 13:57:46 +03:00
Pete Matsyburka 71b8cc7172 add recipient preferences 2024-09-09 13:57:46 +03:00
Pete Matsyburka 126a36dbf3 clear log 2024-09-09 13:57:46 +03:00
Pete Matsyburka 90db64cc03 fix file prefill 2024-09-09 13:57:46 +03:00
Pete Matsyburka 227a067c77 fix readonly condition 2024-09-09 13:57:46 +03:00
Pete Matsyburka dc24df0d4c remove log 2024-09-09 13:57:46 +03:00
Pete Matsyburka 2598962c5b configure image quality 2024-09-09 13:57:46 +03:00
Pete Matsyburka b4884e94f6 add preferences 2024-09-09 13:57:46 +03:00
Pete Matsyburka 4741e64178 fix specs 2024-09-02 20:33:28 +03:00
Pete Matsyburka 25fe35e212 ruby 3.3.4 2024-09-02 20:29:47 +03:00
Pete Matsyburka 92909cea09 clear base64 prefix 2024-09-02 20:11:13 +03:00
Pete Matsyburka b16322dd5b add first name var 2024-09-02 20:11:13 +03:00
dependabot[bot] a04c04141c Bump webpack in the npm_and_yarn group across 1 directory
Bumps the npm_and_yarn group with 1 update in the / directory: [webpack](https://github.com/webpack/webpack).


Updates `webpack` from 5.82.1 to 5.94.0
- [Release notes](https://github.com/webpack/webpack/releases)
- [Commits](https://github.com/webpack/webpack/compare/v5.82.1...v5.94.0)

---
updated-dependencies:
- dependency-name: webpack
  dependency-type: direct:production
  dependency-group: npm_and_yarn
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-09-02 20:11:13 +03:00
Pete Matsyburka 90f21d3ed9 add expired condition 2024-09-02 20:11:13 +03:00
Pete Matsyburka f31225e8c2 fix spec 2024-09-02 20:11:13 +03:00
Oleksandr Turchyn eead1b2bc4 add basic API specs 2024-09-02 20:11:13 +03:00
Pete Matsyburka f90a4e5576 adjust document id pdf 2024-09-02 20:11:13 +03:00
Pete Matsyburka 50987ce38b hide app url input 2024-09-02 20:11:13 +03:00
Pete Matsyburka 26113ac3a6 allow signing link with multiple signers 2024-09-02 20:11:13 +03:00
Pete Matsyburka 0b7b72ce16 clear log 2024-09-02 20:11:13 +03:00
Pete Matsyburka 95d48b2808 adjust submissions response 2024-09-02 20:11:13 +03:00
Pete Matsyburka 8fecf94a28 fix typo 2024-09-02 20:11:13 +03:00
Pete Matsyburka 66dd87b290 adjust padding 2024-09-02 20:11:13 +03:00
Pete Matsyburka d20e8cc37d remove checkboxes label 2024-09-02 20:11:13 +03:00
Pete Matsyburka c189e2ed20 clear log 2024-09-02 20:11:13 +03:00
Pete Matsyburka 57f8f45320 archived template preview 2024-09-02 20:11:13 +03:00
Pete Matsyburka 5684baf1e0 add app url var 2024-08-25 16:03:23 +03:00
Pete Matsyburka b6f307690d add combine pdf toggle 2024-08-25 16:03:23 +03:00
Pete Matsyburka 66c44f8ff5 adjust error page 2024-08-25 16:03:23 +03:00
dependabot[bot] 8f3592c481 Bump rexml from 3.3.3 to 3.3.6 in the bundler group across 1 directory
Bumps the bundler group with 1 update in the / directory: [rexml](https://github.com/ruby/rexml).


Updates `rexml` from 3.3.3 to 3.3.6
- [Release notes](https://github.com/ruby/rexml/releases)
- [Changelog](https://github.com/ruby/rexml/blob/master/NEWS.md)
- [Commits](https://github.com/ruby/rexml/compare/v3.3.3...v3.3.6)

---
updated-dependencies:
- dependency-name: rexml
  dependency-type: indirect
  dependency-group: bundler
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-08-25 16:03:23 +03:00
Pete Matsyburka dc8b1e0741 rescue error 2024-08-25 16:03:23 +03:00
Pete Matsyburka b806a6afc5 disable decline button 2024-08-25 16:03:23 +03:00
Pete Matsyburka d43f8f4dd8 add event data 2024-08-25 16:03:23 +03:00
Pete Matsyburka 37c9bf5741 add reply to 2024-08-25 16:03:23 +03:00
Pete Matsyburka a0d37f516f add audit document id 2024-08-25 16:03:23 +03:00
Pete Matsyburka 2764a18252 decline submission 2024-08-25 16:03:23 +03:00
Pete Matsyburka 96cf228e74 fix upload signature 2024-08-25 16:03:23 +03:00
Pete Matsyburka ff6ff3a51e adjust template resp 2024-08-25 16:03:23 +03:00
Pete Matsyburka 9472a6b5b0 allow to upload initial 2024-08-25 16:03:23 +03:00
Pete Matsyburka 5f3a8bf391 fix condition 2024-08-19 19:51:24 +03:00
Pete Matsyburka a0a0fdd1b5 clean log 2024-08-19 19:51:24 +03:00
Pete Matsyburka 7a6f901b09 add log 2024-08-19 19:51:24 +03:00
Pete Matsyburka c4f05fbd9e remove unique index 2024-08-19 19:51:24 +03:00
Pete Matsyburka 50c342ec5c fix spec 2024-08-19 19:51:24 +03:00
Pete Matsyburka a9cfd2bcfc update submission index 2024-08-19 19:51:24 +03:00
Pete Matsyburka e2bdb2acee use account_id 2024-08-19 19:51:24 +03:00
Pete Matsyburka b8828b503c populate account_id 2024-08-19 19:51:24 +03:00
Pete Matsyburka d63f62109b add submitter account_id 2024-08-19 19:51:24 +03:00
Pete Matsyburka c04483f0ea allow to update external_id 2024-08-19 19:51:24 +03:00
Pete Matsyburka 1efaf30fb7 fix net smtp without creds 2024-08-19 19:51:24 +03:00
Pete Matsyburka 2e462e9633 fix erblint 2024-08-19 19:51:24 +03:00
Pete Matsyburka 838df5f41d fix image documents 2024-08-19 19:51:24 +03:00
Pete Matsyburka 32445f0e19 adjust mobile ui 2024-08-19 19:51:24 +03:00
Pete Matsyburka 5f9067baee fix mobile field types 2024-08-19 19:51:24 +03:00
Pete Matsyburka ad56d5ee14 adjust mobile ui 2024-08-19 19:51:24 +03:00
Pete Matsyburka a091d121d9 confgurable SMTP timeout 2024-08-19 19:51:24 +03:00
Pete Matsyburka cd9c3ee3c1 fix ci 2024-08-19 19:51:24 +03:00
Pete Matsyburka 534c3a2e10 add submission.completed 2024-08-19 19:51:24 +03:00
Pete Matsyburka 7bc8e194df fix font url 2024-08-19 19:51:24 +03:00
Pete Matsyburka de5c4d5bcd adjust retries 2024-08-19 19:51:24 +03:00
dependabot[bot] 1e6b5c684f bump deps 2024-08-10 15:34:13 +03:00
Pete Matsyburka 4651f6b652 simplify variables 2024-08-10 15:34:13 +03:00
Pete Matsyburka e5b0a2355f use aws secret 2024-08-10 15:34:13 +03:00
Pete Matsyburka db22fe7518 fix condition 2024-08-10 15:34:13 +03:00
Pete Matsyburka 0dcd97f49b add verify tool 2024-08-10 15:34:13 +03:00
Pete Matsyburka de14330449 allow to remove page area 2024-08-10 15:34:13 +03:00
Pete Matsyburka 173658138c fix conditions chain 2024-08-10 15:34:13 +03:00
Pete Matsyburka 000f3c72fc fix go to step 2024-08-10 15:34:13 +03:00
Pete Matsyburka 561b16e9fb fix pdf 2024-08-07 17:04:54 +03:00
Pete Matsyburka 507ca49112 adjust ai link 2024-08-07 14:34:32 +03:00
Pete Matsyburka 94d5ab4ace add reason 2024-08-06 00:49:40 +03:00
Pete Matsyburka 91d44a3e31 adjust date format 2024-08-05 19:38:47 +03:00
Pete Matsyburka 478167ea4f add webhook secret 2024-08-05 13:30:04 +03:00
Pete Matsyburka 6b1c3bba19 add testing footer 2024-08-05 10:45:05 +03:00
Pete Matsyburka 73841f751c fix icons 2024-08-04 23:41:32 +03:00
Pete Matsyburka 22dbe3291f adjust compose mount folder 2024-08-04 01:28:22 +03:00
Pete Matsyburka 1dac3fe434 fix cell 2024-08-04 00:13:34 +03:00
Pete Matsyburka d0c49ac04c i18n 2024-08-03 09:26:26 +03:00
Pete Matsyburka 3877dbf83f remove lookbehind regexp 2024-08-02 19:02:58 +03:00
Alex Turchyn d15fd1fb84 allow to view password during registration 2024-08-01 23:31:54 +03:00
Pete Matsyburka 0a105490fe process noreply 2024-08-01 23:22:33 +03:00
Pete Matsyburka 0bd35b526c handle date enter 2024-08-01 18:47:17 +03:00
Pete Matsyburka 3a2910f717 add expire at 2024-08-01 18:34:09 +03:00
Pete Matsyburka c4a91c2b34 extract markdown link 2024-08-01 01:31:25 +03:00
Pete Matsyburka 5643380916 configure page quality 2024-08-01 00:14:33 +03:00
Pete Matsyburka cf6b378ccb adjust payment step 2024-07-31 22:56:18 +03:00
Pete Matsyburka 66bc8cfb5f archive without confirm 2024-07-31 17:57:52 +03:00
Pete Matsyburka 994bd1fe5c dummy script 2024-07-31 10:53:51 +03:00
Pete Matsyburka 12528b6527 fix cell field 2024-07-30 21:18:56 +03:00
Pete Matsyburka d135c93939 fix n+1 2024-07-29 17:23:05 +03:00
Pete Matsyburka 637011d55a remove log 2024-07-29 16:50:25 +03:00
Pete Matsyburka b94e6bf54a adjust font resize 2024-07-26 17:58:00 +03:00
Pete Matsyburka 8f975c2898 fix lang 2024-07-26 17:53:21 +03:00
Pete Matsyburka d25c4fa766 remove log 2024-07-25 15:11:01 +03:00
Pete Matsyburka 43db15b350 add signature id 2024-07-25 09:07:45 +03:00
Pete Matsyburka 09074e2ac0 fix tld 2024-07-24 18:30:21 +03:00
Pete Matsyburka 04295f4540 adjust signature prefill 2024-07-24 09:13:13 +03:00
Pete Matsyburka 11c48a495f update rubocop 2024-07-23 22:42:56 +03:00
Pete Matsyburka 942c961cd1 add it locale 2024-07-22 18:52:40 +03:00
Pete Matsyburka 9193511d5f adjust auto scroll 2024-07-22 17:42:32 +03:00
Pete Matsyburka e0ddf97771 fix rubocop 2024-07-21 17:38:52 +03:00
Pete Matsyburka 2bd249bd5a fix erblint 2024-07-21 15:46:11 +03:00
Pete Matsyburka b72c99dbce add user api 2024-07-21 15:16:55 +03:00
Pete Matsyburka fc0504f91e update rubocop 2024-07-21 15:12:05 +03:00
Alex Turchyn 1da2196b0e add Doorkeeper tables 2024-07-21 15:10:17 +03:00
Pete Matsyburka 4bdb249a2b allow to permanently remove submissions 2024-07-20 22:29:32 +03:00
Pete Matsyburka cb38549480 allow to disable completed email attachments 2024-07-20 21:59:06 +03:00
Pete Matsyburka ee9ee6ee32 adjust attachment create 2024-07-20 21:17:47 +03:00
Pete Matsyburka 748eaaa98d add index on email email event 2024-07-20 09:40:57 +03:00
Pete Matsyburka 9e4ec11e33 adjust signature qr 2024-07-20 09:00:57 +03:00
Pete Matsyburka 93d2b76eae fix form values 2024-07-19 19:30:24 +03:00
Pete Matsyburka 2c8942e888 add form events api 2024-07-19 13:19:56 +03:00
Pete Matsyburka 4424840c81 adjust submissions endpoint 2024-07-19 10:52:16 +03:00
Pete Matsyburka 68392b4e74 add with field placeholder prop 2024-07-19 10:37:01 +03:00
Pete Matsyburka 0b4e345ad6 fix on paste 2024-07-18 10:58:12 +03:00
Pete Matsyburka 1cea5f5a99 fix fields 2024-07-17 22:02:58 +03:00
Pete Matsyburka cdaadbab20 add input mode 2024-07-16 19:41:57 +03:00
Pete Matsyburka d1ec8c6837 use preferences font 2024-07-16 13:18:03 +03:00
Pete Matsyburka cdffa66db1 refactor clone 2024-07-16 08:59:37 +03:00
Pete Matsyburka fca21ab476 fix mm domain 2024-07-15 19:28:57 +03:00
Pete Matsyburka b1d2d92f54 allow to clone template 2024-07-15 15:54:13 +03:00
Pete Matsyburka 10bb9112c7 add with field placeholder option 2024-07-15 14:03:36 +03:00
Pete Matsyburka 58b337a3ee add fields include option 2024-07-15 12:56:02 +03:00
Pete Matsyburka de52f2f5e5 add webhook urls record 2024-07-14 22:53:33 +03:00
Pete Matsyburka f10e941529 adjust field name regexp 2024-07-12 17:44:39 +03:00
Pete Matsyburka 78b1864d7d adjust payment step 2024-07-12 15:25:48 +03:00
Alex Turchyn ab49af9bcb add tool to merge pdf files 2024-07-12 12:25:14 +03:00
Pete Matsyburka 286b8d7067 add heading field 2024-07-12 07:53:09 +03:00
Pete Matsyburka 21c087cba7 fix mark sent 2024-07-11 13:09:35 +03:00
Pete Matsyburka ddd881a52f fix pdf form fields 2024-07-10 21:58:30 +03:00
Pete Matsyburka 12c69578af fix pdf form fields 2024-07-10 17:57:54 +03:00
Pete Matsyburka e96cb5eb8a add blank page button 2024-07-09 23:53:46 +03:00
Pete Matsyburka c01cfe83be add ltv method 2024-07-09 16:41:40 +03:00
Pete Matsyburka b92d1ee38c adjust payment step 2024-07-07 20:33:57 +03:00
Pete Matsyburka b90c67b184 fix cell field font size 2024-07-07 10:22:59 +03:00
Pete Matsyburka edb29977dd skip field description 2024-07-07 09:05:02 +03:00
Alex Turchyn e55c8f251d automatically lower signature font size if text doesn't fit 2024-07-06 09:46:34 +03:00
Pete Matsyburka f1d6bbdc67 fix ci 2024-07-06 09:42:52 +03:00
Pete Matsyburka 8da4c2fa10 adjust link preview 2024-07-05 14:06:37 +03:00
Pete Matsyburka e872fa2497 ruby 3.3.3 2024-07-05 11:45:21 +03:00
Pete Matsyburka 165bfef9b9 remove version from compose file 2024-07-04 22:55:51 +03:00
Pete Matsyburka 8a50c755d2 fix payment field value 2024-07-04 18:25:49 +03:00
Pete Matsyburka 002bc315f6 adjust import 2024-07-04 18:04:36 +03:00
Pete Matsyburka e87522eeab adjust form fields 2024-07-02 22:38:03 +03:00
Pete Matsyburka 377244f948 add form preview 2024-07-02 22:38:02 +03:00
Pete Matsyburka 73423a6f44 fix stamp 2024-07-02 19:11:42 +03:00
Pete Matsyburka d219e3cd7e do not move in preview 2024-07-02 13:17:39 +03:00
Pete Matsyburka 09b0022195 move fields with arrows 2024-07-02 11:41:24 +03:00
Pete Matsyburka 8aac935641 allow to download combined pdf via api 2024-07-01 12:06:05 +03:00
Pete Matsyburka 16e5c13633 fix erblint 2024-07-01 08:05:09 +03:00
Pete Matsyburka fff1831768 add gpt link 2024-06-30 16:21:19 +03:00
Pete Matsyburka f91a318e94 fix drawer 2024-06-29 20:10:28 +03:00
Pete Matsyburka ed06e17b23 adjust field attrs 2024-06-29 11:29:59 +03:00
Pete Matsyburka 63e1be7dfe remove border 2024-06-29 08:52:16 +03:00
Pete Matsyburka 16044612bd fix form fields 2024-06-29 08:44:19 +03:00
Pete Matsyburka 27d8880414 border alert 2024-06-29 00:04:10 +03:00
Pete Matsyburka a0aae6a9de add currencies 2024-06-28 23:34:15 +03:00
Alex Turchyn 749043dfbb process acro form fields 2024-06-28 21:42:39 +03:00
Pete Matsyburka 320fe02e35 remove upload signature button from drawn type 2024-06-28 16:56:51 +03:00
Pete Matsyburka 6f4b9d4a4e double click on builder checkbox 2024-06-28 16:28:37 +03:00
Pete Matsyburka a589cf9471 focus field input 2024-06-27 17:41:08 +03:00
Pete Matsyburka 7fc7283191 copy and paste fields 2024-06-27 15:14:39 +03:00
Pete Matsyburka af03560494 add radio select default value settings 2024-06-27 13:11:48 +03:00
Pete Matsyburka 988d1b93a5 double click to toggle signing date 2024-06-27 12:40:10 +03:00
Pete Matsyburka ab0278198a adjust phone step 2024-06-26 16:18:33 +03:00
Pete Matsyburka fbc649ab59 add extra links 2024-06-26 13:34:47 +03:00
Pete Matsyburka 6b5adc2504 select us date format 2024-06-25 21:49:44 +03:00
Pete Matsyburka 7c4307ef10 fix query 2024-06-25 21:11:48 +03:00
Pete Matsyburka de744e2f6c adjust invite email 2024-06-25 19:05:50 +03:00
Pete Matsyburka c171597c7f adjust max retries 2024-06-25 08:45:38 +03:00
Pete Matsyburka 3034c00bb3 use sidekiq 2024-06-24 21:10:58 +03:00
Pete Matsyburka 9539c476c0 remove legacy tracking param 2024-06-24 14:33:33 +03:00
Pete Matsyburka 4552c72860 add index on external id 2024-06-24 13:27:14 +03:00
Pete Matsyburka 85f8a92721 optimize query 2024-06-23 12:20:01 +03:00
Pete Matsyburka 049b9c78c2 add retries 2024-06-23 09:16:34 +03:00
Pete Matsyburka acf06084bf simplify job params 2024-06-22 10:50:07 +03:00
Pete Matsyburka 61da477733 reorder 2024-06-22 10:50:07 +03:00
Pete Matsyburka fdeec548a3 adjust message 2024-06-22 10:50:07 +03:00
Pete Matsyburka ff0d486831 fix clone template folder 2024-06-22 10:50:07 +03:00
Pete Matsyburka d5bf54ee26 adjust tempalte preferences 2024-06-22 10:50:07 +03:00
Pete Matsyburka 7616774613 use latest alpine 2024-06-21 00:46:11 +03:00
Pete Matsyburka 27c4dc7eca font size in the UI 2024-06-20 22:15:45 +03:00
Pete Matsyburka 4ae834d6f7 skip optional signature 2024-06-20 17:52:40 +03:00
Pete Matsyburka b71a66a082 add font size preference 2024-06-20 16:14:18 +03:00
Pete Matsyburka 80cc38542c fix email attachments 2024-06-18 20:09:58 +03:00
Pete Matsyburka 185aecec42 increase signature size 2024-06-18 17:18:23 +03:00
Pete Matsyburka 8c092eba78 allow to permanently delete submissions 2024-06-18 16:54:15 +03:00
Pete Matsyburka 781ccc701d optimize large files upload 2024-06-16 11:27:24 +03:00
Pete Matsyburka 009e05584a remove submitter signature assoc 2024-06-15 20:14:09 +03:00
Pete Matsyburka f9d52e56a2 prefill signature 2024-06-15 16:23:27 +03:00
Pete Matsyburka 9e66f8412c add extra user fields 2024-06-14 22:59:11 +03:00
Pete Matsyburka 181e42c4b3 refactor 2024-06-14 16:55:20 +03:00
Pete Matsyburka 15e60fc5e7 add emails copy toggle 2024-06-14 12:19:16 +03:00
Pete Matsyburka ee357e58ac fix prefill signature 2024-06-14 10:00:25 +03:00
Pete Matsyburka 0d9a709fa3 disable prefill signature 2024-06-14 09:09:56 +03:00
Pete Matsyburka 7eae09bdb0 assign default values on api complete 2024-06-13 23:45:53 +03:00
Pete Matsyburka e8e32e5d1f add log 2024-06-13 18:05:11 +03:00
Pete Matsyburka a3d54db4c6 adjust list import 2024-06-13 15:20:34 +03:00
Pete Matsyburka 9686ae6250 add field validation 2024-06-13 14:32:49 +03:00
Pete Matsyburka 044195f07b blur on field move 2024-06-13 10:54:10 +03:00
Pete Matsyburka 35f159266a paste raw text 2024-06-13 10:26:36 +03:00
Pete Matsyburka 3b717ae3de double click to edit default value 2024-06-12 22:46:50 +03:00
Pete Matsyburka 2389c3fc40 prefill signature 2024-06-11 19:42:26 +03:00
Pete Matsyburka 92da790e77 fix email tracking params 2024-06-11 16:59:15 +03:00
Pete Matsyburka 14418b93b1 full error log in prod 2024-06-10 17:17:45 +03:00
Pete Matsyburka 1289aff493 parse date using format 2024-06-10 12:08:56 +03:00
Pete Matsyburka 7568f563cb adjust payment step 2024-06-09 23:56:43 +03:00
Pete Matsyburka 5ae56bd50c add field preferences param 2024-06-09 20:25:19 +03:00
Pete Matsyburka c2ded81959 update hmtl lang attr 2024-06-09 16:39:41 +03:00
Pete Matsyburka f2c307ef63 adjust conditions modal 2024-06-09 16:37:28 +03:00
Pete Matsyburka af787887d8 i18n sign in 2024-06-08 23:02:29 +03:00
Pete Matsyburka 54272d83eb add integrations placeholder 2024-06-08 21:30:04 +03:00
Pete Matsyburka 3070fc2074 clear condition values 2024-06-08 12:32:41 +03:00
Pete Matsyburka 6a31d2c722 alert message on missing submitter fields 2024-06-08 10:01:14 +03:00
Pete Matsyburka f26bfe9acc remove button from archived 2024-06-07 21:54:37 +03:00
Pete Matsyburka a729fd6234 fix webhook url on testing 2024-06-06 22:03:15 +03:00
Pete Matsyburka c2051c156e adjust signature step 2024-06-06 17:14:05 +03:00
Pete Matsyburka 3852870c09 add payment field condition 2024-06-05 21:09:08 +03:00
Pete Matsyburka a58253a6a1 add devise emails events 2024-06-05 09:29:00 +03:00
Pete Matsyburka 30c98a970d fix audit trail 2024-06-04 19:34:09 +03:00
Pete Matsyburka 8f2edef09e wrap long email is stamp 2024-06-04 19:31:55 +03:00
Pete Matsyburka 9e465ea47e use field title in audit 2024-06-04 18:33:24 +03:00
Pete Matsyburka cdda55d528 add email events 2024-06-04 17:41:07 +03:00
Pete Matsyburka db302858c4 add template folder name to webhook 2024-06-03 18:13:13 +03:00
Pete Matsyburka b81cc83027 fix signature toggle 2024-06-03 12:31:40 +03:00
Pete Matsyburka 766d653349 fix build 2024-06-03 12:01:09 +03:00
Pete Matsyburka 98c04eac15 normalize client ip 2024-06-03 08:57:14 +03:00
Pete Matsyburka c35dfea154 add log 2024-06-02 21:24:21 +03:00
Pete Matsyburka 37a9a86a1d fix naming 2024-06-02 18:36:59 +03:00
Pete Matsyburka e9a759055b allow to upload user initials 2024-06-02 17:06:09 +03:00
Pete Matsyburka f6c061a57b allow to draw signature on mobile 2024-06-02 16:02:18 +03:00
Pete Matsyburka 91f3d8ed7e adjust meta tags 2024-05-31 22:35:59 +03:00
Pete Matsyburka 3b18502d3d remove onsubmit html event 2024-05-31 14:44:36 +03:00
Pete Matsyburka df8e79683a eager load in test 2024-05-31 13:19:33 +03:00
Alex Turchyn 7080e8ea33 fix js errors 2024-05-31 13:08:02 +03:00
Pete Matsyburka c56f8cb38f fix autocomplete 2024-05-30 23:38:38 +03:00
Pete Matsyburka 1ceac968b0 fix move existing user 2024-05-30 22:18:32 +03:00
Pete Matsyburka 938ee99cfc show full error trace in dev 2024-05-29 16:14:07 +03:00
Pete Matsyburka 2367040e45 add slug to submissions response 2024-05-28 00:25:59 +03:00
Pete Matsyburka df19aabfba adjust users table 2024-05-27 20:36:52 +03:00
Pete Matsyburka f7b5bcd52f add webhooks queue 2024-05-27 14:40:12 +03:00
Pete Matsyburka a249b0a678 fix pdf form flatten fields 2024-05-27 00:47:14 +03:00
Pete Matsyburka 99bc46b40f fix erblint 2024-05-26 22:51:16 +03:00
Pete Matsyburka c1e6f0970d add file types 2024-05-26 22:38:24 +03:00
Pete Matsyburka e8c2316bb2 add users count 2024-05-26 20:51:02 +03:00
Pete Matsyburka 9866e5468c add download combined pdf button 2024-05-26 11:43:47 +03:00
Pete Matsyburka fc60df7974 fix erblint 2024-05-25 16:59:22 +03:00
Pete Matsyburka 08784d28c7 adjust log ip 2024-05-25 16:25:59 +03:00
Pete Matsyburka 0440ea652f increase attachment url ttl 2024-05-25 16:25:56 +03:00
Pete Matsyburka e466562391 use rollbar 2024-05-24 13:35:28 +03:00
Pete Matsyburka b2c8667c53 safe register element 2024-05-23 14:02:48 +03:00
Pete Matsyburka d6893079f7 remove breadcrumbs 2024-05-23 13:39:16 +03:00
Pete Matsyburka ac1d973605 fix loop 2024-05-23 12:45:01 +03:00
Pete Matsyburka 1405fefc85 add sentry 2024-05-23 12:25:25 +03:00
Pete Matsyburka 2a75055fb6 fix pdf parsing 2024-05-22 21:37:23 +03:00
Pete Matsyburka f84fdd51de fix builder areas 2024-05-22 15:39:46 +03:00
Alex Turchyn b38073126e fix dropdown styles 2024-05-22 15:20:04 +03:00
Alex Turchyn c6e681195d upgrade daisyUI to v3.9.4 2024-05-22 13:39:43 +03:00
Pete Matsyburka 53d924be0b fix build 2024-05-20 20:07:17 +03:00
Pete Matsyburka d6cf027d5d fix erblint 2024-05-19 19:44:38 +03:00
Alex Turchyn ec332bab88 flatten pdf for preview images 2024-05-19 19:25:53 +03:00
Alex Turchyn 8b3b90a7e2 replace emojis 2024-05-19 19:12:17 +03:00
Pete Matsyburka 9047e3bed3 add dry run prop 2024-05-18 22:54:26 +03:00
Pete Matsyburka 72b7391245 add documents copy email to template preferences 2024-05-18 22:03:20 +03:00
Pete Matsyburka 2df05f0327 adjust initials height 2024-05-18 17:51:22 +03:00
Pete Matsyburka 95f3eb0e5d display filters 2024-05-18 13:58:21 +03:00
Pete Matsyburka ce8d964ce6 fix ensure result generated 2024-05-17 16:07:08 +03:00
Pete Matsyburka f0d5fc4e2d add submission created webhook event 2024-05-17 13:28:10 +03:00
Pete Matsyburka b4b17d847e add view archived to template page 2024-05-17 12:08:46 +03:00
Alex Turchyn fd537e7ac0 upgrade rack 3 2024-05-16 23:26:30 +03:00
Pete Matsyburka 8c271104e1 single digital signature by default 2024-05-16 23:18:48 +03:00
Pete Matsyburka 51edd60d96 update gem 2024-05-16 15:16:59 +03:00
Pete Matsyburka a265ca1716 remove pdf processor 2024-05-16 13:37:23 +03:00
Pete Matsyburka a773f63a65 remove pdf reader 2024-05-16 11:56:41 +03:00
Pete Matsyburka 576c8a2e24 fix ensure result generated 2024-05-15 23:49:36 +03:00
Pete Matsyburka 5829de9ee4 adjust required fields alert 2024-05-15 11:42:51 +03:00
Pete Matsyburka b202941b79 fix tsa settings 2024-05-15 00:21:52 +03:00
Pete Matsyburka 6f6fb74988 adjust plans settings nav 2024-05-14 23:32:11 +03:00
Pete Matsyburka 0137bdbe4c optimize tsa settings controller 2024-05-12 19:32:09 +03:00
Pete Matsyburka 55d9511c01 add disclosure message 2024-05-12 12:35:20 +03:00
Pete Matsyburka 929d29576d add template webhooks 2024-05-12 11:37:33 +03:00
Pete Matsyburka 6f7128a51c add number format 2024-05-12 11:12:07 +03:00
Pete Matsyburka fc5c4ff2fc adjust reminder options 2024-05-11 16:24:13 +03:00
Pete Matsyburka a81595db2b allow legacy algos 2024-05-10 22:55:21 +03:00
Pete Matsyburka a5615d500d fix missing field steps 2024-05-10 22:24:44 +03:00
Pete Matsyburka da9266befc fix time format 2024-05-10 09:24:09 +03:00
Pete Matsyburka 5b3cffeb73 fix submission archived webhook 2024-05-08 09:53:37 +03:00
Pete Matsyburka 8f7b8a5fdf fix signature size 2024-05-08 09:38:16 +03:00
Pete Matsyburka 6d93116993 fix checkbox add on missclick 2024-05-05 18:18:07 +03:00
Pete Matsyburka a0f4916d3b add field settings dropdown to area 2024-05-05 16:58:19 +03:00
Pete Matsyburka ace897eaf9 update SECURITY.md 2024-05-04 23:15:04 +03:00
Pete Matsyburka b8667c5408 fix audit log on dev 2024-05-04 11:09:48 +03:00
Pete Matsyburka d87d66632e add field-area-active class 2024-05-02 23:33:44 +03:00
Alex Turchyn 3015c2d156 convert BMP images to PNG before uploading in the signing form 2024-05-02 22:10:23 +03:00
Pete Matsyburka 52b9e9ee8e fix underscore field name 2024-05-02 16:33:41 +03:00
Pete Matsyburka 5bfad0271c fix audit log font 2024-05-02 13:50:43 +03:00
Pete Matsyburka 58797891c7 fix trusted cert env 2024-05-02 13:39:47 +03:00
Pete Matsyburka 897750ba1e fix build 2024-05-02 12:43:13 +03:00
Pete Matsyburka 99341e773f refactor sign params 2024-05-01 23:40:09 +03:00
Pete Matsyburka 71319c1548 fix pdf generation 2024-05-01 11:51:40 +03:00
Pete Matsyburka c0a42f6bfc do now show default fields on preview 2024-04-29 15:42:11 +03:00
Pete Matsyburka d5abb46112 fix phone value assignment 2024-04-28 22:09:02 +03:00
Pete Matsyburka 2508b5fc8a adjust template account clone 2024-04-28 16:43:31 +03:00
Pete Matsyburka 4bd1ef0da3 fix params normalize 2024-04-28 16:33:11 +03:00
Pete Matsyburka 4efd748571 fix 2024-04-28 16:32:23 +03:00
Pete Matsyburka 859892d1fd adjust template preferences 2024-04-28 16:26:47 +03:00
Pete Matsyburka 7adeaf93cd only active linked accounts 2024-04-28 15:01:15 +03:00
Pete Matsyburka 5dbe155ecb shorten call 2024-04-28 14:44:33 +03:00
Pete Matsyburka 667d065bee populate linked accounts 2024-04-28 14:27:23 +03:00
Pete Matsyburka bb613059de add archived at to accounts 2024-04-28 13:43:53 +03:00
Pete Matsyburka 1b2dec4c2a adjust create submission resp 2024-04-27 23:08:16 +03:00
Pete Matsyburka 2c2ef97c30 validate params 2024-04-27 21:35:20 +03:00
Pete Matsyburka 6df1d68237 addept value param 2024-04-27 21:35:09 +03:00
Pete Matsyburka 20c9faedff update dep 2024-04-27 20:15:39 +03:00
Pete Matsyburka d931a40ea9 adjust log 2024-04-27 11:11:00 +03:00
Pete Matsyburka 1097b98b85 add log 2024-04-26 23:37:04 +03:00
Pete Matsyburka 2e597fcf43 ruby 3.3.1 2024-04-26 12:39:45 +03:00
Pete Matsyburka bb2fa6a261 add reuse signature prop 2024-04-25 17:51:36 +03:00
Pete Matsyburka 1dc8369487 fix template clone 2024-04-24 21:02:19 +03:00
Pete Matsyburka e6d60f1985 better error message 2024-04-24 13:18:57 +03:00
Pete Matsyburka 82e3cb8a4d fix audit log toggle 2024-04-24 01:19:07 +03:00
Pete Matsyburka aeec0cbf8e phone step langauge 2024-04-23 21:33:40 +03:00
Pete Matsyburka 7a2d1fc6c5 allow to remove personalization configs 2024-04-23 20:47:12 +03:00
Pete Matsyburka 68033ab7d2 fix files link position 2024-04-23 20:09:39 +03:00
Pete Matsyburka 7595c60ea8 remove pattern validation 2024-04-23 15:02:42 +03:00
Pete Matsyburka d9318c04f8 fix builder bg color 2024-04-23 01:57:45 +03:00
Pete Matsyburka 2d76198290 add flatten pdf toggle 2024-04-22 21:34:55 +03:00
Pete Matsyburka 02a7b6ea55 do not allow to skip required field with whitespace 2024-04-22 21:15:57 +03:00
Pete Matsyburka ab57076b8f add class 2024-04-22 20:01:30 +03:00
Pete Matsyburka 82fba47c98 rotate pdf 2024-04-22 19:10:58 +03:00
Pete Matsyburka 5f32e2a5af adjust create event 2024-04-21 17:01:44 +03:00
Pete Matsyburka 70012fbd7c fix form 2024-04-21 16:40:28 +03:00
Pete Matsyburka 668f6ca975 fix cells draw 2024-04-21 00:32:40 +03:00
Pete Matsyburka e78dd301ef fix download ttl 2024-04-20 23:58:43 +03:00
Pete Matsyburka f8ec3daf10 add minimize prop 2024-04-20 18:53:16 +03:00
Pete Matsyburka 75c929639d fix condition last field error 2024-04-20 17:23:28 +03:00
Pete Matsyburka e9c1242b8d verify field only once 2024-04-20 17:07:31 +03:00
Pete Matsyburka e7eb1d4f12 add noindex 2024-04-20 15:47:48 +03:00
Pete Matsyburka abbd3e809a remove sign now from archived 2024-04-20 12:54:14 +03:00
Pete Matsyburka f430b4f5a6 change default email 2024-04-20 11:25:04 +03:00
Pete Matsyburka e65234b4c8 add archived users view 2024-04-19 16:19:02 +03:00
Pete Matsyburka fe04d22c94 fix email normalize 2024-04-19 12:30:10 +03:00
Pete Matsyburka 44a7a5ebdc fix go to last 2024-04-19 10:33:31 +03:00
Pete Matsyburka 119ef0d0c7 add force sso key 2024-04-19 02:04:35 +03:00
Pete Matsyburka cdb8eb7931 fix pdf number of pages 2024-04-18 22:25:07 +03:00
Pete Matsyburka aef176c0cc downgrade net smtp 2024-04-18 18:43:47 +03:00
Pete Matsyburka 0c21b55865 adjust fixed email log 2024-04-18 17:02:53 +03:00
Pete Matsyburka 964b1444a5 adjust completed action 2024-04-18 16:45:09 +03:00
Pete Matsyburka 48de343948 add fix email log 2024-04-18 10:44:45 +03:00
Pete Matsyburka 2640934c15 skip .om tld typo fix 2024-04-18 10:42:54 +03:00
Pete Matsyburka 442dd4d6db add submitters template_id filter 2024-04-18 01:48:14 +03:00
Pete Matsyburka 3168feb3d6 clone template preferences 2024-04-17 21:09:35 +03:00
Pete Matsyburka fbae4933f6 fix multiple bcc addresses 2024-04-17 20:51:34 +03:00
Pete Matsyburka 78bb66c344 adjust wording 2024-04-17 20:24:31 +03:00
Pete Matsyburka 5af65ef74f adjust drawer 2024-04-17 17:27:43 +03:00
Pete Matsyburka 4d354ae9ff do not load storage configs in dev 2024-04-17 16:51:42 +03:00
Pete Matsyburka 1e57c1ace7 add template preferences 2024-04-17 16:09:09 +03:00
Pete Matsyburka fefd138dfd add devise log 2024-04-17 00:12:05 +03:00
Pete Matsyburka 16a77d3536 add log 2024-04-16 14:07:38 +03:00
Pete Matsyburka e03857bdb7 mark required field on mobile 2024-04-16 10:42:11 +03:00
Pete Matsyburka fc679bcd72 add require file links auth toggle 2024-04-15 20:34:00 +03:00
Pete Matsyburka 652b0da021 add fields param 2024-04-15 19:36:16 +03:00
Pete Matsyburka 2d54b0db2f update deps 2024-04-15 18:33:55 +03:00
Pete Matsyburka 7a29894926 add required fields prop 2024-04-15 16:34:44 +03:00
Pete Matsyburka f9ae79dd5b add go to last 2024-04-13 17:41:49 +03:00
Pete Matsyburka cadb78ca35 check on email events 2024-04-13 12:37:48 +03:00
Pete Matsyburka 36a6b7ff1d fix typo 2024-04-13 11:35:04 +03:00
Pete Matsyburka efee46f113 add bulk source 2024-04-12 17:20:00 +03:00
Pete Matsyburka b6639738c3 adjust dockerfile 2024-04-11 22:52:12 +03:00
Alex Turchyn aab07c5f49 add icons 2024-04-11 22:52:11 +03:00
Pete Matsyburka 33c0784bc6 add folder name to submission resp 2024-04-11 16:36:53 +03:00
Pete Matsyburka 6f5ebfc8a6 fix preview completed 2024-04-11 02:45:38 +03:00
Pete Matsyburka 811091ff14 adjust text signature size 2024-04-10 22:24:19 +03:00
Pete Matsyburka 6666f39928 add redacted param 2024-04-10 19:56:35 +03:00
Pete Matsyburka f67db9fe5f add field uuid param 2024-04-10 13:30:27 +03:00
Pete Matsyburka 45f002803e adjust padding 2024-04-10 10:55:51 +03:00
Pete Matsyburka 8c19f90e88 remove debug 2024-04-10 00:44:36 +03:00
Pete Matsyburka a8f5d5cea7 fix typo 2024-04-09 19:08:05 +03:00
Pete Matsyburka 6788071eec add webhook event preferences 2024-04-09 18:11:58 +03:00
Pete Matsyburka 0c690b3033 custom submit button 2024-04-09 01:54:39 +03:00
Pete Matsyburka dcb963b623 add autoscrollFields prop 2024-04-08 18:37:10 +03:00
Pete Matsyburka 04e30dbdd6 add submitter update params 2024-04-08 10:51:18 +03:00
Pete Matsyburka ac6d5a7aed update readme 2024-04-08 01:18:31 +03:00
Pete Matsyburka 3166a31b67 add icon 2024-04-08 01:02:43 +03:00
Pete Matsyburka fc3283eb3a add Korean i18n 2024-04-08 00:33:54 +03:00
Pete Matsyburka d6a96d4aa3 fix base64 unicode 2024-04-07 18:06:26 +03:00
Pete Matsyburka cda349d71c adjust send email views 2024-04-07 17:58:18 +03:00
Pete Matsyburka 87345a3b3d fix rubocop 2024-04-07 11:14:34 +03:00
Pete Matsyburka b791d0699c fix replace button file types 2024-04-06 21:57:35 +03:00
Pete Matsyburka 2cb15f3282 optimize document upload 2024-04-06 21:57:34 +03:00
Pete Matsyburka 8b823ca0d0 do not fix gob tld 2024-04-06 19:41:17 +03:00
Pete Matsyburka f3f08797f7 optimize document upload 2024-04-06 17:08:45 +03:00
Pete Matsyburka cab9672d25 remove direct upload 2024-04-06 15:00:30 +03:00
Pete Matsyburka c8d3b1bace reorder settings 2024-04-06 12:56:05 +03:00
Pete Matsyburka 5ee51b2b57 fix testing account 2024-04-06 01:20:02 +03:00
Pete Matsyburka 525dc503a6 accept default value array 2024-04-06 01:02:24 +03:00
Pete Matsyburka 12cf28792b add uuid to accounts 2024-04-05 19:56:34 +03:00
Pete Matsyburka 33466c1d07 adjust sso 2024-04-05 19:28:57 +03:00
Pete Matsyburka 226ae2172e word break underscore 2024-04-05 19:14:02 +03:00
Pete Matsyburka 8e2700b8b8 break long titles 2024-04-05 16:44:17 +03:00
Pete Matsyburka 753cbd4d27 allow to add custom form completed message 2024-04-05 16:16:30 +03:00
Pete Matsyburka f16405ca70 assign first party i18n name 2024-04-05 02:21:14 +03:00
Pete Matsyburka 23f052c400 add Korean 2024-04-05 02:02:23 +03:00
Pete Matsyburka 90a0a03e15 validate custom certs in the cloud 2024-04-04 14:08:14 +03:00
Pete Matsyburka 2dba8ae366 add submissions dashboard view 2024-04-04 13:45:11 +03:00
Pete Matsyburka 5bb7ad571c display default date only of the signing party 2024-04-03 10:27:19 +03:00
Pete Matsyburka 0cdb7c2c4f fix date normalization 2024-04-02 11:30:52 +03:00
Pete Matsyburka 11bdc522cd add spreadsheet download 2024-04-01 17:56:08 +03:00
Pete Matsyburka 014835f90d adjust download button state 2024-04-01 15:15:55 +03:00
Pete Matsyburka 05e66b60ee fix result generation with annots 2024-04-01 13:16:54 +03:00
Pete Matsyburka 505b185cf2 fix download indicator 2024-03-31 18:31:09 +03:00
Pete Matsyburka 50c32ac6ff replace date value 2024-03-31 18:20:33 +03:00
Pete Matsyburka 878ab25fd4 access account via submission 2024-03-31 17:40:51 +03:00
Pete Matsyburka 1b31772ff6 show default date on the page 2024-03-31 17:34:23 +03:00
Pete Matsyburka d4940234b7 add bulk placeholder 2024-03-30 23:36:40 +02:00
Pete Matsyburka 9bdc5d4863 adjust batch limit 2024-03-30 14:42:42 +02:00
Pete Matsyburka a73fd7929c add smtp timeout 2024-03-30 12:43:15 +02:00
Pete Matsyburka 8269a61751 adjust payment connected 2024-03-29 23:24:51 +02:00
Pete Matsyburka de072d1437 scroll to multiple checkbox field 2024-03-29 21:43:13 +02:00
Pete Matsyburka 5b24793f2b render links with markdown in emails 2024-03-29 21:21:04 +02:00
Pete Matsyburka 660f7ab8f3 password reset neutral on-prem 2024-03-29 14:50:18 +02:00
Pete Matsyburka 6e3fea5fa0 validate email param 2024-03-28 11:25:32 +02:00
Pete Matsyburka 29ecbecbc2 flatten annots 2024-03-27 18:24:41 +02:00
Pete Matsyburka ad3bbe9892 sctoll to radio group 2024-03-27 18:24:35 +02:00
Pete Matsyburka b570ebee69 remember builder selected field type 2024-03-27 14:21:30 +02:00
Pete Matsyburka 207780fa3f clear fields on remove doc 2024-03-27 00:11:10 +02:00
Pete Matsyburka 8aa219433b render blank option checkbox 2024-03-26 19:28:41 +02:00
Pete Matsyburka cb1e9f2be4 do not send completed emails to integration 2024-03-26 18:01:35 +02:00
Pete Matsyburka 202943f58d fix pidfile issue 2024-03-26 01:17:55 +02:00
Pete Matsyburka 6dd15b2091 use letter page size for us 2024-03-25 20:20:38 +02:00
Pete Matsyburka 1c55e4f18b always display default value 2024-03-25 19:16:12 +02:00
Pete Matsyburka 6cc36b1728 allow to update template roles 2024-03-25 17:37:46 +02:00
Pete Matsyburka 37751cd5c1 update template submitters 2024-03-25 16:34:45 +02:00
Pete Matsyburka 80d882cd20 add reply to param 2024-03-24 14:09:28 +02:00
Pete Matsyburka 8bef75e570 move autocomplete controllers 2024-03-23 23:52:50 +02:00
Pete Matsyburka 077eab7005 refactor templates controller 2024-03-23 23:52:49 +02:00
Pete Matsyburka 715c70d422 rename to users 2024-03-23 21:23:59 +02:00
Pete Matsyburka a3c23f9d53 update readme 2024-03-22 17:35:52 +02:00
Pete Matsyburka c1e67a8af0 add submission id var 2024-03-22 13:11:33 +02:00
Pete Matsyburka 2fdc294293 fix annot 2024-03-21 16:25:49 +02:00
Pete Matsyburka 1f49ba069e disable puma pidfile 2024-03-21 15:03:50 +02:00
Pete Matsyburka 43ef0fc0d0 use FORCE_SSL=false 2024-03-21 14:12:11 +02:00
Pete Matsyburka a1a920e536 allow to remove default value 2024-03-21 00:04:53 +02:00
Pete Matsyburka dec33b4331 add extra small field font size 2024-03-20 16:52:53 +02:00
Pete Matsyburka 432f290633 assign template fields on completed via API 2024-03-20 00:13:16 +02:00
Pete Matsyburka cf98616a27 remove log 2024-03-19 13:57:21 +02:00
Pete Matsyburka 1a720cf1ad allow to clone template from testing 2024-03-19 01:44:16 +02:00
Pete Matsyburka 557c6c34f2 search default fields 2024-03-19 01:21:44 +02:00
Pete Matsyburka 943c76505d remove api error log 2024-03-18 23:18:35 +02:00
Pete Matsyburka 66eb83e6a7 allow to search on value 2024-03-18 20:16:26 +02:00
Pete Matsyburka 0844427357 add rollbar queue 2024-03-18 19:35:19 +02:00
Pete Matsyburka 72e9d9ea2d update readme 2024-03-17 23:16:27 +02:00
Pete Matsyburka 3db7d47746 fix option remove 2024-03-17 19:40:30 +02:00
Pete Matsyburka 95899336b8 align number field 2024-03-17 19:06:00 +02:00
Pete Matsyburka f6691c110e adjust field settings 2024-03-17 17:04:39 +02:00
Pete Matsyburka 749722a9df add rate limit 2024-03-16 23:22:11 +02:00
Pete Matsyburka 265b668cb4 show bulk send message 2024-03-16 22:21:23 +02:00
Pete Matsyburka 0527e6a5c1 fix form font size 2024-03-16 01:59:00 +02:00
Pete Matsyburka d989b61d19 fix font size 2024-03-15 23:06:16 +02:00
Pete Matsyburka c1f3e6ff13 reduce image size 2024-03-15 21:16:30 +02:00
Pete Matsyburka 3c0fff2d82 skip on missing font character error 2024-03-15 20:24:12 +02:00
Pete Matsyburka bd599c264f rescue Encoding::CompatibilityError 2024-03-15 13:44:16 +02:00
Pete Matsyburka 0274093c89 save multiple document attachments 2024-03-15 13:03:10 +02:00
Pete Matsyburka 5845b5bdfd process malformed PDF 2024-03-15 12:17:52 +02:00
Pete Matsyburka 3dd0ba6b0b fix pdf font width 2024-03-14 23:10:21 +02:00
Pete Matsyburka e79306f8a3 optimize build annotations 2024-03-14 21:30:57 +02:00
Pete Matsyburka 0171b0c2dd allow to disable stamp logo 2024-03-14 14:09:43 +02:00
Pete Matsyburka a963ef53d3 adjust submitter role assign 2024-03-14 01:21:42 +02:00
Pete Matsyburka 344f302c76 refactor field validation 2024-03-13 20:25:41 +02:00
Pete Matsyburka 17ded282b2 add symbols font 2024-03-13 14:58:32 +02:00
Pete Matsyburka 2ce57df485 sanitize href 2024-03-13 11:54:56 +02:00
Pete Matsyburka 849289287e add html escape replace variable option 2024-03-12 23:47:58 +02:00
Pete Matsyburka 704fbc2393 try get log params 2024-03-11 21:34:59 +02:00
Pete Matsyburka 168fc8bd03 fix testing acocunt 2024-03-11 14:55:50 +02:00
Pete Matsyburka c5598d9efe fix replace email vars 2024-03-11 10:05:43 +02:00
Pete Matsyburka f02315726d error on too small signature 2024-03-10 19:55:45 +02:00
Pete Matsyburka e56dcb59c0 add cors headers on API error 2024-03-10 15:54:44 +02:00
Pete Matsyburka c74f350ea2 fix typo 2024-03-10 12:03:46 +02:00
Pete Matsyburka af9d015cf2 refactor mailer custom content 2024-03-10 00:41:18 +02:00
Pete Matsyburka 633e5ac5e4 allow to disable confetti 2024-03-10 00:13:35 +02:00
Pete Matsyburka 2d0f323854 adjust sender email var 2024-03-09 22:55:55 +02:00
Pete Matsyburka cfe5610ec9 add field description and title 2024-03-09 22:55:54 +02:00
Pete Matsyburka 55616ac321 sort field areas 2024-03-08 00:34:16 +02:00
Pete Matsyburka 8f2d1deb38 do not show console link on testing 2024-03-07 21:36:41 +02:00
Pete Matsyburka e2763ceb80 add builder selectors 2024-03-07 19:01:34 +02:00
Pete Matsyburka 10462f3140 fix existing submitter uuids 2024-03-07 16:28:14 +02:00
Pete Matsyburka 3aef92486b silence active storage error 2024-03-06 23:14:07 +02:00
Pete Matsyburka c1a9ee0bbb return error on file download 400+ resp 2024-03-06 22:36:15 +02:00
Pete Matsyburka 398fae82cb optimize api pagination 2024-03-06 18:58:27 +02:00
Pete Matsyburka a16443f961 add completed at submitter filter 2024-03-06 18:44:13 +02:00
Pete Matsyburka ad4e0e1d27 adjust email variables 2024-03-06 00:27:16 +02:00
Pete Matsyburka 493a2108e9 fix safari builder scaling 2024-03-05 20:53:54 +02:00
Pete Matsyburka c2c9bb64b1 fix audit log original sha 2024-03-05 17:16:18 +02:00
Pete Matsyburka bb1426c0af log generation error 2024-03-05 13:56:47 +02:00
Pete Matsyburka 2505768e4d SMTP auto option 2024-03-04 22:30:07 +02:00
Pete Matsyburka 23a1b70534 turbo false console links 2024-03-03 21:21:27 +02:00
Pete Matsyburka b34616ecbc fix default date value 2024-03-03 11:50:22 +02:00
Pete Matsyburka 0111252f6f add timeserver spec info 2024-03-02 23:16:43 +02:00
Pete Matsyburka a561cc5f73 adjust invalid date error 2024-03-02 19:50:58 +02:00
Pete Matsyburka 519e6f1f79 cleanup 2024-03-02 12:09:35 +02:00
Pete Matsyburka da871b5d3a adjust builder page numbers 2024-03-02 11:24:04 +02:00
Pete Matsyburka ae3f939d23 add field conditions placeholder 2024-03-02 11:13:39 +02:00
Pete Matsyburka fb6ede564e retry webhooks on faraday error 2024-03-01 18:51:11 +02:00
Pete Matsyburka 1c701d4fe8 adjust FR i18n 2024-03-01 16:34:11 +02:00
Pete Matsyburka c743cc689d validate account config keys 2024-02-29 23:59:49 +02:00
Pete Matsyburka 8432bec3d6 fix result generation with cell 2024-02-29 21:39:52 +02:00
Pete Matsyburka 8e72b0b0e4 show testing shared templates 2024-02-29 21:39:46 +02:00
Pete Matsyburka 19e4bbc01e parse emails user 2024-02-28 21:54:27 +02:00
Pete Matsyburka 56a9e412c8 autocomplete debounce 2024-02-28 14:13:23 +02:00
Pete Matsyburka 4b75623b86 add index on blob checksum 2024-02-28 10:35:51 +02:00
Pete Matsyburka e1ea42a997 add index on submission events 2024-02-28 09:49:59 +02:00
Pete Matsyburka ee37710e57 adjust expand form 2024-02-28 01:54:10 +02:00
Pete Matsyburka ae99218789 adjust field options preview mode 2024-02-28 00:40:42 +02:00
Pete Matsyburka 9fe8214ca0 adjust blobs proxy 2024-02-28 00:40:19 +02:00
Pete Matsyburka 6ca44a5cc3 optimize autocomplete 2024-02-27 15:24:28 +02:00
Pete Matsyburka 801abd197b optimize account submissions assoc 2024-02-27 13:29:18 +02:00
Pete Matsyburka 970977e469 allow to drag existing field on the page 2024-02-27 01:41:57 +02:00
Pete Matsyburka f83674b934 share template only hosted 2024-02-26 18:02:24 +02:00
Pete Matsyburka 57c7dc1b5e adjust template clone response 2024-02-25 22:53:35 +02:00
Pete Matsyburka e9277f4eeb use node 20 in GH actions 2024-02-25 21:17:50 +02:00
Pete Matsyburka 4052b6fe13 puma pid /dev/null 2024-02-25 21:15:00 +02:00
Pete Matsyburka 608347b1e6 adjust pdf processor 2024-02-25 17:19:51 +02:00
Pete Matsyburka 345fe674d4 store original doc uuid in metadata 2024-02-25 16:25:04 +02:00
Pete Matsyburka 92f8901a67 better log 2024-02-25 13:11:13 +02:00
Pete Matsyburka fcd40308ee allow to resent copy email 2024-02-25 12:53:36 +02:00
Pete Matsyburka 2bd023e5d0 fix i18n 2024-02-25 00:33:34 +02:00
Pete Matsyburka a697d6330e allow to disable completed submission emails 2024-02-25 00:08:59 +02:00
Pete Matsyburka ab56e8be20 retry webhooks on timeout 2024-02-24 20:24:43 +02:00
Pete Matsyburka b9a906b569 optimize templates api 2024-02-24 19:59:50 +02:00
Pete Matsyburka 02adbae758 add expand prop 2024-02-24 17:35:38 +02:00
Pete Matsyburka 866aab7fac fix submission preview page 2024-02-24 15:36:57 +02:00
Pete Matsyburka c81b4d855b check signed blob data purpose 2024-02-24 13:04:14 +02:00
Pete Matsyburka 4ad58fc285 not found on wrong blob signature 2024-02-24 13:00:09 +02:00
Pete Matsyburka 8227dd4f6a add formula placeholder 2024-02-24 12:04:08 +02:00
Pete Matsyburka beb677734f do not skip required checkboxes 2024-02-23 19:38:53 +02:00
Pete Matsyburka 03db636d51 add number field type 2024-02-23 15:58:49 +02:00
Pete Matsyburka b83abc9499 fix copy field to multiple pages 2024-02-23 10:50:53 +02:00
Pete Matsyburka 9738cdcf8f fix preview page images 2024-02-23 02:15:33 +02:00
Pete Matsyburka 179ae01551 remove rollbar 2024-02-21 20:16:10 +02:00
Pete Matsyburka 295860a9de update readme 2024-02-21 17:07:56 +02:00
Pete Matsyburka 0a6c15b6b8 add AR form i18n 2024-02-21 16:45:34 +02:00
Pete Matsyburka 18f80020ee update readme 2024-02-21 10:44:19 +02:00
Pete Matsyburka 3768fb94bf prefill stamp field 2024-02-21 02:32:31 +02:00
Pete Matsyburka 7b2cadcb91 add default draw field prop 2024-02-21 02:04:44 +02:00
438 changed files with 16469 additions and 4084 deletions
+1 -1
View File
@@ -9,7 +9,7 @@
"rules": {
"vue/no-deprecated-html-element-is": 0,
"vue/no-mutating-props": 0,
"vue/no-v-html": 0
"vue/one-component-per-file": 0
},
"parserOptions": {
"ecmaVersion": 2022,
+7 -5
View File
@@ -11,7 +11,7 @@ jobs:
- name: Install Ruby
uses: ruby/setup-ruby@v1
with:
ruby-version: 3.2.2
ruby-version: 3.3.4
- name: Cache gems
uses: actions/cache@v1
with:
@@ -35,7 +35,7 @@ jobs:
- name: Install Ruby
uses: ruby/setup-ruby@v1
with:
ruby-version: 3.2.2
ruby-version: 3.3.4
- name: Cache gems
uses: actions/cache@v1
with:
@@ -59,7 +59,7 @@ jobs:
- name: Install Node.js
uses: actions/setup-node@v1
with:
node-version: 16.13.1
node-version: 20.9.0
- name: Cache directory path
id: yarn-cache-dir-path
run: echo "::set-output name=dir::$(yarn cache dir)"
@@ -100,13 +100,15 @@ jobs:
- name: Install Ruby
uses: ruby/setup-ruby@v1
with:
ruby-version: 3.2.2
ruby-version: 3.3.4
- name: Set up Node
uses: actions/setup-node@v1
with:
node-version: 16.13.1
node-version: 20.9.0
- name: Install Chrome
uses: browser-actions/setup-chrome@latest
with:
chrome-version: 125
- name: Cache node_modules
uses: actions/cache@v1
with:
+1
View File
@@ -36,3 +36,4 @@ yarn-debug.log*
/attachments
/docuseal
/ee
dump.rdb
+10 -3
View File
@@ -10,7 +10,8 @@ AllCops:
- node_modules/**/*
- bin/*
- vendor/**/*
TargetRubyVersion: '3.2'
TargetRubyVersion: '3.3'
SuggestExtensions: false
Metrics/BlockLength:
Exclude:
@@ -33,6 +34,7 @@ Metrics/MethodLength:
Max: 30
Exclude:
- 'db/migrate/**'
- 'spec/**/*'
Metrics/CyclomaticComplexity:
Max: 15
@@ -44,7 +46,9 @@ Layout/LineLength:
AllowedPatterns: ['\A\s*#']
Metrics/AbcSize:
Max: 40
Max: 45
Exclude:
- spec/**/*
Metrics/ModuleLength:
Max: 500
@@ -64,6 +68,9 @@ RSpec/ExampleLength:
RSpec/MultipleMemoizedHelpers:
Max: 6
Metrics/BlockNesting:
Max: 4
Rails/I18nLocaleTexts:
Enabled: false
@@ -76,5 +83,5 @@ Rails/SkipsModelValidations:
Rails/ApplicationController:
Enabled: false
Capybara/ClickLinkOrButtonStyle:
Rails/Output:
Enabled: false
+28 -11
View File
@@ -1,10 +1,12 @@
FROM ruby:3.2.2-alpine3.18 as fonts
FROM ruby:3.3.4-alpine as fonts
WORKDIR /fonts
RUN apk --no-cache add wget && wget https://github.com/satbyy/go-noto-universal/releases/download/v7.0/GoNotoKurrent-Regular.ttf && wget https://github.com/satbyy/go-noto-universal/releases/download/v7.0/GoNotoKurrent-Bold.ttf && wget https://github.com/impallari/DancingScript/raw/master/fonts/DancingScript-Regular.otf && wget https://github.com/impallari/DancingScript/raw/master/OFL.txt
RUN apk --no-cache add fontforge wget && wget https://github.com/satbyy/go-noto-universal/releases/download/v7.0/GoNotoKurrent-Regular.ttf && wget https://github.com/satbyy/go-noto-universal/releases/download/v7.0/GoNotoKurrent-Bold.ttf && wget https://github.com/impallari/DancingScript/raw/master/fonts/DancingScript-Regular.otf && wget https://cdn.jsdelivr.net/gh/notofonts/notofonts.github.io/fonts/NotoSansSymbols2/hinted/ttf/NotoSansSymbols2-Regular.ttf && wget https://github.com/Maxattax97/gnu-freefont/raw/master/ttf/FreeSans.ttf && wget https://github.com/impallari/DancingScript/raw/master/OFL.txt
FROM ruby:3.2.2-alpine3.18 as webpack
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.3.4-alpine as webpack
ENV RAILS_ENV=production
ENV NODE_ENV=production
@@ -21,27 +23,41 @@ RUN yarn install --network-timeout 1000000
COPY ./bin/shakapacker ./bin/shakapacker
COPY ./config/webpack ./config/webpack
COPY ./config/shakapacker.yml ./config/shakapacker.yml
COPY ./postcss.config.js ./postcss.config.js ./
COPY ./tailwind.config.js ./tailwind.config.js ./
COPY ./tailwind.form.config.js ./tailwind.form.config.js ./
COPY ./tailwind.application.config.js ./tailwind.application.config.js ./
COPY ./postcss.config.js ./postcss.config.js
COPY ./tailwind.config.js ./tailwind.config.js
COPY ./tailwind.form.config.js ./tailwind.form.config.js
COPY ./tailwind.application.config.js ./tailwind.application.config.js
COPY ./app/javascript ./app/javascript
COPY ./app/views ./app/views
RUN echo "gem 'shakapacker'" > Gemfile && ./bin/shakapacker
FROM ruby:3.2.2-alpine3.18 as app
FROM ruby:3.3.4-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
WORKDIR /app
RUN apk add --no-cache build-base sqlite-dev libpq-dev mariadb-dev vips-dev vips-poppler poppler-utils vips-heif libc6-compat ttf-freefont && mkdir /fonts
RUN apk add --no-cache sqlite-dev libpq-dev mariadb-dev vips-dev vips-poppler poppler-utils redis vips-heif gcompat ttf-freefont && mkdir /fonts && rm /usr/share/fonts/freefont/FreeSans.otf
RUN echo $'.include = /etc/ssl/openssl.cnf\n\
\n\
[provider_sect]\n\
default = default_sect\n\
legacy = legacy_sect\n\
\n\
[default_sect]\n\
activate = 1\n\
\n\
[legacy_sect]\n\
activate = 1' >> /app/openssl_legacy.cnf
COPY ./Gemfile ./Gemfile.lock ./
RUN bundle update --bundler && bundle install && rm -rf ~/.bundle /usr/local/bundle/cache && ruby -e "puts Dir['/usr/local/bundle/**/{spec,rdoc,resources/shared,resources/collation,resources/locales}']" | xargs rm -rf
RUN apk add --no-cache build-base && bundle install && apk del 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
COPY ./bin ./bin
COPY ./app ./app
@@ -54,6 +70,7 @@ COPY ./tmp ./tmp
COPY LICENSE README.md Rakefile config.ru .version ./
COPY --from=fonts /fonts/GoNotoKurrent-Regular.ttf /fonts/GoNotoKurrent-Bold.ttf /fonts/DancingScript-Regular.otf /fonts/OFL.txt /fonts
COPY --from=fonts /fonts/FreeSans.ttf /usr/share/fonts/freefont
COPY --from=webpack /app/public/packs ./public/packs
RUN ln -s /fonts /app/public/fonts
@@ -63,4 +80,4 @@ WORKDIR /data/docuseal
ENV WORKDIR=/data/docuseal
EXPOSE 3000
CMD ["/app/bin/rails", "server"]
CMD ["/app/bin/bundle", "exec", "puma", "-C", "/app/config/puma.rb", "--dir", "/app"]
+7 -6
View File
@@ -2,13 +2,15 @@
source 'https://rubygems.org'
ruby '3.2.2'
ruby '3.3.4'
gem 'arabic-letter-connector', require: 'arabic-letter-connector/logic'
gem 'aws-sdk-s3', require: false
gem 'aws-sdk-secretsmanager', require: false
gem 'azure-storage-blob', require: false
gem 'bootsnap', require: false
gem 'cancancan'
gem 'csv'
gem 'devise'
gem 'devise-two-factor'
gem 'dotenv', require: false
@@ -21,25 +23,24 @@ gem 'image_processing'
gem 'jwt'
gem 'lograge'
gem 'mysql2', require: false
gem 'net-smtp', '0.4.0'
gem 'oj'
gem 'pagy'
gem 'pdf-reader'
gem 'pg', require: false
gem 'premailer-rails'
gem 'pretender'
gem 'puma'
gem 'puma', require: false
gem 'rack'
gem 'rails'
gem 'rails_autolink'
gem 'rails-i18n'
gem 'rollbar', require: ENV.key?('ROLLBAR_ACCESS_TOKEN')
gem 'rotp'
gem 'rqrcode'
gem 'ruby-vips'
gem 'rubyXL'
gem 'shakapacker'
gem 'sidekiq', require: ENV.key?('REDIS_URL')
gem 'sqlite3', require: false
gem 'sidekiq'
gem 'sqlite3', require: false, force_ruby_platform: true
gem 'strip_attributes'
gem 'turbo-rails'
gem 'twitter_cldr', require: false
+195 -197
View File
@@ -1,36 +1,35 @@
GEM
remote: https://rubygems.org/
specs:
Ascii85 (1.1.0)
actioncable (7.1.2)
actionpack (= 7.1.2)
activesupport (= 7.1.2)
actioncable (7.1.3.4)
actionpack (= 7.1.3.4)
activesupport (= 7.1.3.4)
nio4r (~> 2.0)
websocket-driver (>= 0.6.1)
zeitwerk (~> 2.6)
actionmailbox (7.1.2)
actionpack (= 7.1.2)
activejob (= 7.1.2)
activerecord (= 7.1.2)
activestorage (= 7.1.2)
activesupport (= 7.1.2)
actionmailbox (7.1.3.4)
actionpack (= 7.1.3.4)
activejob (= 7.1.3.4)
activerecord (= 7.1.3.4)
activestorage (= 7.1.3.4)
activesupport (= 7.1.3.4)
mail (>= 2.7.1)
net-imap
net-pop
net-smtp
actionmailer (7.1.2)
actionpack (= 7.1.2)
actionview (= 7.1.2)
activejob (= 7.1.2)
activesupport (= 7.1.2)
actionmailer (7.1.3.4)
actionpack (= 7.1.3.4)
actionview (= 7.1.3.4)
activejob (= 7.1.3.4)
activesupport (= 7.1.3.4)
mail (~> 2.5, >= 2.5.4)
net-imap
net-pop
net-smtp
rails-dom-testing (~> 2.2)
actionpack (7.1.2)
actionview (= 7.1.2)
activesupport (= 7.1.2)
actionpack (7.1.3.4)
actionview (= 7.1.3.4)
activesupport (= 7.1.3.4)
nokogiri (>= 1.8.5)
racc
rack (>= 2.2.4)
@@ -38,35 +37,35 @@ GEM
rack-test (>= 0.6.3)
rails-dom-testing (~> 2.2)
rails-html-sanitizer (~> 1.6)
actiontext (7.1.2)
actionpack (= 7.1.2)
activerecord (= 7.1.2)
activestorage (= 7.1.2)
activesupport (= 7.1.2)
actiontext (7.1.3.4)
actionpack (= 7.1.3.4)
activerecord (= 7.1.3.4)
activestorage (= 7.1.3.4)
activesupport (= 7.1.3.4)
globalid (>= 0.6.0)
nokogiri (>= 1.8.5)
actionview (7.1.2)
activesupport (= 7.1.2)
actionview (7.1.3.4)
activesupport (= 7.1.3.4)
builder (~> 3.1)
erubi (~> 1.11)
rails-dom-testing (~> 2.2)
rails-html-sanitizer (~> 1.6)
activejob (7.1.2)
activesupport (= 7.1.2)
activejob (7.1.3.4)
activesupport (= 7.1.3.4)
globalid (>= 0.3.6)
activemodel (7.1.2)
activesupport (= 7.1.2)
activerecord (7.1.2)
activemodel (= 7.1.2)
activesupport (= 7.1.2)
activemodel (7.1.3.4)
activesupport (= 7.1.3.4)
activerecord (7.1.3.4)
activemodel (= 7.1.3.4)
activesupport (= 7.1.3.4)
timeout (>= 0.4.0)
activestorage (7.1.2)
actionpack (= 7.1.2)
activejob (= 7.1.2)
activerecord (= 7.1.2)
activesupport (= 7.1.2)
activestorage (7.1.3.4)
actionpack (= 7.1.3.4)
activejob (= 7.1.3.4)
activerecord (= 7.1.3.4)
activesupport (= 7.1.3.4)
marcel (~> 1.0)
activesupport (7.1.2)
activesupport (7.1.3.4)
base64
bigdecimal
concurrent-ruby (~> 1.0, >= 1.0.2)
@@ -78,26 +77,28 @@ GEM
tzinfo (~> 2.0)
addressable (2.8.6)
public_suffix (>= 2.0.2, < 6.0)
afm (0.2.2)
annotate (3.2.0)
activerecord (>= 3.2, < 8.0)
rake (>= 10.4, < 14.0)
arabic-letter-connector (0.1.1)
ast (2.4.2)
aws-eventstream (1.3.0)
aws-partitions (1.874.0)
aws-sdk-core (3.190.1)
aws-partitions (1.913.0)
aws-sdk-core (3.191.6)
aws-eventstream (~> 1, >= 1.3.0)
aws-partitions (~> 1, >= 1.651.0)
aws-sigv4 (~> 1.8)
jmespath (~> 1, >= 1.6.1)
aws-sdk-kms (1.75.0)
aws-sdk-core (~> 3, >= 3.188.0)
aws-sdk-kms (1.79.0)
aws-sdk-core (~> 3, >= 3.191.0)
aws-sigv4 (~> 1.1)
aws-sdk-s3 (1.142.0)
aws-sdk-core (~> 3, >= 3.189.0)
aws-sdk-s3 (1.146.1)
aws-sdk-core (~> 3, >= 3.191.0)
aws-sdk-kms (~> 1)
aws-sigv4 (~> 1.8)
aws-sdk-secretsmanager (1.91.0)
aws-sdk-core (~> 3, >= 3.191.0)
aws-sigv4 (~> 1.1)
aws-sigv4 (1.8.0)
aws-eventstream (~> 1, >= 1.0.2)
azure-storage-blob (2.0.3)
@@ -110,69 +111,71 @@ GEM
nokogiri (~> 1, >= 1.10.8)
base64 (0.2.0)
bcrypt (3.1.20)
better_html (2.0.2)
better_html (2.1.1)
actionview (>= 6.0)
activesupport (>= 6.0)
ast (~> 2.0)
erubi (~> 1.4)
parser (>= 2.4)
smart_properties
bigdecimal (3.1.5)
bigdecimal (3.1.8)
bindex (0.8.1)
bootsnap (1.17.0)
bootsnap (1.18.3)
msgpack (~> 1.2)
builder (3.2.4)
bullet (7.1.4)
builder (3.3.0)
bullet (7.1.6)
activesupport (>= 3.0.0)
uniform_notifier (~> 1.11)
camertron-eprun (1.1.1)
cancancan (3.5.0)
capybara (3.39.2)
capybara (3.40.0)
addressable
matrix
mini_mime (>= 0.1.3)
nokogiri (~> 1.8)
nokogiri (~> 1.11)
rack (>= 1.6.0)
rack-test (>= 0.6.3)
regexp_parser (>= 1.5, < 3.0)
xpath (~> 3.2)
childprocess (5.0.0)
chunky_png (1.4.0)
cldr-plurals-runtime-rb (1.1.0)
cmdparse (3.0.7)
coderay (1.1.3)
concurrent-ruby (1.2.2)
concurrent-ruby (1.3.4)
connection_pool (2.4.1)
crack (0.4.5)
crack (1.0.0)
bigdecimal
rexml
crass (1.0.6)
css_parser (1.16.0)
css_parser (1.17.1)
addressable
csv (3.3.0)
cuprite (0.15)
capybara (~> 3.0)
ferrum (~> 0.14.0)
date (3.3.4)
debug (1.9.1)
debug (1.9.2)
irb (~> 1.10)
reline (>= 0.3.8)
declarative (0.0.20)
devise (4.9.3)
devise (4.9.4)
bcrypt (~> 3.0)
orm_adapter (~> 0.1)
railties (>= 4.1.0)
responders
warden (~> 1.2.3)
devise-two-factor (5.0.0)
devise-two-factor (6.0.0)
activesupport (~> 7.0)
devise (~> 4.0)
railties (~> 7.0)
rotp (~> 6.0)
diff-lcs (1.5.0)
diff-lcs (1.5.1)
digest-crc (0.6.5)
rake (>= 12.0.0, < 14.0.0)
docile (1.4.0)
dotenv (2.8.1)
drb (2.2.0)
ruby2_keywords
dotenv (3.1.0)
drb (2.2.1)
email_typo (0.2.3)
erb_lint (0.5.0)
activesupport
@@ -181,13 +184,13 @@ GEM
rainbow
rubocop
smart_properties
erubi (1.12.0)
factory_bot (6.4.4)
erubi (1.13.0)
factory_bot (6.4.6)
activesupport (>= 5.0.0)
factory_bot_rails (6.4.2)
factory_bot_rails (6.4.3)
factory_bot (~> 6.4)
railties (>= 5.0.0)
faker (3.2.2)
faker (3.3.1)
i18n (>= 1.8.11, < 2)
faraday (1.10.3)
faraday-em_http (~> 1.0)
@@ -225,34 +228,34 @@ GEM
geom2d (0.4.1)
globalid (1.2.1)
activesupport (>= 6.1)
google-apis-core (0.11.2)
google-apis-core (0.14.1)
addressable (~> 2.5, >= 2.5.1)
googleauth (>= 0.16.2, < 2.a)
googleauth (~> 1.9)
httpclient (>= 2.8.1, < 3.a)
mini_mime (~> 1.0)
representable (~> 3.0)
retriable (>= 2.0, < 4.a)
rexml
webrick
google-apis-iamcredentials_v1 (0.17.0)
google-apis-core (>= 0.11.0, < 2.a)
google-apis-storage_v1 (0.29.0)
google-apis-core (>= 0.11.0, < 2.a)
google-cloud-core (1.6.1)
google-apis-iamcredentials_v1 (0.20.0)
google-apis-core (>= 0.14.0, < 2.a)
google-apis-storage_v1 (0.37.0)
google-apis-core (>= 0.14.0, < 2.a)
google-cloud-core (1.7.0)
google-cloud-env (>= 1.0, < 3.a)
google-cloud-errors (~> 1.0)
google-cloud-env (2.1.0)
google-cloud-env (2.1.1)
faraday (>= 1.0, < 3.a)
google-cloud-errors (1.3.1)
google-cloud-storage (1.45.0)
google-cloud-errors (1.4.0)
google-cloud-storage (1.49.0)
addressable (~> 2.8)
digest-crc (~> 0.4)
google-apis-iamcredentials_v1 (~> 0.1)
google-apis-storage_v1 (~> 0.29.0)
google-apis-core (~> 0.13)
google-apis-iamcredentials_v1 (~> 0.18)
google-apis-storage_v1 (~> 0.33)
google-cloud-core (~> 1.6)
googleauth (>= 0.16.2, < 2.a)
googleauth (~> 1.9)
mini_mime (~> 1.0)
googleauth (1.9.1)
googleauth (1.11.0)
faraday (>= 1.0, < 3.a)
google-cloud-env (~> 2.1)
jwt (>= 1.4, < 3.0)
@@ -260,30 +263,31 @@ GEM
os (>= 0.9, < 2.0)
signet (>= 0.16, < 2.a)
hashdiff (1.1.0)
hashery (2.1.2)
hexapdf (0.37.1)
hexapdf (0.42.0)
cmdparse (~> 3.0, >= 3.0.3)
geom2d (~> 0.4, >= 0.4.1)
openssl (>= 2.2.1)
htmlentities (4.3.4)
httpclient (2.8.3)
i18n (1.14.1)
i18n (1.14.6)
concurrent-ruby (~> 1.0)
image_processing (1.12.2)
mini_magick (>= 4.9.5, < 5)
ruby-vips (>= 2.0.17, < 3)
io-console (0.7.1)
irb (1.11.0)
rdoc
reline (>= 0.3.8)
io-console (0.7.2)
irb (1.14.0)
rdoc (>= 4.0.0)
reline (>= 0.4.2)
jmespath (1.6.2)
json (2.7.1)
jwt (2.7.1)
json (2.7.2)
jwt (2.8.1)
base64
language_server-protocol (3.17.0.3)
launchy (2.5.2)
launchy (3.0.0)
addressable (~> 2.8)
letter_opener (1.8.1)
launchy (>= 2.2, < 3)
childprocess (~> 5.0)
letter_opener (1.10.0)
launchy (>= 2.2, < 4)
letter_opener_web (2.0.0)
actionmailer (>= 5.2)
letter_opener (~> 1.7)
@@ -302,21 +306,21 @@ GEM
net-imap
net-pop
net-smtp
marcel (1.0.2)
marcel (1.0.4)
matrix (0.4.2)
method_source (1.0.0)
method_source (1.1.0)
mini_magick (4.12.0)
mini_mime (1.1.5)
mini_portile2 (2.8.5)
minitest (5.20.0)
mini_portile2 (2.8.7)
minitest (5.25.1)
msgpack (1.7.2)
multi_json (1.15.0)
multipart-post (2.3.0)
multipart-post (2.4.0)
mutex_m (0.2.0)
mysql2 (0.5.5)
mysql2 (0.5.6)
net-http-persistent (4.0.2)
connection_pool (~> 2.2)
net-imap (0.4.9)
net-imap (0.4.14)
date
net-protocol
net-pop (0.1.2)
@@ -325,28 +329,27 @@ GEM
timeout
net-smtp (0.4.0)
net-protocol
nio4r (2.7.0)
nokogiri (1.15.5)
nio4r (2.7.3)
nokogiri (1.16.7)
mini_portile2 (~> 2.8.2)
racc (~> 1.4)
nokogiri (1.16.7-aarch64-linux)
racc (~> 1.4)
nokogiri (1.16.7-x86_64-linux)
racc (~> 1.4)
oj (3.16.3)
bigdecimal (>= 3.0)
openssl (3.2.0)
orm_adapter (0.5.0)
os (1.1.4)
pagy (6.2.0)
parallel (1.24.0)
parser (3.2.2.4)
package_json (0.1.0)
pagy (8.1.2)
parallel (1.25.1)
parser (3.3.4.0)
ast (~> 2.4.1)
racc
pdf-reader (2.12.0)
Ascii85 (~> 1.0)
afm (~> 0.2.1)
hashery (~> 2.0)
ruby-rc4
ttfunk
pg (1.5.4)
premailer (1.21.0)
pg (1.5.6)
premailer (1.23.0)
addressable
css_parser (>= 1.12.0)
htmlentities (>= 4.0.0)
@@ -363,34 +366,34 @@ GEM
pry (>= 0.10.4)
psych (5.1.2)
stringio
public_suffix (5.0.4)
puma (6.4.0)
public_suffix (5.0.5)
puma (6.4.3)
nio4r (~> 2.0)
racc (1.7.3)
rack (2.2.8)
racc (1.8.1)
rack (3.1.7)
rack-proxy (0.7.7)
rack
rack-session (1.0.2)
rack (< 3)
rack-session (2.0.0)
rack (>= 3.0.0)
rack-test (2.1.0)
rack (>= 1.3)
rackup (1.0.0)
rack (< 3)
webrick
rails (7.1.2)
actioncable (= 7.1.2)
actionmailbox (= 7.1.2)
actionmailer (= 7.1.2)
actionpack (= 7.1.2)
actiontext (= 7.1.2)
actionview (= 7.1.2)
activejob (= 7.1.2)
activemodel (= 7.1.2)
activerecord (= 7.1.2)
activestorage (= 7.1.2)
activesupport (= 7.1.2)
rackup (2.1.0)
rack (>= 3)
webrick (~> 1.8)
rails (7.1.3.4)
actioncable (= 7.1.3.4)
actionmailbox (= 7.1.3.4)
actionmailer (= 7.1.3.4)
actionpack (= 7.1.3.4)
actiontext (= 7.1.3.4)
actionview (= 7.1.3.4)
activejob (= 7.1.3.4)
activemodel (= 7.1.3.4)
activerecord (= 7.1.3.4)
activestorage (= 7.1.3.4)
activesupport (= 7.1.3.4)
bundler (>= 1.15.0)
railties (= 7.1.2)
railties (= 7.1.3.4)
rails-dom-testing (2.2.0)
activesupport (>= 5.0.0)
minitest
@@ -398,114 +401,108 @@ GEM
rails-html-sanitizer (1.6.0)
loofah (~> 2.21)
nokogiri (~> 1.14)
rails-i18n (7.0.8)
rails-i18n (7.0.9)
i18n (>= 0.7, < 2)
railties (>= 6.0.0, < 8)
rails_autolink (1.1.8)
actionview (> 3.1)
activesupport (> 3.1)
railties (> 3.1)
railties (7.1.2)
actionpack (= 7.1.2)
activesupport (= 7.1.2)
railties (7.1.3.4)
actionpack (= 7.1.3.4)
activesupport (= 7.1.3.4)
irb
rackup (>= 1.0.0)
rake (>= 12.2)
thor (~> 1.0, >= 1.2.2)
zeitwerk (~> 2.6)
rainbow (3.1.1)
rake (13.1.0)
rdoc (6.6.2)
rake (13.2.1)
rdoc (6.7.0)
psych (>= 4.0.0)
redis-client (0.19.1)
redis-client (0.22.2)
connection_pool
regexp_parser (2.8.3)
reline (0.4.1)
regexp_parser (2.9.2)
reline (0.5.10)
io-console (~> 0.5)
representable (3.2.0)
declarative (< 0.1.0)
trailblazer-option (>= 0.1.1, < 0.2.0)
uber (< 0.2.0)
request_store (1.5.1)
request_store (1.6.0)
rack (>= 1.4)
responders (3.1.1)
actionpack (>= 5.2)
railties (>= 5.2)
retriable (3.1.2)
rexml (3.2.6)
rollbar (3.4.2)
rexml (3.3.6)
strscan
rotp (6.3.0)
rqrcode (2.2.0)
chunky_png (~> 1.0)
rqrcode_core (~> 1.0)
rqrcode_core (1.2.0)
rspec-core (3.12.2)
rspec-support (~> 3.12.0)
rspec-expectations (3.12.3)
rspec-core (3.13.0)
rspec-support (~> 3.13.0)
rspec-expectations (3.13.0)
diff-lcs (>= 1.2.0, < 2.0)
rspec-support (~> 3.12.0)
rspec-mocks (3.12.6)
rspec-support (~> 3.13.0)
rspec-mocks (3.13.0)
diff-lcs (>= 1.2.0, < 2.0)
rspec-support (~> 3.12.0)
rspec-rails (6.1.0)
rspec-support (~> 3.13.0)
rspec-rails (6.1.2)
actionpack (>= 6.1)
activesupport (>= 6.1)
railties (>= 6.1)
rspec-core (~> 3.12)
rspec-expectations (~> 3.12)
rspec-mocks (~> 3.12)
rspec-support (~> 3.12)
rspec-support (3.12.1)
rubocop (1.59.0)
rspec-core (~> 3.13)
rspec-expectations (~> 3.13)
rspec-mocks (~> 3.13)
rspec-support (~> 3.13)
rspec-support (3.13.1)
rubocop (1.65.0)
json (~> 2.3)
language_server-protocol (>= 3.17.0)
parallel (~> 1.10)
parser (>= 3.2.2.4)
parser (>= 3.3.0.2)
rainbow (>= 2.2.2, < 4.0)
regexp_parser (>= 1.8, < 3.0)
regexp_parser (>= 2.4, < 3.0)
rexml (>= 3.2.5, < 4.0)
rubocop-ast (>= 1.30.0, < 2.0)
rubocop-ast (>= 1.31.1, < 2.0)
ruby-progressbar (~> 1.7)
unicode-display_width (>= 2.4.0, < 3.0)
rubocop-ast (1.30.0)
parser (>= 3.2.1.0)
rubocop-capybara (2.19.0)
rubocop (~> 1.41)
rubocop-factory_bot (2.24.0)
rubocop (~> 1.33)
rubocop-performance (1.20.1)
rubocop-ast (1.31.3)
parser (>= 3.3.1.0)
rubocop-performance (1.21.0)
rubocop (>= 1.48.1, < 2.0)
rubocop-ast (>= 1.30.0, < 2.0)
rubocop-rails (2.23.1)
rubocop-ast (>= 1.31.1, < 2.0)
rubocop-rails (2.24.1)
activesupport (>= 4.2.0)
rack (>= 1.1)
rubocop (>= 1.33.0, < 2.0)
rubocop-ast (>= 1.30.0, < 2.0)
rubocop-rspec (2.25.0)
rubocop (~> 1.40)
rubocop-capybara (~> 2.17)
rubocop-factory_bot (~> 2.22)
rubocop-ast (>= 1.31.1, < 2.0)
rubocop-rspec (3.0.3)
rubocop (~> 1.61)
ruby-progressbar (1.13.0)
ruby-rc4 (0.1.5)
ruby-vips (2.2.0)
ruby-vips (2.2.1)
ffi (~> 1.12)
ruby2_keywords (0.0.5)
rubyXL (3.4.25)
rubyXL (3.4.27)
nokogiri (>= 1.10.8)
rubyzip (>= 1.3.0)
rubyzip (2.3.2)
semantic_range (3.0.0)
shakapacker (7.1.0)
shakapacker (8.0.0)
activesupport (>= 5.2)
package_json
rack-proxy (>= 0.6.1)
railties (>= 5.2)
semantic_range (>= 2.3.0)
sidekiq (7.2.0)
sidekiq (7.2.4)
concurrent-ruby (< 2)
connection_pool (>= 2.3.0)
rack (>= 2.2.4)
redis-client (>= 0.14.0)
signet (0.18.0)
redis-client (>= 0.19.0)
signet (0.19.0)
addressable (~> 2.8)
faraday (>= 0.17.5, < 3.a)
jwt (>= 1.5, < 3.0)
@@ -517,17 +514,16 @@ GEM
simplecov-html (0.12.3)
simplecov_json_formatter (0.1.4)
smart_properties (1.17.0)
sqlite3 (1.7.0)
sqlite3 (1.7.3)
mini_portile2 (~> 2.8.0)
sqlite3 (1.7.0-arm64-darwin)
stringio (3.1.0)
stringio (3.1.1)
strip_attributes (1.13.0)
activemodel (>= 3.0, < 8.0)
thor (1.3.0)
strscan (3.1.0)
thor (1.3.2)
timeout (0.4.1)
trailblazer-option (0.1.2)
ttfunk (1.7.0)
turbo-rails (1.5.0)
turbo-rails (2.0.5)
actionpack (>= 6.0.0)
activejob (>= 6.0.0)
railties (>= 6.0.0)
@@ -537,7 +533,7 @@ GEM
tzinfo
tzinfo (2.0.6)
concurrent-ruby (~> 1.0)
tzinfo-data (1.2023.4)
tzinfo-data (1.2024.1)
tzinfo (>= 1.0.0)
uber (0.1.0)
unicode-display_width (2.5.0)
@@ -549,7 +545,7 @@ GEM
activemodel (>= 6.0.0)
bindex (>= 0.4.0)
railties (>= 6.0.0)
webmock (3.19.1)
webmock (3.23.0)
addressable (>= 2.8.0)
crack (>= 0.3.2)
hashdiff (>= 0.4.0, < 2.0.0)
@@ -559,22 +555,25 @@ GEM
websocket-extensions (0.1.5)
xpath (3.2.0)
nokogiri (~> 1.8)
zeitwerk (2.6.12)
zeitwerk (2.6.18)
PLATFORMS
arm64-darwin-22
aarch64-linux
ruby
x86_64-linux
DEPENDENCIES
annotate
arabic-letter-connector
aws-sdk-s3
aws-sdk-secretsmanager
azure-storage-blob
better_html
bootsnap
bullet
cancancan
capybara
csv
cuprite
debug
devise
@@ -593,9 +592,9 @@ DEPENDENCIES
letter_opener_web
lograge
mysql2
net-smtp (= 0.4.0)
oj
pagy
pdf-reader
pg
premailer-rails
pretender
@@ -605,7 +604,6 @@ DEPENDENCIES
rails
rails-i18n
rails_autolink
rollbar
rotp
rqrcode
rspec-rails
@@ -627,7 +625,7 @@ DEPENDENCIES
webmock
RUBY VERSION
ruby 3.2.2p53
ruby 3.3.4p94
BUNDLED WITH
2.5.3
+12 -6
View File
@@ -42,6 +42,7 @@ DocuSeal is an open source platform that provides secure and efficient digital d
- PDF signature verification
- Users management
- Mobile-optimized
- Signing available in 13 languages
- API and Webhooks for integrations
- Easy to deploy in minutes
@@ -50,11 +51,13 @@ DocuSeal is an open source platform that provides secure and efficient digital d
- User roles
- Automated reminders
- Invitation and identify verification via SMS
- Conditional fields and formulas
- Bulk send with CSV, XLSX spreadsheet import
- SSO / SAML
- Template creation with HTML API ([Guide](https://www.docuseal.co/guides/create-pdf-document-fillable-form-with-html-api))
- Template creation with PDF or DOCX and text tags API ([Guide](https://www.docuseal.co/guides/use-embedded-text-field-tags-in-the-pdf-to-create-a-fillable-form))
- Embedded signing form ([React](https://github.com/docusealco/docuseal-react), [Vue](https://github.com/docusealco/docuseal-vue) or [JavaScript](https://www.docuseal.co/docs/embedded))
- Embedded document form builder ([React](https://github.com/docusealco/docuseal-react), [Vue](https://github.com/docusealco/docuseal-vue) or [JavaScript](https://www.docuseal.co/docs/embedded))
- Template creation with PDF or DOCX and field tags API ([Guide](https://www.docuseal.co/guides/use-embedded-text-field-tags-in-the-pdf-to-create-a-fillable-form))
- Embedded signing form ([React](https://github.com/docusealco/docuseal-react), [Vue](https://github.com/docusealco/docuseal-vue), [Angular](https://github.com/docusealco/docuseal-angular) or [JavaScript](https://www.docuseal.co/docs/embedded))
- Embedded document form builder ([React](https://github.com/docusealco/docuseal-react), [Vue](https://github.com/docusealco/docuseal-vue), [Angular](https://github.com/docusealco/docuseal-angular) or [JavaScript](https://www.docuseal.co/docs/embedded))
- [Learn more](https://www.docuseal.co/pricing)
## Deploy
@@ -64,9 +67,6 @@ DocuSeal is an open source platform that provides secure and efficient digital d
| [<img alt="Deploy on Heroku" src="https://www.herokucdn.com/deploy/button.svg" height="40">](https://heroku.com/deploy?template=https://github.com/docusealco/docuseal-heroku) | [<img alt="Deploy on Railway" src="https://railway.app/button.svg" height="40">](https://railway.app/template/IGoDnc?referralCode=ruU7JR)|
|**DigitalOcean**|**Render**|
| [<img alt="Deploy on DigitalOcean" src="https://www.deploytodo.com/do-btn-blue.svg" height="40">](https://cloud.digitalocean.com/apps/new?repo=https://github.com/docusealco/docuseal-digitalocean/tree/master&refcode=421d50f53990) | [<img alt="Deploy to Render" src="https://render.com/images/deploy-to-render-button.svg" height="40">](https://render.com/deploy?repo=https://github.com/docusealco/docuseal-render)
|**Koyeb**|**Elestio**|
| [<img alt="Deploy on Koyeb" src="https://www.koyeb.com/static/images/deploy/button.svg" height="40">](https://app.koyeb.com/deploy?name=docuseal&type=docker&image=docker.io/docuseal/docuseal&env[PORT]=8000&env[DATABASE_URL]=CHANGE_ME&env[SECRET_KEY_BASE]=CHANGE_ME&ports=8000;http;/) | [<img alt="Deploy on Elestio" src="https://pub-da36157c854648669813f3f76c526c2b.r2.dev/deploy-on-elestio-black.png">](https://dash.elest.io/deploy?soft=DocuSeal&id=339) |
#### Docker
@@ -99,3 +99,9 @@ At DocuSeal we have expertise and technologies to make documents creation, filli
Distributed under the AGPLv3 License. See [LICENSE](https://github.com/docusealco/docuseal/blob/master/LICENSE) for more information.
Unless otherwise noted, all files © 2023 DocuSeal LLC.
## Tools
- [Signature Maker](https://www.docuseal.co/online-signature)
- [Sign Document Online](https://www.docuseal.co/sign-documents-online)
- [Fill PDF Online](https://www.docuseal.co/fill-pdf)
+9
View File
@@ -3,3 +3,12 @@
If you come across any security concern or vulnarability, please report the information via email to security@docuseal.co instead of opening a GitHub issue. We will promptly respond and will collaborate with you to validate the issue, and resolve it ASAP.
**We have a bug bounty program to reward security researchers.**
Out of scope vulnerabilities:
- CSRF
- DNSSEC, CAA, CSP headers
- DNS or email security related
- Rate Limiting
Note: We reserve the right to classify any reported vulnerability as out of scope for the bug bounty program.
+12 -2
View File
@@ -8,9 +8,19 @@ class AccountConfigsController < ApplicationController
AccountConfig::ALLOW_TYPED_SIGNATURE,
AccountConfig::FORCE_MFA,
AccountConfig::ALLOW_TO_RESUBMIT,
AccountConfig::ESIGNING_PREFERENCE_KEY
AccountConfig::FORM_PREFILL_SIGNATURE_KEY,
AccountConfig::ESIGNING_PREFERENCE_KEY,
AccountConfig::FORM_WITH_CONFETTI_KEY,
AccountConfig::DOWNLOAD_LINKS_AUTH_KEY,
AccountConfig::FORCE_SSO_AUTH_KEY,
AccountConfig::FLATTEN_RESULT_PDF_KEY,
AccountConfig::WITH_SIGNATURE_ID,
AccountConfig::COMBINE_PDF_RESULT_KEY,
AccountConfig::REQUIRE_SIGNING_REASON_KEY
].freeze
InvalidKey = Class.new(StandardError)
def create
@account_config.update!(account_config_params)
@@ -20,7 +30,7 @@ class AccountConfigsController < ApplicationController
private
def load_account_config
return head :not_found 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])
+7 -5
View File
@@ -7,7 +7,8 @@ class AccountsController < ApplicationController
'fr-FR' => 'French (France)',
'es-ES' => 'Spanish (Spain)',
'pt-PT' => 'Portuguese (Portugal)',
'de-DE' => 'German (Germany)'
'de-DE' => 'German (Germany)',
'it-IT' => 'Italian (Italy)'
}.freeze
before_action :load_account
@@ -23,7 +24,7 @@ class AccountsController < ApplicationController
@encrypted_config.assign_attributes(app_url_params)
unless URI.parse(@encrypted_config.value.to_s).class.in?([URI::HTTP, URI::HTTPS])
@encrypted_config.errors.add(:value, 'should be a valid URL')
@encrypted_config.errors.add(:value, I18n.t('should_be_a_valid_url'))
return render :show, status: :unprocessable_entity
end
@@ -32,7 +33,7 @@ class AccountsController < ApplicationController
Docuseal.refresh_default_url_options!
redirect_to settings_account_path, notice: 'Account information has been updated'
redirect_to settings_account_path, notice: I18n.t('account_information_has_been_updated')
rescue ActiveRecord::RecordInvalid
render :show, status: :unprocessable_entity
end
@@ -42,11 +43,12 @@ class AccountsController < ApplicationController
true_user.update!(locked_at: Time.current)
# rubocop:disable Layout/LineLength
render turbo_stream: turbo_stream.replace(
:account_delete_button,
html: helpers.tag.p('Your account removal request will be processed within 2 weeks. ' \
'Please contact us if you want to keep your account.')
html: helpers.tag.p(I18n.t('your_account_removal_request_will_be_processed_within_2_weeks_please_contact_us_if_you_want_to_keep_your_account'))
)
# rubocop:enable Layout/LineLength
end
private
@@ -8,11 +8,12 @@ module Api
skip_authorization_check
before_action :set_cors_headers
before_action :set_noindex_headers
def show
blob_uuid, = ApplicationRecord.signed_id_verifier.verified(params[:signed_uuid])
blob_uuid, purp, exp = ApplicationRecord.signed_id_verifier.verified(params[:signed_uuid])
if blob_uuid.blank?
if blob_uuid.blank? || (purp.present? && purp != 'blob') || (exp && exp < Time.current.to_i)
Rollbar.error('Blob not found') if defined?(Rollbar)
return head :not_found
@@ -20,6 +21,8 @@ module Api
blob = ActiveStorage::Blob.find_by!(uuid: blob_uuid)
authorization_check!(blob) if exp.blank?
if request.headers['Range'].present?
send_blob_byte_range_data blob, request.headers['Range']
else
@@ -31,5 +34,22 @@ module Api
end
end
end
private
def authorization_check!(blob)
is_authorized =
blob.attachments.all? do |a|
a.name.in?(%w[logo preview_images]) ||
(current_user && a.record.account.id == current_user.account_id) ||
!a.record.account.account_configs.find_or_initialize_by(key: AccountConfig::DOWNLOAD_LINKS_AUTH_KEY).value
end
return if is_authorized
Rollbar.error('Blob aunauthorized') if defined?(Rollbar)
raise CanCan::AccessDenied
end
end
end
@@ -7,11 +7,16 @@ module Api
skip_before_action :authenticate_user!
skip_authorization_check
before_action :set_cors_headers
before_action :set_noindex_headers
# rubocop:disable Metrics
def show
Rollbar.info('Blob legacy') if defined?(Rollbar)
blob = ActiveStorage::Blob.find_signed!(params[:signed_blob_id] || params[:signed_id])
blob = ActiveStorage::Blob.find_signed(params[:signed_blob_id] || params[:signed_id])
return head :not_found unless blob
is_permitted = blob.attachments.any? do |a|
(current_user && a.record.account.id == current_user.account_id) ||
+28 -13
View File
@@ -19,10 +19,14 @@ module Api
render json: { error: e.message }, status: :unprocessable_entity
end
rescue_from RateLimit::LimitApproached do |e|
Rollbar.error(e) if defined?(Rollbar)
render json: { error: 'Too many requests' }, status: :too_many_requests
end
if Rails.env.production?
rescue_from CanCan::AccessDenied do |e|
Rollbar.warning(e) if defined?(Rollbar)
render json: { error: e.message }, status: :forbidden
end
@@ -35,31 +39,42 @@ module Api
private
def paginate(relation)
result = relation.order(id: :desc)
def paginate(relation, field: :id)
result = relation.order(field => :desc)
.limit([params.fetch(:limit, DEFAULT_LIMIT).to_i, MAX_LIMIT].min)
result = result.where(relation.arel_table[:id].lt(params[:after])) if params[:after].present?
result = result.where(relation.arel_table[:id].gt(params[:before])) if params[:before].present?
if field == :id
result = result.where(id: ...params[:after].to_i) if params[:after].present?
result = result.where(id: (params[:before].to_i + 1)...) if params[:before].present?
else
result = result.where(field => ...params[:after]) if params[:after].present?
result = result.where(field => (params[:before] + 1)...) if params[:before].present?
end
result
end
def authenticate_user!
@current_user ||=
if request.headers['X-Auth-Token'].present?
sha256 = Digest::SHA256.hexdigest(request.headers['X-Auth-Token'])
User.joins(:access_token).active.find_by(access_token: { sha256: })
end
render json: { error: 'Not authenticated' }, status: :unauthorized unless current_user
end
def current_user
super || @current_user ||=
if request.headers['X-Auth-Token'].present?
sha256 = Digest::SHA256.hexdigest(request.headers['X-Auth-Token'])
User.joins(:access_token).active.find_by(access_token: { sha256: })
end
end
def current_account
current_user&.account
end
def set_noindex_headers
headers['X-Robots-Tag'] = 'noindex'
end
def set_cors_headers
headers['Access-Control-Allow-Origin'] = '*'
headers['Access-Control-Allow-Methods'] = 'POST, GET, PUT, PATCH, DELETE, OPTIONS'
+25 -4
View File
@@ -1,16 +1,37 @@
# frozen_string_literal: true
module Api
class AttachmentsController < ApiBaseController
skip_before_action :authenticate_user!
skip_authorization_check
class AttachmentsController < ActionController::API
include ActionController::Cookies
include ActiveStorage::SetCurrent
COOKIE_STORE_LIMIT = 10
def create
submitter = Submitter.find_by!(slug: params[:submitter_slug])
attachment = Submitters.create_attachment!(submitter, params)
render json: attachment.as_json(only: %i[uuid], methods: %i[url filename content_type])
if params[:remember_signature] == 'true' && submitter.email.present?
cookies.encrypted[:signature_uuids] = build_new_cookie_signatures_json(submitter, attachment)
end
render json: attachment.as_json(only: %i[uuid created_at], methods: %i[url filename content_type])
end
def build_new_cookie_signatures_json(submitter, attachment)
values =
begin
JSON.parse(cookies.encrypted[:signature_uuids].presence || '{}')
rescue JSON::ParserError
{}
end
values[submitter.email] = attachment.uuid
values = values.to_a.last(COOKIE_STORE_LIMIT).to_h if values.size > COOKIE_STORE_LIMIT
values.to_json
end
end
end
@@ -0,0 +1,36 @@
# frozen_string_literal: true
module Api
class FormEventsController < ApiBaseController
load_and_authorize_resource :submitter, parent: false
def index
submitters = @submitters.where.not(completed_at: nil)
params[:after] = Time.zone.at(params[:after].to_i) if params[:after].present?
params[:before] = Time.zone.at(params[:before].to_i) if params[:before].present?
submitters = paginate(
submitters.preload(template: :folder, submission: [:submitters, { audit_trail_attachment: :blob,
combined_document_attachment: :blob }],
documents_attachments: :blob, attachments_attachments: :blob),
field: :completed_at
)
render json: {
data: submitters.map do |s|
{
event_type: 'form.completed',
timestamp: s.completed_at,
data: Submitters::SerializeForWebhook.call(s)
}
end,
pagination: {
count: submitters.size,
next: submitters.last&.completed_at&.to_i,
prev: submitters.first&.completed_at&.to_i
}
}
end
end
end
@@ -0,0 +1,52 @@
# frozen_string_literal: true
module Api
class SubmissionEventsController < ApiBaseController
load_and_authorize_resource :submission, parent: false
def index
submissions = build_completed_query(@submissions)
params[:after] = Time.zone.at(params[:after].to_i) if params[:after].present?
params[:before] = Time.zone.at(params[:before].to_i) if params[:before].present?
submissions = paginate(submissions.preload(
:created_by_user, :submission_events,
template: :folder,
submitters: { documents_attachments: :blob, attachments_attachments: :blob },
audit_trail_attachment: :blob
),
field: :completed_at)
render json: {
data: submissions.map do |s|
{
event_type: 'submission.completed',
timestamp: s.completed_at,
data: Submissions::SerializeForApi.call(s, s.submitters)
}
end,
pagination: {
count: submissions.size,
next: submissions.last&.completed_at&.to_i,
prev: submissions.first&.completed_at&.to_i
}
}
end
private
def build_completed_query(submissions)
submissions = submissions.where(
Submitter.where(completed_at: nil).where(
Submitter.arel_table[:submission_id].eq(Submission.arel_table[:id])
).select(1).arel.exists.not
)
submissions.joins(:submitters)
.group(:id)
.select(Submission.arel_table[Arel.star],
Submitter.arel_table[:completed_at].maximum.as('completed_at'))
end
end
end
+42 -51
View File
@@ -17,11 +17,16 @@ module Api
submissions = submissions.joins(template: :folder).where(folder: { name: params[:template_folder] })
end
submissions = paginate(submissions.preload(:created_by_user, :template, :submitters,
submissions = paginate(submissions.preload(:created_by_user, :submitters,
template: :folder,
combined_document_attachment: :blob,
audit_trail_attachment: :blob))
render json: {
data: submissions.as_json(serialize_params),
data: submissions.map do |s|
Submissions::SerializeForApi.call(s, s.submitters, params,
with_events: false, with_documents: false, with_values: false)
end,
pagination: {
count: submissions.size,
next: submissions.last&.id,
@@ -43,33 +48,11 @@ module Api
@submission.audit_trail_attachment = Submissions::GenerateAuditTrail.call(@submission)
end
serialized_submitters = submitters.map { |submitter| Submitters::SerializeForApi.call(submitter) }
json = @submission.as_json(
serialize_params.deep_merge(
include: { submission_events: { only: %i[id submitter_id event_type event_timestamp] } }
)
)
if submitters.all?(&:completed_at?)
last_submitter = submitters.max_by(&:completed_at)
json[:documents] = serialized_submitters.find { |e| e['id'] == last_submitter.id }['documents']
json[:status] = 'completed'
json[:completed_at] = last_submitter.completed_at
else
json[:documents] = []
json[:status] = 'pending'
json[:completed_at] = nil
end
json[:submitters] = serialized_submitters
render json:
render json: Submissions::SerializeForApi.call(@submission, submitters, params)
end
def create
Params::SubmissionCreateValidator.call(params, dry_run: !Docuseal.multitenant?)
Params::SubmissionCreateValidator.call(params)
return render json: { error: 'Template not found' }, status: :unprocessable_entity if @template.nil?
@@ -84,29 +67,51 @@ module Api
submissions = create_submissions(@template, params)
submissions.each do |submission|
SendSubmissionCreatedWebhookRequestJob.perform_async({ 'submission_id' => submission.id })
end
Submissions.send_signature_requests(submissions)
submissions.each do |submission|
if submission.submitters.all?(&:completed_at?) && submission.submitters.last
ProcessSubmitterCompletionJob.perform_later(submission.submitters.last)
ProcessSubmitterCompletionJob.perform_async({ 'submitter_id' => submission.submitters.last.id })
end
end
render json: submissions.flat_map(&:submitters)
rescue Submitters::NormalizeValues::BaseError => e
render json: build_create_json(submissions)
rescue Submitters::NormalizeValues::BaseError, DownloadUtils::UnableToDownload => e
Rollbar.warning(e) if defined?(Rollbar)
render json: { error: e.message }, status: :unprocessable_entity
end
def destroy
@submission.update!(archived_at: Time.current)
if params[:permanently] == 'true'
@submission.destroy!
else
@submission.update!(archived_at: Time.current)
render json: @submission.as_json(only: %i[id], methods: %i[archived_at])
SendSubmissionArchivedWebhookRequestJob.perform_async('submission_id' => @submission.id)
end
render json: @submission.as_json(only: %i[id archived_at])
end
private
def build_create_json(submissions)
json = submissions.flat_map do |submission|
submission.submitters.map do |s|
Submitters::SerializeForApi.call(s, with_documents: false, with_urls: true, params:)
end
end
json = { submitters: json } if request.path.ends_with?('/init')
json
end
def create_submissions(template, params)
is_send_email = !params[:send_email].in?(['false', false])
@@ -126,7 +131,6 @@ module Api
template:,
user: current_user,
source: :api,
mark_as_sent: is_send_email,
submitters_order: params[:submitters_order] || params[:order] || 'preserved',
submissions_attrs:,
params:
@@ -144,31 +148,18 @@ module Api
end
end
def serialize_params
{
only: %i[id source submitters_order created_at updated_at archived_at],
methods: %i[audit_log_url],
include: {
submitters: { only: %i[id slug uuid name email phone
completed_at opened_at sent_at
created_at updated_at external_id metadata],
methods: %i[status application_key] },
template: { only: %i[id name created_at updated_at] },
created_by_user: { only: %i[id email first_name last_name] }
}
}
end
def submissions_params
permitted_attrs = [
:send_email, :send_sms, :bcc_completed, :completed_redirect_url,
:send_email, :send_sms, :bcc_completed, :completed_redirect_url, :reply_to, :go_to_last,
:expire_at,
{
message: %i[subject body],
submitters: [[:send_email, :send_sms, :completed_redirect_url, :uuid, :name, :email, :role,
:completed, :phone, :application_key, :external_id,
:completed, :phone, :application_key, :external_id, :reply_to, :go_to_last,
{ metadata: {}, values: {}, readonly_fields: [], message: %i[subject body],
fields: [%i[name default_value title description
readonly validation_pattern invalid_message]] }]]
fields: [:name, :uuid, :default_value, :value, :title, :description,
:readonly, :validation_pattern, :invalid_message,
{ default_value: [], value: [], preferences: {} }] }]]
}
]
@@ -13,7 +13,7 @@ module Api
SubmissionEvents.create_with_tracking_data(submitter, 'view_form', request)
SendFormViewedWebhookRequestJob.perform_later(submitter)
SendFormViewedWebhookRequestJob.perform_async({ 'submitter_id' => submitter.id })
render json: {}
end
@@ -1,35 +0,0 @@
# frozen_string_literal: true
module Api
class SubmittersAutocompleteController < ApiBaseController
load_and_authorize_resource :submitter, parent: false
SELECT_COLUMNS = %w[email phone name].freeze
LIMIT = 100
def index
submitters = search_submitters(@submitters)
values = submitters.limit(LIMIT).group(SELECT_COLUMNS.join(', ')).pluck(SELECT_COLUMNS.join(', '))
attrs = values.map { |row| SELECT_COLUMNS.zip(row).to_h }
attrs = attrs.uniq { |e| e[params[:field]] } if params[:field].present?
render json: attrs
end
private
def search_submitters(submitters)
if SELECT_COLUMNS.include?(params[:field])
column = Submitter.arel_table[params[:field].to_sym]
term = "%#{params[:q].downcase}%"
submitters.where(column.lower.matches(term))
else
Submitters.search(submitters, params[:q])
end
end
end
end
+89 -14
View File
@@ -11,13 +11,21 @@ module Api
submitters = submitters.where(external_id: params[:external_id]) if params[:external_id].present?
submitters = submitters.where(submission_id: params[:submission_id]) if params[:submission_id].present?
if params[:template_id].present?
submitters = submitters.joins(:submission).where(submission: { template_id: params[:template_id] })
end
submitters = maybe_filder_by_completed_at(submitters, params)
submitters = paginate(
submitters.preload(:template, :submission, :submission_events,
documents_attachments: :blob, attachments_attachments: :blob)
)
render json: {
data: submitters.map { |s| Submitters::SerializeForApi.call(s, with_template: true, with_events: true) },
data: submitters.map do |s|
Submitters::SerializeForApi.call(s, with_template: true, with_events: true, params:)
end,
pagination: {
count: submitters.size,
next: submitters.last&.id,
@@ -29,7 +37,7 @@ module Api
def show
Submissions::EnsureResultGenerated.call(@submitter) if @submitter.completed_at?
render json: Submitters::SerializeForApi.call(@submitter, with_template: true, with_events: true)
render json: Submitters::SerializeForApi.call(@submitter, with_template: true, with_events: true, params:)
end
def update
@@ -60,42 +68,101 @@ module Api
end
if @submitter.completed_at?
ProcessSubmitterCompletionJob.perform_later(@submitter)
ProcessSubmitterCompletionJob.perform_async({ 'submitter_id' => @submitter.id })
elsif normalized_params[:send_email] || normalized_params[:send_sms]
Submitters.send_signature_requests([@submitter])
end
render json: Submitters::SerializeForApi.call(@submitter, with_template: false, with_events: false)
render json: Submitters::SerializeForApi.call(@submitter, with_template: false,
with_urls: true,
with_events: false,
params:)
rescue Submitters::NormalizeValues::BaseError, DownloadUtils::UnableToDownload => e
Rollbar.warning(e) if defined?(Rollbar)
render json: { error: e.message }, status: :unprocessable_entity
end
def submitter_params
submitter_params = params.key?(:submitter) ? params.require(:submitter) : params
submitter_params.permit(
:send_email, :send_sms, :uuid, :name, :email, :role,
:completed, :phone, :application_key, :external_id,
:send_email, :send_sms, :reply_to, :completed_redirect_url, :uuid, :name, :email, :role,
:completed, :phone, :application_key, :external_id, :go_to_last,
{ metadata: {}, values: {}, readonly_fields: [], message: %i[subject body],
fields: [%i[name default_value readonly validation_pattern invalid_message]] }
fields: [[:name, :uuid, :default_value, :value,
:readonly, :validation_pattern, :invalid_message,
{ default_value: [], value: [], preferences: {} }]] }
)
end
private
def maybe_filder_by_completed_at(submitters, params)
if params[:completed_after].present?
submitters = submitters.where(completed_at: Time.zone.parse(params[:completed_after])..)
end
if params[:completed_before].present?
submitters = submitters.where(completed_at: ..Time.zone.parse(params[:completed_before]))
end
submitters
end
def assign_submitter_attrs(submitter, attrs)
submitter.email = Submissions.normalize_email(attrs[:email]) if attrs.key?(:email)
submitter.phone = attrs[:phone].to_s.gsub(/[^0-9+]/, '') if attrs.key?(:phone)
submitter.values = submitter.values.merge(attrs[:values].to_unsafe_h) if attrs[:values].present?
submitter.completed_at = attrs[:completed] ? Time.current : submitter.completed_at
submitter.external_id = attrs[:application_key] if attrs.key?(:application_key)
submitter.external_id = attrs[:external_id] if attrs.key?(:external_id)
submitter.metadata = attrs[:metadata] if attrs.key?(:metadata)
values = attrs[:values]&.to_unsafe_h || {}
assign_submission_fields(submitter.submission)
phone_field_uuid = submitter.submission.template_fields.find do |f|
values[f['uuid']].present? && f['type'] == 'phone'
end&.dig('uuid')
submitter.email = Submissions.normalize_email(attrs[:email]) if attrs.key?(:email)
if attrs.key?(:phone)
submitter.phone = attrs[:phone].to_s.gsub(/[^0-9+]/, '')
elsif values[phone_field_uuid].present?
submitter.phone = values[phone_field_uuid].to_s.gsub(/[^0-9+]/, '')
end
values = values.except(phone_field_uuid)
submitter.values = submitter.values.merge(values) if values.present?
submitter.metadata = attrs[:metadata] if attrs.key?(:metadata)
maybe_assign_completed_attributes(submitter, attrs)
assign_external_id(submitter, attrs)
assign_preferences(submitter, attrs)
submitter
end
def maybe_assign_completed_attributes(submitter, attrs)
submitter.completed_at = attrs[:completed] ? Time.current : submitter.completed_at
if attrs[:completed]
submitter.values = Submitters::SubmitValues.merge_default_values(submitter)
submitter.values = Submitters::SubmitValues.merge_formula_values(submitter)
submitter.values = Submitters::SubmitValues.maybe_remove_condition_values(submitter)
submitter.values = submitter.values.transform_values do |v|
v == '{{date}}' ? Time.current.in_time_zone(submitter.account.timezone).to_date.to_s : v
end
end
submitter
end
def assign_external_id(submitter, attrs)
submitter.external_id = attrs[:application_key] if attrs.key?(:application_key)
submitter.external_id = attrs[:external_id] if attrs.key?(:external_id)
submitter
end
def assign_submission_fields(submission)
submission.template_fields ||= submission.template.fields
submission.template_schema ||= submission.template.schema
@@ -111,6 +178,14 @@ module Api
end
submitter.preferences['send_sms'] = submitter_preferences['send_sms'] if submitter_preferences.key?('send_sms')
submitter.preferences['reply_to'] = submitter_preferences['reply_to'] if submitter_preferences.key?('reply_to')
if submitter_preferences.key?('go_to_last')
submitter.preferences['go_to_last'] = submitter_preferences['go_to_last']
end
if submitter_preferences.key?('completed_redirect_url')
submitter.preferences['completed_redirect_url'] = submitter_preferences['completed_redirect_url']
end
return unless submitter_preferences.key?('email_message_uuid')
@@ -1,16 +0,0 @@
# frozen_string_literal: true
module Api
class TemplateFoldersAutocompleteController < ApiBaseController
load_and_authorize_resource :template_folder, parent: false
LIMIT = 100
def index
template_folders = @template_folders.joins(:templates).where(templates: { archived_at: nil }).distinct
template_folders = TemplateFolders.search(template_folders, params[:q]).limit(LIMIT)
render json: template_folders.as_json(only: %i[name archived_at])
end
end
end
@@ -7,6 +7,11 @@ module Api
def create
authorize!(:manage, @template)
ActiveRecord::Associations::Preloader.new(
records: [@template],
associations: [schema_documents: :preview_images_attachments]
).call
cloned_template = Templates::Clone.call(
@template,
author: current_user,
@@ -18,18 +23,11 @@ module Api
cloned_template.source = :api
cloned_template.save!
Templates::CloneAttachments.call(template: cloned_template, original_template: @template)
schema_documents = Templates::CloneAttachments.call(template: cloned_template, original_template: @template)
render json: cloned_template.as_json(serialize_params)
end
SendTemplateCreatedWebhookRequestJob.perform_async('template_id' => cloned_template.id)
private
def serialize_params
{
include: { author: { only: %i[id email first_name last_name] },
documents: { only: %i[id uuid], methods: %i[url filename] } }
}
render json: Templates::SerializeForApi.call(cloned_template, schema_documents)
end
end
end
+48 -19
View File
@@ -7,10 +7,31 @@ module Api
def index
templates = filter_templates(@templates, params)
templates = paginate(templates.preload(:author, documents_attachments: :blob))
templates = paginate(templates.preload(:author, :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)
render json: {
data: templates.as_json(serialize_params),
data: templates.map do |t|
Templates::SerializeForApi.call(
t,
schema_documents.select { |e| e.record_id == t.id },
preview_image_attachments
)
end,
pagination: {
count: templates.size,
next: templates.last&.id,
@@ -20,7 +41,7 @@ module Api
end
def show
render json: @template.as_json(serialize_params)
render json: Templates::SerializeForApi.call(@template)
end
def update
@@ -28,6 +49,14 @@ module Api
@template.folder = TemplateFolders.find_or_create_by_name(current_user, folder_name)
end
Array.wrap(params[:roles].presence || params.dig(:template, :roles).presence).each_with_index do |role, index|
if (item = @template.submitters[index])
item['name'] = role
else
@template.submitters << { 'name' => role, 'uuid' => SecureRandom.uuid }
end
end
archived = params.key?(:archived) ? params[:archived] : params.dig(:template, :archived)
if archived.in?([true, false])
@@ -36,11 +65,13 @@ module Api
@template.update!(template_params)
SendTemplateUpdatedWebhookRequestJob.perform_async('template_id' => @template.id)
render json: @template.as_json(only: %i[id updated_at])
end
def destroy
if params[:permanently] == 'true' && !Docuseal.multitenant?
if params[:permanently] == 'true'
@template.destroy!
else
@template.update!(archived_at: Time.current)
@@ -61,29 +92,27 @@ module Api
templates
end
def serialize_params
{
methods: %i[application_key],
include: { author: { only: %i[id email first_name last_name] },
documents: { only: %i[id uuid], methods: %i[url preview_image_url filename] } }
}
end
def template_params
permit_params = [
permitted_params = [
:name,
{ schema: [%i[attachment_uuid name]],
submitters: [%i[name uuid]],
fields: [[:uuid, :submitter_uuid, :name, :type, :required, :readonly, :default_value,
:external_id,
{
submitters: [%i[name uuid is_requester invite_by_uuid linked_to_uuid email]],
fields: [[:uuid, :submitter_uuid, :name, :type,
:required, :readonly, :default_value,
:title, :description,
{ preferences: {},
conditions: [%i[field_uuid value action]],
options: [%i[value uuid]],
areas: [%i[x y w h cell_w attachment_uuid option_uuid page]] }]] }
validation: %i[message pattern],
areas: [%i[x y w h cell_w attachment_uuid option_uuid page]] }]]
}
]
if params.key?(:template)
params.require(:template).permit(*permit_params)
params.require(:template).permit(permitted_params)
else
params.permit(*permit_params)
params.permit(permitted_params)
end
end
end
@@ -1,29 +0,0 @@
# frozen_string_literal: true
module Api
class TemplatesDocumentsController < ApiBaseController
load_and_authorize_resource :template
def create
return head :unprocessable_entity if params[:blobs].blank? && params[:files].blank?
documents = Templates::CreateAttachments.call(@template, params)
schema = documents.map do |doc|
{ attachment_uuid: doc.uuid, name: doc.filename.base }
end
render json: {
schema:,
documents: documents.as_json(
methods: %i[metadata signed_uuid],
include: {
preview_images: { methods: %i[url metadata filename] }
}
)
}
rescue Templates::CreateAttachments::PdfEncrypted
render json: { error: 'PDF encrypted' }, status: :unprocessable_entity
end
end
end
+42
View File
@@ -0,0 +1,42 @@
# frozen_string_literal: true
module Api
class ToolsController < ApiBaseController
skip_authorization_check
def merge
files = params[:files] || []
return render json: { error: 'Files are required' }, status: :unprocessable_entity if files.blank?
return render json: { error: 'At least 2 files are required' }, status: :unprocessable_entity if files.size < 2
render json: {
data: Base64.encode64(PdfUtils.merge(files.map { |base64| StringIO.new(Base64.decode64(base64)) }).string)
}
end
def verify
file = Base64.decode64(params[:file])
pdf = HexaPDF::Document.new(io: StringIO.new(file))
trusted_certs = Accounts.load_trusted_certs(current_account)
is_checksum_found = ActiveStorage::Attachment.joins(:blob)
.where(name: 'documents', record_type: 'Submitter')
.exists?(blob: { checksum: Digest::MD5.base64digest(file) })
render json: {
checksum_status: is_checksum_found ? 'verified' : 'not_found',
signatures: pdf.signatures.map do |sig|
{
verification_result: sig.verify(trusted_certs:).messages,
signer_name: sig.signer_name,
signing_reason: sig.signing_reason,
signing_time: sig.signing_time,
signature_type: sig.signature_type
}
end
}
end
end
end
+11
View File
@@ -0,0 +1,11 @@
# frozen_string_literal: true
module Api
class UsersController < ApiBaseController
authorize_resource :current_user
def show
render json: current_user.as_json(only: %i[id first_name last_name email])
end
end
end
+1 -1
View File
@@ -12,6 +12,6 @@ class ApiSettingsController < ApplicationController
current_user.access_token.save!
redirect_back(fallback_location: settings_api_index_path, notice: 'API token as been updated.')
redirect_back(fallback_location: settings_api_index_path, notice: I18n.t('api_token_has_been_updated'))
end
end
+10 -2
View File
@@ -22,6 +22,12 @@ class ApplicationController < ActionController::Base
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
end
if Rails.env.production?
rescue_from CanCan::AccessDenied do |e|
Rollbar.warning(e) if defined?(Rollbar)
@@ -46,7 +52,8 @@ class ApplicationController < ActionController::Base
private
def with_browser_locale(&)
locale = request.env['HTTP_ACCEPT_LANGUAGE'].to_s[BROWSER_LOCALE_REGEXP].to_s
locale = params[:lang].presence
locale ||= request.env['HTTP_ACCEPT_LANGUAGE'].to_s[BROWSER_LOCALE_REGEXP].to_s
locale =
if locale.starts_with?('en-') && locale != 'en-US'
@@ -72,7 +79,8 @@ class ApplicationController < ActionController::Base
redirect_to setup_index_path unless User.exists?
end
def button_title(title: 'Submit', disabled_with: 'Submitting', title_class: '', icon: nil, icon_disabled: nil)
def button_title(title: I18n.t('submit'), disabled_with: I18n.t('submitting'), title_class: '', icon: nil,
icon_disabled: nil)
render_to_string(partial: 'shared/button_title',
locals: { title:, disabled_with:, title_class:, icon:, icon_disabled: })
end
@@ -5,6 +5,12 @@ class ConsoleRedirectController < ApplicationController
skip_authorization_check
def index
if request.path == '/upgrade'
params[:redir] = Docuseal.multitenant? ? "#{Docuseal::CONSOLE_URL}/plans" : "#{Docuseal::CONSOLE_URL}/on_premise"
end
params[:redir] = "#{Docuseal::CONSOLE_URL}/manage" if request.path == '/manage'
return redirect_to(new_user_session_path({ redir: params[:redir] }.compact)) if true_user.blank?
auth = JsonWebToken.encode(uuid: true_user.uuid,
+5 -49
View File
@@ -7,62 +7,18 @@ class DashboardController < ApplicationController
before_action :maybe_render_landing
before_action :maybe_redirect_mfa_setup
load_and_authorize_resource :template_folder, parent: false
load_and_authorize_resource :template, parent: false
SHOW_TEMPLATES_FOLDERS_THRESHOLD = 9
TEMPLATES_PER_PAGE = 12
FOLDERS_PER_PAGE = 18
skip_authorization_check
def index
@template_folders = filter_template_folders(@template_folders)
@pagy, @template_folders = pagy(
@template_folders,
items: FOLDERS_PER_PAGE,
page: @template_folders.count > SHOW_TEMPLATES_FOLDERS_THRESHOLD ? params[:page] : 1
)
if @pagy.count > SHOW_TEMPLATES_FOLDERS_THRESHOLD
@templates = @templates.none
if cookies.permanent[:dashboard_view] == 'submissions'
SubmissionsDashboardController.dispatch(:index, request, response)
else
@template_folders = @template_folders.reject { |e| e.name == TemplateFolder::DEFAULT_NAME }
@templates = filter_templates(@templates)
items =
if @template_folders.size < 4
TEMPLATES_PER_PAGE
else
(@template_folders.size < 7 ? 9 : 6)
end
@pagy, @templates = pagy(@templates, items:)
TemplatesDashboardController.dispatch(:index, request, response)
end
end
private
def filter_template_folders(template_folders)
rel = template_folders.joins(:active_templates)
.order(id: :desc)
.distinct
TemplateFolders.search(rel, params[:q])
end
def filter_templates(templates)
rel = templates.active.preload(:author).order(id: :desc)
if params[:q].blank?
shared_template_ids =
TemplateSharing.where(account_id: [current_account.id, TemplateSharing::ALL_ID]).select(:template_id)
rel = rel.where(folder_id: current_account.default_template_folder.id).or(rel.where(id: shared_template_ids))
end
Templates.search(rel, params[:q])
end
def maybe_redirect_product_url
return if !Docuseal.multitenant? || signed_in?
@@ -77,7 +33,7 @@ class DashboardController < ApplicationController
account_id: current_user.account_id,
key: AccountConfig::FORCE_MFA)
redirect_to mfa_setup_path, notice: 'Setup 2FA to continue'
redirect_to mfa_setup_path, notice: I18n.t('setup_2fa_to_continue')
end
def maybe_render_landing
@@ -11,7 +11,7 @@ class EmailSmtpSettingsController < ApplicationController
if @encrypted_config.update(email_configs)
SettingsMailer.smtp_successful_setup(@encrypted_config.value['from_email']).deliver_now!
redirect_to settings_email_index_path, notice: 'Changes have been saved'
redirect_to settings_email_index_path, notice: I18n.t('changes_have_been_saved')
else
render :index, status: :unprocessable_entity
end
@@ -0,0 +1,39 @@
# frozen_string_literal: true
class EmbedScriptsController < ActionController::Metal
DUMMY_SCRIPT = <<~JAVASCRIPT.freeze
const DummyBuilder = class extends HTMLElement {
connectedCallback() {
this.innerHTML = `
<div style="text-align: center; padding: 20px; font-family: Arial, sans-serif;">
<h2>Upgrade to Pro</h2>
<p>Unlock embedded components by upgrading to Pro</p>
<div style="margin-top: 40px;">
<a href="#{Docuseal::CONSOLE_URL}/on_premise" target="_blank" style="padding: 15px 25px; background-color: #222; color: white; text-decoration: none; border-radius: 5px; font-size: 16px; cursor: pointer;">
Learn More
</a>
</div>
</div>
`;
}
};
const DummyForm = class extends DummyBuilder {};
if (!window.customElements.get('docuseal-builder')) {
window.customElements.define('docuseal-builder', DummyBuilder);
}
if (!window.customElements.get('docuseal-form')) {
window.customElements.define('docuseal-form', DummyForm);
}
JAVASCRIPT
def show
headers['Content-Type'] = 'application/javascript'
self.response_body = DUMMY_SCRIPT
self.status = 200
end
end
+20 -1
View File
@@ -15,6 +15,11 @@ class ErrorsController < ActionController::Base
'/api/templates/docx'
].freeze
SAFE_ERROR_MESSAGE_CLASSES = [
ActionDispatch::Http::Parameters::ParseError,
JSON::ParserError
].freeze
def show
if request.original_fullpath.in?(ENTERPRISE_PATHS) && error_status_code == 404
return render json: { status: 404, message: ENTERPRISE_FEATURE_MESSAGE }, status: :not_found
@@ -22,7 +27,13 @@ class ErrorsController < ActionController::Base
respond_to do |f|
f.json do
render json: { status: error_status_code }, status: error_status_code
set_cors_headers
exception = request.env['action_dispatch.exception']
error = exception.message if exception.class.in?(SAFE_ERROR_MESSAGE_CLASSES)
render json: { status: error_status_code, error: }.compact, status: error_status_code
end
f.html { render error_status_code.to_s, status: error_status_code }
@@ -31,6 +42,14 @@ class ErrorsController < ActionController::Base
private
def set_cors_headers
headers['Access-Control-Allow-Origin'] = '*'
headers['Access-Control-Allow-Methods'] = 'POST, GET, PUT, PATCH, DELETE, OPTIONS'
headers['Access-Control-Allow-Headers'] = '*'
headers['Access-Control-Max-Age'] = '1728000'
headers['Access-Control-Allow-Credentials'] = true
end
def error_status_code
@error_status_code ||=
ActionDispatch::ExceptionWrapper.new(request.env,
+8 -6
View File
@@ -48,7 +48,7 @@ class EsignSettingsController < ApplicationController
if (@encrypted_config.value && @encrypted_config.value['custom']&.any? { |e| e['name'] == @cert_record.name }) ||
@cert_record.name == DEFAULT_CERT_NAME
@cert_record.errors.add(:name, 'already exists')
@cert_record.errors.add(:name, I18n.t('already_exists'))
return render turbo_stream: turbo_stream.replace(:modal, template: 'esign_settings/new'),
status: :unprocessable_entity
@@ -56,9 +56,11 @@ class EsignSettingsController < ApplicationController
save_new_cert!(@encrypted_config, @cert_record)
redirect_to settings_esign_path, notice: 'Certificate has been successfully added!'
rescue OpenSSL::PKCS12::PKCS12Error
@cert_record.errors.add(:password, "is invalid. Make sure you're uploading a valid .p12 file")
redirect_to settings_esign_path, notice: I18n.t('certificate_has_been_successfully_added_')
rescue OpenSSL::PKCS12::PKCS12Error => e
Rollbar.error(e) if defined?(Rollbar)
@cert_record.errors.add(:password, e.message)
render turbo_stream: turbo_stream.replace(:modal, template: 'esign_settings/new'), status: :unprocessable_entity
end
@@ -71,7 +73,7 @@ class EsignSettingsController < ApplicationController
@encrypted_config.save!
redirect_to settings_esign_path, notice: 'Default certificate has been selected'
redirect_to settings_esign_path, notice: I18n.t('default_certificate_has_been_selected')
end
def destroy
@@ -79,7 +81,7 @@ class EsignSettingsController < ApplicationController
@encrypted_config.save!
redirect_to settings_esign_path, notice: 'Certificate has been removed'
redirect_to settings_esign_path, notice: I18m.t('certificate_has_been_removed')
end
private
+5 -5
View File
@@ -18,11 +18,11 @@ class MfaSetupController < ApplicationController
current_user.otp_required_for_login = true
current_user.save!
redirect_to settings_profile_index_path, notice: '2FA has been configured'
redirect_to settings_profile_index_path, notice: I18n.t('2fa_has_been_configured')
else
@provision_url = current_user.otp_provisioning_uri(current_user.email, issuer: Docuseal.product_name)
@error_message = 'Code is invalid'
@error_message = I18n.t('code_is_invalid')
render turbo_stream: turbo_stream.replace(:mfa_form, partial: 'mfa_setup/form'), status: :unprocessable_entity
end
@@ -32,9 +32,9 @@ class MfaSetupController < ApplicationController
if current_user.validate_and_consume_otp!(params[:otp_attempt])
current_user.update!(otp_required_for_login: false, otp_secret: nil)
redirect_to settings_profile_index_path, notice: '2FA has been removed'
redirect_to settings_profile_index_path, notice: I18n.t('2fa_has_been_removed')
else
@error_message = 'Code is invalid'
@error_message = I18n.t('code_is_invalid')
render turbo_stream: turbo_stream.replace(:modal, template: 'mfa_setup/edit'), status: :unprocessable_entity
end
@@ -43,7 +43,7 @@ class MfaSetupController < ApplicationController
private
def set_provision_url
return redirect_to root_path, alert: '2FA has been set up already' if current_user.otp_required_for_login
return redirect_to root_path, alert: I18n.t('2fa_has_been_set_up_already') if current_user.otp_required_for_login
current_user.otp_secret ||= User.generate_otp_secret
@@ -13,9 +13,9 @@ class NotificationsSettingsController < ApplicationController
def create
if @account_config.value.present? ? @account_config.save : @account_config.delete
redirect_back fallback_location: settings_notifications_path, notice: 'Changes have been saved'
redirect_back fallback_location: settings_notifications_path, notice: I18n.t('changes_have_been_saved')
else
redirect_back fallback_location: settings_notifications_path, alert: 'Unable to save'
redirect_back fallback_location: settings_notifications_path, alert: I18n.t('unable_to_save')
end
end
@@ -35,12 +35,12 @@ class NotificationsSettingsController < ApplicationController
def load_reminder_config
@reminder_config =
AccountConfig.find_or_initialize_by(account: current_account, key: AccountConfig::SUBMITTER_REMAILERS)
AccountConfig.find_or_initialize_by(account: current_account, key: AccountConfig::SUBMITTER_REMINDERS)
end
def email_config_params
params.require(:account_config).permit!.tap do |attrs|
attrs[:key] = nil unless attrs[:key].in?([AccountConfig::BCC_EMAILS, AccountConfig::SUBMITTER_REMAILERS])
attrs[:key] = nil unless attrs[:key].in?([AccountConfig::BCC_EMAILS, AccountConfig::SUBMITTER_REMINDERS])
end
end
end
+6
View File
@@ -5,6 +5,12 @@ class PasswordsController < Devise::PasswordsController
attribute :user
end
def create
super do |resource|
resource.errors.clear unless Docuseal.multitenant?
end
end
def update
super do |resource|
Current.user = resource
@@ -1,26 +1,58 @@
# frozen_string_literal: true
class PersonalizationSettingsController < ApplicationController
ALLOWED_KEYS = [
AccountConfig::FORM_COMPLETED_BUTTON_KEY,
AccountConfig::SUBMITTER_INVITATION_EMAIL_KEY,
AccountConfig::SUBMITTER_DOCUMENTS_COPY_EMAIL_KEY,
AccountConfig::SUBMITTER_COMPLETED_EMAIL_KEY,
AccountConfig::FORM_COMPLETED_MESSAGE_KEY
].freeze
InvalidKey = Class.new(StandardError)
before_action :load_and_authorize_account_config, only: :create
def show
authorize!(:read, AccountConfig)
end
def create
account_config =
current_account.account_configs.find_or_initialize_by(key: account_config_params[:key])
if @account_config.value.is_a?(Hash)
@account_config.value = @account_config.value.reject do |_, v|
v.blank? && v != false
end
end
authorize!(:create, account_config)
if @account_config.value != false && @account_config.value.blank?
@account_config.destroy!
else
@account_config.save!
end
account_config.update!(account_config_params)
redirect_back(fallback_location: settings_personalization_path, notice: 'Settings have been saved.')
redirect_back(fallback_location: settings_personalization_path, notice: I18n.t('settings_have_been_saved'))
end
private
def load_and_authorize_account_config
@account_config =
current_account.account_configs.find_or_initialize_by(key: account_config_params[:key])
@account_config.assign_attributes(account_config_params)
authorize!(:create, @account_config)
raise InvalidKey unless ALLOWED_KEYS.include?(@account_config.key)
@account_config
end
def account_config_params
attrs = params.require(:account_config).permit!
return attrs if attrs[:value].is_a?(String)
attrs[:value]&.transform_values! do |value|
if value.in?(%w[true false])
value == 'true'
@@ -8,7 +8,7 @@ class PreviewDocumentPageController < ActionController::API
def show
attachment_uuid = ApplicationRecord.signed_id_verifier.verified(params[:signed_uuid], purpose: :attachment)
attachment = ActiveStorage::Attachment.find_by(uuid: attachment_uuid, name: :preview_images) if attachment_uuid
attachment = ActiveStorage::Attachment.find_by(uuid: attachment_uuid) if attachment_uuid
return head :not_found unless attachment
+2 -2
View File
@@ -9,7 +9,7 @@ class ProfileController < ApplicationController
def update_contact
if current_user.update(contact_params)
redirect_to settings_profile_index_path, notice: 'Contact information has been updated'
redirect_to settings_profile_index_path, notice: I18n.t('contact_information_has_been_update')
else
render :index, status: :unprocessable_entity
end
@@ -18,7 +18,7 @@ class ProfileController < ApplicationController
def update_password
if current_user.update(password_params)
bypass_sign_in(current_user)
redirect_to settings_profile_index_path, notice: 'Password has been changed'
redirect_to settings_profile_index_path, notice: I18n.t('password_has_been_changed')
else
render :index, status: :unprocessable_entity
end
@@ -1,62 +0,0 @@
# frozen_string_literal: true
class RegistrationsController < Devise::RegistrationsController
prepend_before_action :require_no_authentication, only: [:show]
prepend_before_action :maybe_redirect_if_signed_in, only: [:show]
def show; end
def create
super
Accounts.create_default_template(resource.account) if resource.account.persisted?
end
private
def after_sign_up_path_for(...)
if params[:redir].present?
return console_redirect_index_path(redir: params[:redir]) if params[:redir].starts_with?(Docuseal::CONSOLE_URL)
return params[:redir]
end
super
end
def maybe_redirect_if_signed_in
return unless signed_in?
return if params[:redir].blank?
redirect_to after_sign_up_path_for(current_user), allow_other_host: true
end
def set_flash_message(key, kind, options = {})
return if key == :alert && kind == 'already_authenticated'
super
end
def build_resource(_hash = {})
account = Account.new(account_params)
account.timezone = Accounts.normalize_timezone(account.timezone)
self.resource = account.users.new(user_params)
account.name ||= resource.full_name if params[:action] == 'create'
end
def user_params
return {} if params[:user].blank?
params.require(:user).permit(:first_name, :last_name, :email, :password).compact_blank.tap do |attrs|
attrs[:password] ||= SecureRandom.hex if params[:action] == 'create'
end
end
def account_params
return {} if params[:account].blank?
params.require(:account).permit(:name, :timezone).compact_blank
end
end
@@ -7,18 +7,28 @@ class SendSubmissionEmailController < ApplicationController
skip_before_action :verify_authenticity_token
skip_authorization_check
SEND_DURATION = 30.minutes
def success; end
def create
@submitter =
if params[:template_slug]
Submitter.joins(submission: :template).find_by!(email: params[:email],
Submitter.joins(submission: :template).find_by!(email: params[:email].to_s.downcase,
template: { slug: params[:template_slug] })
elsif params[:submission_slug]
Submitter.joins(:submission).find_by!(email: params[:email].to_s.downcase,
submission: { slug: params[:submission_slug] })
else
Submitter.find_by!(slug: params[:submitter_slug])
end
SubmitterMailer.documents_copy_email(@submitter).deliver_later!
RateLimit.call("send-email-#{@submitter.id}", limit: 2, ttl: 5.minutes)
unless EmailEvent.exists?(tag: :submitter_documents_copy, email: @submitter.email, emailable: @submitter,
event_type: :send, created_at: SEND_DURATION.ago..Time.current)
SubmitterMailer.documents_copy_email(@submitter, sig: true).deliver_later!
end
respond_to do |f|
f.html { redirect_to success_send_submission_email_index_path }
+5 -1
View File
@@ -3,12 +3,16 @@
class SessionsController < Devise::SessionsController
before_action :configure_permitted_parameters
around_action :with_browser_locale
def create
email = sign_in_params[:email].to_s.downcase
if Docuseal.multitenant? && !User.exists?(email:)
Rollbar.warning('Sign in new user') if defined?(Rollbar)
return redirect_to new_registration_path(sign_up: true, user: sign_in_params.slice(:email)),
notice: 'Create a new account'
notice: I18n.t('create_a_new_account')
end
if User.exists?(email:, otp_required_for_login: true) && sign_in_params[:otp_attempt].blank?
+3 -3
View File
@@ -21,7 +21,7 @@ class SetupController < ApplicationController
@encrypted_config = EncryptedConfig.new(encrypted_config_params)
unless URI.parse(encrypted_config_params[:value].to_s).class.in?([URI::HTTP, URI::HTTPS])
@encrypted_config.errors.add(:value, 'should be a valid URL')
@encrypted_config.errors.add(:value, I18n.t('should_be_a_valid_url'))
return render :index, status: :unprocessable_entity
end
@@ -66,10 +66,10 @@ class SetupController < ApplicationController
end
def redirect_to_root_if_signed
redirect_to root_path, notice: 'You are already signed in'
redirect_to root_path, notice: I18n.t('you_are_already_signed_in')
end
def ensure_first_user_not_created!
redirect_to new_user_session_path, notice: 'Please sign in.' if User.exists?
redirect_to new_user_session_path, notice: I18n.t('please_sign_in') if User.exists?
end
end
@@ -3,7 +3,6 @@
class SsoSettingsController < ApplicationController
before_action :load_encrypted_config
authorize_resource :encrypted_config, only: :index
authorize_resource :encrypted_config, parent: false, except: :index
def index; end
+32 -12
View File
@@ -11,29 +11,35 @@ class StartFormController < ApplicationController
def show
@submitter = @template.submissions.new(account_id: @template.account_id)
.submitters.new(uuid: @template.submitters.first['uuid'])
.submitters.new(uuid: (filter_undefined_submitters(@template).first ||
@template.submitters.first)['uuid'])
end
def update
return redirect_to start_form_path(@template.slug) if @template.archived_at?
@submitter = Submitter.where(submission: @template.submissions.where(archived_at: nil))
.order(id: :desc)
.then { |rel| params[:resubmit].present? ? rel.where(completed_at: nil) : rel }
.find_or_initialize_by(**submitter_params.compact_blank)
@submitter = find_or_initialize_submitter(@template, submitter_params)
if @submitter.completed_at?
redirect_to start_form_completed_path(@template.slug, email: submitter_params[:email])
else
if @template.submitters.to_a.size > 1 && @submitter.new_record?
@error_message = 'Not found'
if filter_undefined_submitters(@template).size > 1 && @submitter.new_record?
@error_message = I18n.t('not_found')
return render :show
end
assign_submission_attributes(@submitter, @template) if @submitter.new_record?
if (is_new_record = @submitter.new_record?)
assign_submission_attributes(@submitter, @template)
Submissions::AssignDefinedSubmitters.call(@submitter.submission)
end
if @submitter.save
if is_new_record
SendSubmissionCreatedWebhookRequestJob.perform_async({ 'submission_id' => @submitter.submission.id })
end
redirect_to submit_form_path(@submitter.slug)
else
render :show
@@ -49,14 +55,21 @@ class StartFormController < ApplicationController
private
def find_or_initialize_submitter(template, submitter_params)
Submitter.where(submission: template.submissions.where(expire_at: Time.current..)
.or(template.submissions.where(expire_at: nil)).where(archived_at: nil))
.order(id: :desc)
.where(declined_at: nil)
.then { |rel| params[:resubmit].present? ? rel.where(completed_at: nil) : rel }
.find_or_initialize_by(**submitter_params.compact_blank)
end
def assign_submission_attributes(submitter, template)
resubmit_submitter =
if params[:resubmit].present?
Submitter.where(submission: @template.submissions).find_by(slug: params[:resubmit])
end
(Submitter.where(submission: template.submissions).find_by(slug: params[:resubmit]) if params[:resubmit].present?)
submitter.assign_attributes(
uuid: template.submitters.first['uuid'],
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) || {},
@@ -73,11 +86,18 @@ class StartFormController < ApplicationController
submitter.submission ||= Submission.new(template:,
account_id: template.account_id,
template_submitters: template.submitters,
submitters: [submitter],
source: :link)
submitter.account_id = submitter.submission.account_id
submitter
end
def filter_undefined_submitters(template)
Templates.filter_undefined_submitters(template)
end
def submitter_params
params.require(:submitter).permit(:email, :phone, :name).tap do |attrs|
attrs[:email] = Submissions.normalize_email(attrs[:email])
@@ -11,7 +11,7 @@ class StorageSettingsController < ApplicationController
if @encrypted_config.update(storage_configs)
LoadActiveStorageConfigs.reload
redirect_to settings_storage_index_path, notice: 'Changes have been saved'
redirect_to settings_storage_index_path, notice: I18n.t('changes_have_been_saved')
else
render :index, status: :unprocessable_entity
end
@@ -0,0 +1,15 @@
# frozen_string_literal: true
class SubmissionsArchivedController < ApplicationController
load_and_authorize_resource :submission, parent: false
def index
@submissions = @submissions.joins(:template)
@submissions = @submissions.where.not(archived_at: nil)
.or(@submissions.where.not(templates: { archived_at: nil }))
.preload(:created_by_user, template: :author)
@submissions = Submissions.search(@submissions, params[:q], search_template: true)
@pagy, @submissions = pagy(@submissions.preload(:submitters).order(id: :desc))
end
end
+28 -20
View File
@@ -6,23 +6,8 @@ class SubmissionsController < ApplicationController
load_and_authorize_resource :submission, only: %i[show destroy]
PRELOAD_ALL_PAGES_AMOUNT = 200
def show
ActiveRecord::Associations::Preloader.new(
records: [@submission],
associations: [:template, { template_schema_documents: :blob }]
).call
total_pages =
@submission.template_schema_documents.sum { |e| e.metadata.dig('pdf', 'number_of_pages').to_i }
if total_pages < PRELOAD_ALL_PAGES_AMOUNT
ActiveRecord::Associations::Preloader.new(
records: @submission.template_schema_documents,
associations: [:blob, { preview_images_attachments: :blob }]
).call
end
@submission = Submissions.preload_with_pages(@submission)
render :show, layout: 'plain'
end
@@ -34,6 +19,8 @@ class SubmissionsController < ApplicationController
def create
authorize!(:create, Submission)
save_template_message(@template, params) if params[:save_message] == '1'
if params[:is_custom_message] != '1'
params.delete(:subject)
params.delete(:body)
@@ -52,24 +39,45 @@ class SubmissionsController < ApplicationController
user: current_user,
source: :invite,
submitters_order: params[:preserve_order] == '1' ? 'preserved' : 'random',
mark_as_sent: params[:send_email] == '1',
submissions_attrs: submissions_params[:submission].to_h.values,
params: params.merge('send_completed_email' => true))
end
submissions.each do |submission|
SendSubmissionCreatedWebhookRequestJob.perform_async({ 'submission_id' => submission.id })
end
Submissions.send_signature_requests(submissions)
redirect_to template_path(@template), notice: 'New recipients have been added'
redirect_to template_path(@template), notice: I18n.t('new_recipients_have_been_added')
end
def destroy
@submission.update!(archived_at: Time.current)
notice =
if params[:permanently].present?
@submission.destroy!
redirect_back(fallback_location: template_path(@submission.template), notice: 'Submission has been archived')
I18n.t('submission_has_been_removed')
else
@submission.update!(archived_at: Time.current)
SendSubmissionArchivedWebhookRequestJob.perform_async('submission_id' => @submission.id)
I18n.t('submission_has_been_archived')
end
redirect_back(fallback_location: template_path(@submission.template), notice:)
end
private
def save_template_message(template, params)
template.preferences['request_email_subject'] = params[:subject] if params[:subject].present?
template.preferences['request_email_body'] = params[:body] if params[:body].present?
template.save!
end
def submissions_params
params.permit(submission: { submitters: [%i[uuid email phone name]] })
end
@@ -0,0 +1,20 @@
# frozen_string_literal: true
class SubmissionsDashboardController < ApplicationController
load_and_authorize_resource :submission, parent: false
def index
@submissions = @submissions.joins(:template)
@submissions = @submissions.where(archived_at: nil)
.where(templates: { archived_at: nil })
.preload(:created_by_user, template: :author)
@submissions = Submissions.search(@submissions, params[:q], search_template: true)
@submissions = @submissions.pending if params[:status] == 'pending'
@submissions = @submissions.completed if params[:status] == 'completed'
@pagy, @submissions = pagy(@submissions.preload(:submitters).order(id: :desc))
end
end
@@ -16,13 +16,16 @@ class SubmissionsDebugController < ApplicationController
render 'submit_form/show'
end
f.pdf do
if params[:audit]
Submissions::GenerateAuditTrail.call(@submitter.submission)
else
Submissions::GenerateResultAttachments.call(@submitter)
end
result =
if params[:audit]
Submissions::GenerateAuditTrail.call(@submitter.submission)
elsif params[:combined]
Submissions::GenerateCombinedAttachment.call(@submitter)
else
Submissions::GenerateResultAttachments.call(@submitter)
end
send_data ActiveStorage::Attachment.where(name: params[:audit] ? :audit_trail : :documents).last.download,
send_data Array.wrap(result).first.download,
filename: 'debug.pdf',
disposition: 'inline',
type: 'application/pdf'
@@ -4,10 +4,20 @@ class SubmissionsDownloadController < ApplicationController
skip_before_action :authenticate_user!
skip_authorization_check
TTL = 20.minutes
TTL = 40.minutes
FILES_TTL = 5.minutes
def index
submitter = Submitter.find_by!(slug: params[:submitter_slug])
submitter = Submitter.find_signed(params[:sig], purpose: :download_completed) if params[:sig].present?
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)
@@ -17,18 +27,44 @@ class SubmissionsDownloadController < ApplicationController
return head :not_found unless last_submitter.completed_at?
if last_submitter.completed_at < TTL.ago &&
(current_user.nil? || !current_user.account.submitters.exists?(id: last_submitter.id))
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
urls =
Submitters.select_attachments_for_download(last_submitter).map do |attachment|
ActiveStorage::Blob.proxy_url(attachment.blob)
end
if params[:combined] == 'true'
url = build_combined_url(submitter)
render json: urls
if url
render json: [url]
else
head :not_found
end
else
render json: build_urls(last_submitter)
end
end
private
def current_user_submitter?(submitter)
current_user && current_user.account.submitters.exists?(id: submitter.id)
end
def build_urls(submitter)
Submitters.select_attachments_for_download(submitter).map do |attachment|
ActiveStorage::Blob.proxy_url(attachment.blob, expires_at: FILES_TTL.from_now.to_i)
end
end
def build_combined_url(submitter)
return if submitter.submission.submitters.exists?(completed_at: nil)
return if submitter.submission.submitters.order(:completed_at).last != submitter
attachment = submitter.submission.combined_document_attachment
attachment ||= Submissions::GenerateCombinedAttachment.call(submitter)
ActiveStorage::Blob.proxy_url(attachment.blob, expires_at: FILES_TTL.from_now.to_i)
end
end
@@ -4,37 +4,31 @@ class SubmissionsPreviewController < ApplicationController
skip_before_action :authenticate_user!
skip_authorization_check
PRELOAD_ALL_PAGES_AMOUNT = 200
TTL = 20.minutes
TTL = 40.minutes
def show
@submission = Submission.find_by!(slug: params[:slug])
submitter = Submitter.find_signed(params[:sig], purpose: :download_completed) if params[:sig].present?
signature_valid =
if submitter && submitter.submission.slug == params[:slug]
@submission = submitter.submission
true
end
@submission ||= Submission.find_by!(slug: params[:slug])
if !@submission.submitters.all?(&:completed_at?) && current_user.blank?
raise ActionController::RoutingError, 'Not Found'
raise ActionController::RoutingError, I18n.t('not_found')
end
unless submission_valid_ttl?(@submission)
if !submission_valid_ttl?(@submission) && !signature_valid
Rollbar.info("TTL: #{@submission.id}") if defined?(Rollbar)
return redirect_to submissions_preview_completed_path(@submission.slug)
end
ActiveRecord::Associations::Preloader.new(
records: [@submission],
associations: [:template, { template_schema_documents: :blob }]
).call
total_pages =
@submission.template_schema_documents.sum { |e| e.metadata.dig('pdf', 'number_of_pages').to_i }
if total_pages < PRELOAD_ALL_PAGES_AMOUNT
ActiveRecord::Associations::Preloader.new(
records: @submission.template_schema_documents,
associations: [:blob, { preview_images_attachments: :blob }]
).call
end
@submission = Submissions.preload_with_pages(@submission)
render 'submissions/show', layout: 'plain'
end
@@ -42,7 +36,7 @@ class SubmissionsPreviewController < ApplicationController
def completed
@submission = Submission.find_by!(slug: params[:submissions_preview_slug])
render :completed, layout: 'plain'
render :completed, layout: 'form'
end
private
+34 -21
View File
@@ -7,46 +7,59 @@ class SubmitFormController < ApplicationController
skip_before_action :authenticate_user!
skip_authorization_check
PRELOAD_ALL_PAGES_AMOUNT = 200
CONFIG_KEYS = [].freeze
def show
@submitter = Submitter.find_by!(slug: params[:slug])
return redirect_to submit_form_completed_path(@submitter.slug) if @submitter.completed_at?
return render :archived if @submitter.submission.template.archived_at? || @submitter.submission.archived_at?
return render :expired if @submitter.submission.expired?
return render :declined if @submitter.declined_at?
ActiveRecord::Associations::Preloader.new(
records: [@submitter],
associations: [submission: [:template, { template_schema_documents: :blob }]]
).call
total_pages =
@submitter.submission.template_schema_documents.sum { |e| e.metadata.dig('pdf', 'number_of_pages').to_i }
if total_pages < PRELOAD_ALL_PAGES_AMOUNT
ActiveRecord::Associations::Preloader.new(
records: @submitter.submission.template_schema_documents,
associations: [:blob, { preview_images_attachments: :blob }]
).call
end
Submitters.preload_with_pages(@submitter)
Submitters::MaybeUpdateDefaultValues.call(@submitter, current_user)
cookies[:submitter_sid] = @submitter.signed_id
@attachments_index = ActiveStorage::Attachment.where(record: @submitter.submission.submitters, name: :attachments)
.preload(:blob).index_by(&:uuid)
render(@submitter.submission.template.archived_at? || @submitter.submission.archived_at? ? :archived : :show)
@form_configs = Submitters::FormConfigs.call(@submitter, CONFIG_KEYS)
return unless @form_configs[:prefill_signature]
if (user_signature = UserConfigs.load_signature(current_user))
@signature_attachment = ActiveStorage::Attachment.find_or_create_by!(
blob_id: user_signature.blob_id,
name: 'attachments',
record: @submitter
)
end
@signature_attachment ||=
Submitters::MaybeAssignDefaultBrowserSignature.call(@submitter, params, cookies, @attachments_index.values)
@attachments_index[@signature_attachment.uuid] = @signature_attachment if @signature_attachment
end
def update
submitter = Submitter.find_by!(slug: params[:slug])
if submitter.completed_at?
return render json: { error: 'Form has been completed already.' }, status: :unprocessable_entity
return render json: { error: I18n.t('form_has_been_completed_already') }, status: :unprocessable_entity
end
if submitter.template.archived_at? || submitter.submission.archived_at?
Rollbar.info("Archived template: #{submitter.template.id}") if defined?(Rollbar)
return render json: { error: I18n.t('form_has_been_archived') }, status: :unprocessable_entity
end
return render json: { error: 'Form has been archived.' }, status: :unprocessable_entity
if submitter.submission.expired?
return render json: { error: I18n.t('form_has_been_expired') }, status: :unprocessable_entity
end
if submitter.declined_at?
return render json: { error: I18n.t('form_has_been_declined') },
status: :unprocessable_entity
end
Submitters::SubmitValues.call(submitter, params, request)
@@ -57,7 +70,7 @@ class SubmitFormController < ApplicationController
end
def completed
@submitter = Submitter.find_by!(slug: params[:submit_form_slug])
@submitter = Submitter.completed.find_by!(slug: params[:submit_form_slug])
end
def success; end
@@ -0,0 +1,29 @@
# frozen_string_literal: true
class SubmitFormDeclineController < ApplicationController
skip_before_action :authenticate_user!
skip_authorization_check
def create
submitter = Submitter.find_by!(slug: params[:submit_form_slug])
return redirect_to submit_form_path(submitter.slug) if submitter.declined_at? ||
submitter.completed_at? ||
submitter.submission.archived_at? ||
submitter.submission.expired? ||
submitter.submission.template.archived_at?
ApplicationRecord.transaction do
submitter.update!(declined_at: Time.current)
SubmissionEvents.create_with_tracking_data(submitter, 'decline_form', request, { reason: params[:reason] })
end
user = submitter.submission.created_by_user || submitter.template.author
SubmitterMailer.declined_email(submitter, user).deliver_later!
SendFormDeclinedWebhookRequestJob.perform_async('submitter_id' => submitter.id)
redirect_to submit_form_path(submitter.slug)
end
end
@@ -0,0 +1,37 @@
# frozen_string_literal: true
class SubmitFormDownloadController < ApplicationController
skip_before_action :authenticate_user!
skip_authorization_check
FILES_TTL = 5.minutes
def index
submitter = Submitter.find_by!(slug: params[:submit_form_slug])
return redirect_to submitter_download_index_path(submitter.slug) if submitter.completed_at?
return head :unprocessable_entity if submitter.declined_at? ||
submitter.submission.archived_at? ||
submitter.submission.expired? ||
submitter.submission.template.archived_at?
last_completed_submitter = submitter.submission.submitters
.where.not(id: submitter.id)
.where.not(completed_at: nil)
.max_by(&:completed_at)
attachments =
if last_completed_submitter
Submitters.select_attachments_for_download(last_completed_submitter)
else
submitter.submission.template.schema_documents.preload(:blob)
end
urls = attachments.map do |attachment|
ActiveStorage::Blob.proxy_url(attachment.blob, expires_at: FILES_TTL.from_now.to_i)
end
render json: urls
end
end
@@ -0,0 +1,21 @@
# frozen_string_literal: true
class SubmitFormDrawSignatureController < ApplicationController
layout false
around_action :with_browser_locale, only: %i[show]
skip_before_action :authenticate_user!
skip_authorization_check
def show
@submitter = Submitter.find_by!(slug: params[:slug])
return redirect_to submit_form_completed_path(@submitter.slug) if @submitter.completed_at?
if @submitter.submission.template.archived_at? || @submitter.submission.archived_at?
return redirect_to submit_form_path(@submitter.slug)
end
render :show
end
end
@@ -0,0 +1,58 @@
# frozen_string_literal: true
class SubmitFormInviteController < ApplicationController
skip_before_action :authenticate_user!
skip_authorization_check
def create
submitter = Submitter.find_by!(slug: params[:submit_form_slug])
return head :unprocessable_entity unless can_invite?(submitter)
invite_submitters = filter_invite_submitters(submitter)
ApplicationRecord.transaction do
invite_submitters.each do |item|
attrs = submitters_attributes.find { |e| e[:uuid] == item['uuid'] }
next unless attrs
submitter.submission.submitters.create!(**attrs, account_id: submitter.account_id)
SubmissionEvents.create_with_tracking_data(submitter, 'invite_party', request, { uuid: submitter.uuid })
end
submitter.submission.update!(submitters_order: :preserved)
end
submitter.submission.submitters.reload
if invite_submitters.all? { |s| submitter.submission.submitters.any? { |e| e.uuid == s['uuid'] } }
Submitters::SubmitValues.call(submitter, ActionController::Parameters.new(completed: 'true'), request)
head :ok
else
head :unprocessable_entity
end
end
private
def can_invite?(submitter)
!submitter.declined_at? &&
!submitter.completed_at? &&
!submitter.submission.archived_at? &&
!submitter.submission.expired? &&
!submitter.submission.template.archived_at?
end
def filter_invite_submitters(submitter)
(submitter.submission.template_submitters || submitter.submission.template.submitters).select do |s|
s['invite_by_uuid'] == submitter.uuid && submitter.submission.submitters.none? { |e| e.uuid == s['uuid'] }
end
end
def submitters_attributes
params.require(:submission).permit(submitters: [%i[uuid email]]).fetch(:submitters, [])
end
end
@@ -0,0 +1,23 @@
# frozen_string_literal: true
class SubmitFormValuesController < ApplicationController
skip_before_action :authenticate_user!
skip_authorization_check
def index
submitter = Submitter.find_by!(slug: params[:submit_form_slug])
return render json: {} if submitter.completed_at? || submitter.declined_at?
return render json: {} if submitter.submission.template.archived_at? ||
submitter.submission.archived_at? ||
submitter.submission.expired?
value = submitter.values[params['field_uuid']]
attachment = submitter.attachments.where(created_at: params[:after]..).find_by(uuid: value) if value.present?
render json: {
value:,
attachment: attachment&.as_json(only: %i[uuid created_at], methods: %i[url filename content_type])
}, head: :ok
end
end
@@ -0,0 +1,33 @@
# frozen_string_literal: true
class SubmittersAutocompleteController < ApplicationController
load_and_authorize_resource :submitter, parent: false
SELECT_COLUMNS = %w[email phone name].freeze
LIMIT = 100
def index
submitters = search_submitters(@submitters)
values = submitters.limit(LIMIT).group(SELECT_COLUMNS.join(', ')).pluck(SELECT_COLUMNS.join(', '))
attrs = values.map { |row| SELECT_COLUMNS.zip(row).to_h }
attrs = attrs.uniq { |e| e[params[:field]] } if params[:field].present?
render json: attrs
end
private
def search_submitters(submitters)
if SELECT_COLUMNS.include?(params[:field])
column = Submitter.arel_table[params[:field].to_sym]
term = "#{params[:q].downcase}%"
submitters.where(column.matches(term))
else
Submitters.search(submitters, params[:q])
end
end
end
@@ -7,15 +7,17 @@ class SubmittersSendEmailController < ApplicationController
if Docuseal.multitenant? && SubmissionEvent.exists?(submitter: @submitter,
event_type: 'send_email',
created_at: 24.hours.ago..Time.current)
Rollbar.warning("Already sent: #{@submitter.id}") if defined?(Rollbar)
return redirect_back(fallback_location: submission_path(@submitter.submission),
alert: 'Email has been sent already.')
alert: I18n.t('email_has_been_sent_already'))
end
SendSubmitterInvitationEmailJob.perform_later('submitter_id' => @submitter.id)
SendSubmitterInvitationEmailJob.perform_async('submitter_id' => @submitter.id)
@submitter.sent_at ||= Time.current
@submitter.save!
redirect_back(fallback_location: submission_path(@submitter.submission), notice: 'Email has been sent')
redirect_back(fallback_location: submission_path(@submitter.submission), notice: I18n.t('email_has_been_sent'))
end
end
@@ -0,0 +1,33 @@
# frozen_string_literal: true
class TemplateDocumentsController < ApplicationController
load_and_authorize_resource :template
def create
if params[:blobs].blank? && params[:files].blank?
return render json: { error: I18n.t('file_is_missing') }, status: :unprocessable_entity
end
old_fields_hash = @template.fields.hash
documents = Templates::CreateAttachments.call(@template, params, extract_fields: true)
schema = documents.map do |doc|
{ attachment_uuid: doc.uuid, name: doc.filename.base }
end
render json: {
schema:,
fields: old_fields_hash == @template.fields.hash ? nil : @template.fields,
submitters: old_fields_hash == @template.fields.hash ? nil : @template.submitters,
documents: documents.as_json(
methods: %i[metadata signed_uuid],
include: {
preview_images: { methods: %i[url metadata filename] }
}
)
}
rescue Templates::CreateAttachments::PdfEncrypted
render json: { error: 'PDF encrypted', status: 'pdf_encrypted' }, status: :unprocessable_entity
end
end
@@ -0,0 +1,14 @@
# frozen_string_literal: true
class TemplateFoldersAutocompleteController < ApplicationController
load_and_authorize_resource :template_folder, parent: false
LIMIT = 100
def index
template_folders = @template_folders.joins(:templates).where(templates: { archived_at: nil }).distinct
template_folders = TemplateFolders.search(template_folders, params[:q]).limit(LIMIT)
render json: template_folders.as_json(only: %i[name archived_at])
end
end
@@ -15,9 +15,9 @@ class TemplateFoldersController < ApplicationController
def update
if @template_folder != current_account.default_template_folder &&
@template_folder.update(template_folder_params)
redirect_to folder_path(@template_folder), notice: 'Folder name has been updated'
redirect_to folder_path(@template_folder), notice: I18n.t('folder_name_has_been_updated')
else
redirect_to folder_path(@template_folder), alert: 'Unable to rename folder'
redirect_to folder_path(@template_folder), alert: I18n.t('unable_to_rename_folder')
end
end
@@ -6,7 +6,7 @@ class TemplatesArchivedSubmissionsController < ApplicationController
def index
@submissions = @submissions.where.not(archived_at: nil)
@submissions = Submissions.search(@submissions, params[:q])
@submissions = Submissions.search(@submissions, params[:q], search_values: true)
@pagy, @submissions = pagy(@submissions.preload(:submitters).order(id: :desc))
rescue ActiveRecord::RecordNotFound
+38 -13
View File
@@ -8,7 +8,7 @@ class TemplatesController < ApplicationController
def show
submissions = @template.submissions.accessible_by(current_ability)
submissions = submissions.active if @template.archived_at.blank?
submissions = Submissions.search(submissions, params[:q])
submissions = Submissions.search(submissions, params[:q], search_values: true)
@base_submissions = submissions
@@ -21,13 +21,13 @@ class TemplatesController < ApplicationController
end
def new
@template.name = "#{@base_template.name} (Clone)" if @base_template
@template.name = "#{@base_template.name} (#{I18n.t('clone')})" if @base_template
end
def edit
ActiveRecord::Associations::Preloader.new(
records: [@template],
associations: [schema_documents: { preview_images_attachments: :blob }]
associations: [schema_documents: [:blob, { preview_images_attachments: :blob }]]
).call
@template_data =
@@ -43,6 +43,11 @@ class TemplatesController < ApplicationController
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])
@@ -53,7 +58,7 @@ class TemplatesController < ApplicationController
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
@template.folder = @template.account.default_template_folder if @template.account_id != current_account.id
else
@template.account = current_account
end
@@ -61,24 +66,32 @@ class TemplatesController < ApplicationController
if @template.save
Templates::CloneAttachments.call(template: @template, original_template: @base_template) if @base_template
SendTemplateUpdatedWebhookRequestJob.perform_async('template_id' => @template.id)
maybe_redirect_to_template(@template)
else
render turbo_stream: turbo_stream.replace(:modal, template: 'templates/new'), status: :unprocessable_entity
end
end
def update
@template.update!(template_params)
SendTemplateUpdatedWebhookRequestJob.perform_async('template_id' => @template.id)
head :ok
end
def destroy
notice =
if params[:permanently].present?
@template.destroy!
Rollbar.info("Remove template: #{@template.id}") if defined?(Rollbar)
'Template has been removed.'
I18n.t('template_has_been_removed')
else
@template.update!(archived_at: Time.current)
'Template has been archived.'
I18n.t('template_has_been_archived')
end
redirect_back(fallback_location: root_path, notice:)
@@ -86,6 +99,22 @@ class TemplatesController < ApplicationController
private
def template_params
params.require(:template).permit(
:name,
{ schema: [%i[attachment_uuid name]],
submitters: [%i[name uuid is_requester linked_to_uuid invite_by_uuid email]],
fields: [[:uuid, :submitter_uuid, :name, :type,
:required, :readonly, :default_value,
:title, :description,
{ preferences: {},
conditions: [%i[field_uuid value action]],
options: [%i[value uuid]],
validation: %i[message pattern],
areas: [%i[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.exists?(id: account_id)
end
@@ -94,14 +123,10 @@ class TemplatesController < ApplicationController
if template.account == current_account
redirect_to(edit_template_path(@template))
else
redirect_back(fallback_location: root_path, notice: 'Template has been clonned')
redirect_back(fallback_location: root_path, notice: I18n.t('template_has_been_cloned'))
end
end
def template_params
params.require(:template).permit(:name)
end
def load_base_template
return if params[:base_template_id].blank?
@@ -0,0 +1,63 @@
# frozen_string_literal: true
class TemplatesDashboardController < ApplicationController
load_and_authorize_resource :template_folder, parent: false
load_and_authorize_resource :template, parent: false
SHOW_TEMPLATES_FOLDERS_THRESHOLD = 9
TEMPLATES_PER_PAGE = 12
FOLDERS_PER_PAGE = 18
def index
@template_folders = filter_template_folders(@template_folders)
@pagy, @template_folders = pagy(
@template_folders,
items: FOLDERS_PER_PAGE,
page: @template_folders.count > SHOW_TEMPLATES_FOLDERS_THRESHOLD ? params[:page] : 1
)
if @pagy.count > SHOW_TEMPLATES_FOLDERS_THRESHOLD
@templates = @templates.none
else
@template_folders = @template_folders.reject { |e| e.name == TemplateFolder::DEFAULT_NAME }
@templates = filter_templates(@templates)
items =
if @template_folders.size < 4
TEMPLATES_PER_PAGE
else
(@template_folders.size < 7 ? 9 : 6)
end
@pagy, @templates = pagy(@templates, items:)
end
end
private
def filter_template_folders(template_folders)
rel = template_folders.joins(:active_templates)
.order(id: :desc)
.distinct
TemplateFolders.search(rel, params[:q])
end
def filter_templates(templates)
rel = templates.active.preload(:author).order(id: :desc)
if params[:q].blank?
if Docuseal.multitenant? && !current_account.testing?
rel = rel.where(folder_id: current_account.default_template_folder.id)
else
shared_template_ids =
TemplateSharing.where(account_id: [current_account.id, TemplateSharing::ALL_ID]).select(:template_id)
rel = rel.where(folder_id: current_account.default_template_folder.id).or(rel.where(id: shared_template_ids))
end
end
Templates.search(rel, params[:q])
end
end
@@ -0,0 +1,33 @@
# frozen_string_literal: true
class TemplatesDebugController < ApplicationController
load_and_authorize_resource :template
def show
attachment = @template.documents.first
pdf = HexaPDF::Document.new(io: StringIO.new(attachment.download))
fields = Templates::FindAcroFields.call(pdf, attachment)
attachment.metadata['pdf'] ||= {}
attachment.metadata['pdf']['fields'] = fields
@template.update!(fields: Templates::ProcessDocument.normalize_attachment_fields(@template, [attachment]))
ActiveRecord::Associations::Preloader.new(
records: [@template],
associations: [schema_documents: { preview_images_attachments: :blob }]
).call
@template_data =
@template.as_json.merge(
documents: @template.schema_documents.as_json(
methods: %i[metadata signed_uuid],
include: { preview_images: { methods: %i[url metadata filename] } }
)
).to_json
render 'templates/edit', layout: 'plain'
end
end
@@ -9,9 +9,9 @@ class TemplatesFoldersController < ApplicationController
@template.folder = TemplateFolders.find_or_create_by_name(current_user, params[:name])
if @template.save
redirect_back(fallback_location: template_path(@template), notice: 'Document template has been moved')
redirect_back(fallback_location: template_path(@template), notice: I18n.t('document_template_has_been_moved'))
else
redirect_back(fallback_location: template_path(@template), notice: 'Unable to move template into folder')
redirect_back(fallback_location: template_path(@template), notice: I18n.t('unable_to_move_template_into_folder'))
end
end
@@ -0,0 +1,23 @@
# frozen_string_literal: true
class TemplatesFormPreviewController < ApplicationController
layout 'form'
load_and_authorize_resource :template
def show
@submitter = Submitter.new(uuid: params[:uuid] || @template.submitters.first['uuid'],
account: current_account,
submission: @template.submissions.new(template_submitters: @template.submitters,
account: current_account))
@submitter.submission.submitters = @template.submitters.map { |item| Submitter.new(uuid: item['uuid']) }
Submitters.preload_with_pages(@submitter)
@attachments_index = ActiveStorage::Attachment.where(record: @submitter.submission.submitters, name: :attachments)
.preload(:blob).index_by(&:uuid)
@form_configs = Submitters::FormConfigs.call(@submitter)
end
end
@@ -0,0 +1,42 @@
# frozen_string_literal: true
class TemplatesPreferencesController < ApplicationController
load_and_authorize_resource :template
def show; end
def create
authorize!(:update, @template)
@template.preferences = @template.preferences.merge(template_params[:preferences])
@template.preferences = @template.preferences.reject { |_, v| (v.is_a?(String) || v.is_a?(Hash)) && v.blank? }
@template.save!
head :ok
end
private
def template_params
params.require(:template).permit(
preferences: %i[bcc_completed request_email_subject request_email_body
documents_copy_email_subject documents_copy_email_body
documents_copy_email_enabled documents_copy_email_attach_audit
completed_notification_email_attach_documents
completed_redirect_url
completed_notification_email_subject completed_notification_email_body
completed_notification_email_enabled completed_notification_email_attach_audit] +
[completed_message: %i[title body]]
).tap do |attrs|
attrs[:preferences] = attrs[:preferences].transform_values do |value|
if %w[true false].include?(value)
value == 'true'
elsif value.respond_to?(:compact_blank)
value.compact_blank
else
value
end
end
end
end
end
@@ -0,0 +1,54 @@
# frozen_string_literal: true
class TemplatesRecipientsController < ApplicationController
load_and_authorize_resource :template
def create
authorize!(:update, @template)
@template.submitters =
submitters_params.map { |s| s.reject { |_, v| v.is_a?(String) && v.blank? } }
@template.save!
render json: { submitters: @template.submitters }
end
private
def submitters_params
params.require(:template).permit(
submitters: [%i[name uuid is_requester invite_by_uuid linked_to_uuid email option]]
).fetch(:submitters, {}).values.filter_map do |s|
next if s[:uuid].blank?
if s[:is_requester] == '1' && s[:invite_by_uuid].blank?
s[:is_requester] = true
else
s.delete(:is_requester)
end
s.delete(:invite_by_uuid) if s[:invite_by_uuid].blank?
option = s.delete(:option)
if option.present?
case option
when 'is_requester'
s[:is_requester] = true
when 'not_set'
s.delete(:is_requester)
s.delete(:email)
s.delete(:linked_to_uuid)
s.delete(:invite_by_uuid)
when /\Alinked_to_(.*)\z/
s[:linked_to_uuid] = ::Regexp.last_match(-1)
when /\Ainvite_by_(.*)\z/
s[:invite_by_uuid] = ::Regexp.last_match(-1)
end
end
s
end
end
end
@@ -6,6 +6,6 @@ class TemplatesRestoreController < ApplicationController
def create
@template.update!(archived_at: nil)
redirect_to template_path(@template), notice: 'Template has been unarchived'
redirect_to template_path(@template), notice: I18n.t('template_has_been_unarchived')
end
end
+14 -11
View File
@@ -12,19 +12,28 @@ class TemplatesUploadsController < ApplicationController
save_template!(@template, url_params)
documents = Templates::CreateAttachments.call(@template, url_params || params)
documents = Templates::CreateAttachments.call(@template, url_params || params, 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)
schema.each { |item| item['pending_fields'] = true } if @template.fields.present?
end
@template.update!(schema:)
SendTemplateCreatedWebhookRequestJob.perform_async('template_id' => @template.id)
redirect_to edit_template_path(@template)
rescue Templates::CreateAttachments::PdfEncrypted
render turbo_stream: turbo_stream.append(params[:form_id], html: helpers.tag.prompt_password)
rescue StandardError => e
Rollbar.error(e) if defined?(Rollbar)
redirect_to root_path, alert: 'Unable to upload file'
raise if Rails.env.local?
redirect_to root_path, alert: I18n.t('unable_to_update_file')
end
private
@@ -43,23 +52,17 @@ class TemplatesUploadsController < ApplicationController
def create_file_params_from_url
tempfile = Tempfile.new
tempfile.binmode
tempfile.write(conn.get(Addressable::URI.parse(params[:url]).display_uri.to_s).body)
tempfile.write(DownloadUtils.call(params[:url]).body)
tempfile.rewind
file = ActionDispatch::Http::UploadedFile.new(
tempfile:,
filename: File.basename(
URI.decode_www_form_component(params[:filename].presence || params[:url])
URI.decode_www_form_component(params[:filename].presence || params[:url]), '.*'
),
type: Marcel::MimeType.for(tempfile)
)
{ files: [file] }
end
def conn
Faraday.new do |faraday|
faraday.response :follow_redirects
end
end
end
+22 -14
View File
@@ -1,38 +1,46 @@
# frozen_string_literal: true
class TimestampServerController < ApplicationController
HASH_ALGORITHM = 'SHA256'
before_action :build_encrypted_config
authorize_resource :encrypted_config
TimestampError = Class.new(StandardError)
def create
return head :not_found if Docuseal.multitenant?
test_timeserver_url(@encrypted_config.value) if @encrypted_config.value.present?
if @encrypted_config.value.present? ? @encrypted_config.save : @encrypted_config.delete
redirect_back fallback_location: settings_notifications_path, notice: 'Changes have been saved'
redirect_back fallback_location: settings_notifications_path, notice: I18n.t('changes_have_been_saved')
else
redirect_back fallback_location: settings_notifications_path, alert: 'Unable to save'
redirect_back fallback_location: settings_notifications_path, alert: I18n.t('unable_to_save')
end
rescue HexaPDF::Error, SocketError, Submissions::TimestampHandler::TimestampError, OpenSSL::Timestamp::TimestampError
redirect_back fallback_location: settings_notifications_path, alert: 'Invalid Timeserver'
rescue SocketError, TimestampError, OpenSSL::Timestamp::TimestampError
redirect_back fallback_location: settings_notifications_path, alert: t('invalid_timeserver')
end
private
def test_timeserver_url(url)
pdf = HexaPDF::Document.new
pdf.pages.add
req = OpenSSL::Timestamp::Request.new
req.algorithm = HASH_ALGORITHM
req.message_imprint = OpenSSL::Digest.digest(HASH_ALGORITHM, 'test')
pkcs = Accounts.load_signing_pkcs(current_account)
uri = Addressable::URI.parse(url)
pdf.sign(StringIO.new,
reason: 'Test',
certificate: pkcs.certificate,
key: pkcs.key,
signature_size: 10_000,
certificate_chain: pkcs.ca_certs || [],
timestamp_handler: Submissions::TimestampHandler.new(tsa_url: url))
conn = Faraday.new(uri.origin) do |c|
c.basic_auth(uri.user, uri.password) if uri.password.present?
end
response = conn.post(uri.path, req.to_der,
'content-type' => 'application/timestamp-query')
raise TimestampError if response.status != 200 || response.body.blank?
response
end
def load_encrypted_config
@@ -0,0 +1,33 @@
# frozen_string_literal: true
class UserConfigsController < ApplicationController
before_action :load_user_config
authorize_resource :user_config
ALLOWED_KEYS = [
UserConfig::RECEIVE_COMPLETED_EMAIL
].freeze
InvalidKey = Class.new(StandardError)
def create
@user_config.update!(user_config_params)
head :ok
end
private
def load_user_config
raise InvalidKey unless ALLOWED_KEYS.include?(user_config_params[:key])
@user_config =
UserConfig.find_or_initialize_by(user: current_user, key: user_config_params[:key])
end
def user_config_params
params.required(:user_config).permit!.tap do |attrs|
attrs[:value] = attrs[:value] == '1' if attrs[:value].in?(%w[1 0])
end
end
end
@@ -0,0 +1,43 @@
# frozen_string_literal: true
class UserInitialsController < ApplicationController
before_action :load_user_config
authorize_resource :user_config
def edit; end
def update
file = params[:file]
return redirect_to settings_profile_index_path, notice: I18n.t('unable_to_save_initials') if file.blank?
blob = ActiveStorage::Blob.create_and_upload!(io: file.open,
filename: file.original_filename,
content_type: file.content_type)
attachment = ActiveStorage::Attachment.create!(
blob:,
name: 'initials',
record: current_user
)
if @user_config.update(value: attachment.uuid)
redirect_to settings_profile_index_path, notice: I18n.t('initials_has_been_saved')
else
redirect_to settings_profile_index_path, notice: I18n.t('unable_to_save_initials')
end
end
def destroy
@user_config.destroy
redirect_to settings_profile_index_path, notice: I18n.t('initials_has_been_removed')
end
private
def load_user_config
@user_config =
UserConfig.find_or_initialize_by(user: current_user, key: UserConfig::INITIALS_KEY)
end
end
@@ -9,7 +9,7 @@ class UserSignaturesController < ApplicationController
def update
file = params[:file]
return redirect_to settings_profile_index_path, notice: 'Unable to save signature' if file.blank?
return redirect_to settings_profile_index_path, notice: I18n.t('Unable to save signature') if file.blank?
blob = ActiveStorage::Blob.create_and_upload!(io: file.open,
filename: file.original_filename,
@@ -22,16 +22,16 @@ class UserSignaturesController < ApplicationController
)
if @user_config.update(value: attachment.uuid)
redirect_to settings_profile_index_path, notice: 'Signature has been saved'
redirect_to settings_profile_index_path, notice: I18n.t('signature_has_been_saved')
else
redirect_to settings_profile_index_path, notice: 'Unable to save signature'
redirect_to settings_profile_index_path, notice: I18n.t('Unable to save signature')
end
end
def destroy
@user_config.destroy
redirect_to settings_profile_index_path, notice: 'Signature has been removed'
redirect_to settings_profile_index_path, notice: I18n.t('signature_has_been_removed')
end
private
+41 -11
View File
@@ -1,13 +1,20 @@
# frozen_string_literal: true
class UsersController < ApplicationController
load_and_authorize_resource :user, only: %i[index edit new update destroy]
load_and_authorize_resource :user, only: %i[index edit update destroy]
before_action :build_user, only: :create
authorize_resource :user, only: :create
before_action :build_user, only: %i[new create]
authorize_resource :user, only: %i[new create]
def index
@pagy, @users = pagy(@users.active.order(id: :desc))
@users =
if params[:status] == 'archived'
@users.archived
else
@users.active
end
@pagy, @users = pagy(@users.where(account: current_account).order(id: :desc))
end
def new; end
@@ -15,23 +22,41 @@ class UsersController < ApplicationController
def edit; end
def create
existing_user = User.accessible_by(current_ability).find_by(email: @user.email)
if existing_user
existing_user.archived_at = nil
existing_user.assign_attributes(user_params)
existing_user.account = current_account
@user = existing_user
end
if @user.save
UserMailer.invitation_email(@user).deliver_later!
redirect_to settings_users_path, notice: 'User has been invited'
redirect_back fallback_location: settings_users_path, notice: I18n.t('user_has_been_invited')
else
render turbo_stream: turbo_stream.replace(:modal, template: 'users/new'), status: :unprocessable_entity
end
end
def update
return redirect_to settings_users_path, notice: 'Unable to update user.' if Docuseal.demo?
return redirect_to settings_users_path, notice: I18n.t('unable_to_update_user') if Docuseal.demo?
attrs = user_params.compact_blank
attrs = user_params.compact_blank.merge(user_params.slice(:archived_at))
attrs.delete(:role) if !role_valid?(attrs[:role]) || current_user == @user
if params.dig(:user, :account_id).present?
account = Account.accessible_by(current_ability).find(params[:user][:account_id])
authorize!(:manage, account)
@user.account = account
end
if @user.update(attrs)
redirect_to settings_users_path, notice: 'User has been updated'
redirect_back fallback_location: settings_users_path, notice: I18n.t('user_has_been_updated')
else
render turbo_stream: turbo_stream.replace(:modal, template: 'users/edit'), status: :unprocessable_entity
end
@@ -39,12 +64,12 @@ class UsersController < ApplicationController
def destroy
if Docuseal.demo? || @user.id == current_user.id
return redirect_to settings_users_path, notice: 'Unable to remove user'
return redirect_to settings_users_path, notice: I18n.t('unable_to_remove_user')
end
@user.update!(archived_at: Time.current)
redirect_to settings_users_path, notice: 'User has been removed'
redirect_back fallback_location: settings_users_path, notice: I18n.t('user_has_been_removed')
end
private
@@ -65,6 +90,11 @@ class UsersController < ApplicationController
end
def user_params
params.require(:user).permit(:email, :first_name, :last_name, :password, :role)
if params.key?(:user)
params.require(:user).permit(:email, :first_name, :last_name, :password,
:role, :archived_at, :account_id)
else
{}
end
end
end
@@ -9,26 +9,11 @@ class VerifyPdfSignatureController < ApplicationController
HexaPDF::Document.new(io: file.open)
end
cert_data = if Docuseal.multitenant?
Docuseal::CERTS
else
EncryptedConfig.find_by(key: EncryptedConfig::ESIGN_CERTS_KEY)&.value || {}
end
default_pkcs = GenerateCertificate.load_pkcs(cert_data)
custom_certs = cert_data.fetch('custom', []).map do |e|
OpenSSL::PKCS12.new(Base64.urlsafe_decode64(e['data']), e['password'].to_s)
end
trusted_certs = [default_pkcs.certificate,
*default_pkcs.ca_certs,
*custom_certs.map(&:certificate),
*custom_certs.flat_map(&:ca_certs).compact]
trusted_certs = Accounts.load_trusted_certs(current_account)
render turbo_stream: turbo_stream.replace('result', partial: 'result',
locals: { pdfs:, files: params[:files], trusted_certs: })
rescue HexaPDF::MalformedPDFError
render turbo_stream: turbo_stream.replace('result', html: helpers.tag.div('Invalid PDF', id: 'result'))
render turbo_stream: turbo_stream.replace('result', html: helpers.tag.div(I18n.t('invalid_pdf'), id: 'result'))
end
end
@@ -0,0 +1,37 @@
# frozen_string_literal: true
class WebhookPreferencesController < ApplicationController
EVENTS = %w[
form.viewed
form.started
form.completed
form.declined
template.created
template.updated
submission.created
submission.archived
].freeze
before_action :load_account_config
authorize_resource :account_config, parent: false
def create
@account_config.value[account_config_params[:event]] = account_config_params[:value] == '1'
@account_config.save!
head :ok
end
private
def load_account_config
@account_config =
current_account.account_configs.find_or_initialize_by(key: AccountConfig::WEBHOOK_PREFERENCES_KEY)
@account_config.value ||= {}
end
def account_config_params
params.permit(:event, :value)
end
end
@@ -0,0 +1,29 @@
# frozen_string_literal: true
class WebhookSecretController < ApplicationController
before_action :load_encrypted_config
authorize_resource :encrypted_config, parent: false
def index; end
def create
@encrypted_config.assign_attributes(value: {
encrypted_config_params[:key] => encrypted_config_params[:value]
}.compact_blank)
@encrypted_config.value.present? ? @encrypted_config.save! : @encrypted_config.delete
redirect_back(fallback_location: settings_webhooks_path, notice: I18n.t('webhook_secret_has_been_saved'))
end
private
def load_encrypted_config
@encrypted_config =
current_account.encrypted_configs.find_or_initialize_by(key: EncryptedConfig::WEBHOOK_SECRET_KEY)
end
def encrypted_config_params
params.require(:encrypted_config).permit(value: %i[key value]).fetch(:value, {})
end
end
@@ -11,15 +11,16 @@ class WebhookSettingsController < ApplicationController
@encrypted_config.value.present? ? @encrypted_config.save! : @encrypted_config.delete
redirect_back(fallback_location: settings_webhooks_path, notice: 'Webhook URL has been saved.')
redirect_back(fallback_location: settings_webhooks_path, notice: I18n.t('webhook_url_has_been_saved'))
end
def update
submitter = current_account.submitters.where.not(completed_at: nil).order(:id).last
SendFormCompletedWebhookRequestJob.perform_later(submitter)
SendFormCompletedWebhookRequestJob.perform_async({ 'submitter_id' => submitter.id,
'encrypted_config_id' => @encrypted_config.id })
redirect_back(fallback_location: settings_webhooks_path, notice: 'Webhook request has been sent.')
redirect_back(fallback_location: settings_webhooks_path, notice: I18n.t('webhook_request_has_been_sent'))
end
private
+78 -20
View File
@@ -3,8 +3,10 @@ import { encodeMethodIntoRequestBody } from '@hotwired/turbo-rails/app/javascrip
import { createApp, reactive } from 'vue'
import TemplateBuilder from './template_builder/builder'
import ImportList from './template_builder/import_list'
import ToggleVisible from './elements/toggle_visible'
import ToggleCookies from './elements/toggle_cookies'
import DisableHidden from './elements/disable_hidden'
import TurboModal from './elements/turbo_modal'
import FileDropzone from './elements/file_dropzone'
@@ -20,6 +22,13 @@ import FolderAutocomplete from './elements/folder_autocomplete'
import SignatureForm from './elements/signature_form'
import SubmitForm from './elements/submit_form'
import PromptPassword from './elements/prompt_password'
import EmailsTextarea from './elements/emails_textarea'
import ToggleOnSubmit from './elements/toggle_on_submit'
import PasswordInput from './elements/password_input'
import SearchInput from './elements/search_input'
import ToggleAttribute from './elements/toggle_attribute'
import LinkedInput from './elements/linked_input'
import CheckboxGroup from './elements/checkbox_group'
import * as TurboInstantClick from './lib/turbo_instant_click'
@@ -37,23 +46,6 @@ document.addEventListener('keyup', (e) => {
}
})
window.customElements.define('toggle-visible', ToggleVisible)
window.customElements.define('disable-hidden', DisableHidden)
window.customElements.define('turbo-modal', TurboModal)
window.customElements.define('file-dropzone', FileDropzone)
window.customElements.define('menu-active', MenuActive)
window.customElements.define('clipboard-copy', ClipboardCopy)
window.customElements.define('dynamic-list', DynamicList)
window.customElements.define('download-button', DownloadButton)
window.customElements.define('set-origin-url', SetOriginUrl)
window.customElements.define('set-timezone', SetTimezone)
window.customElements.define('autoresize-textarea', AutoresizeTextarea)
window.customElements.define('submitters-autocomplete', SubmittersAutocomplete)
window.customElements.define('folder-autocomplete', FolderAutocomplete)
window.customElements.define('signature-form', SignatureForm)
window.customElements.define('submit-form', SubmitForm)
window.customElements.define('prompt-password', PromptPassword)
document.addEventListener('turbo:before-fetch-request', encodeMethodIntoRequestBody)
document.addEventListener('turbo:submit-end', async (event) => {
const resp = event.detail?.formSubmission?.result?.fetchResponse?.response
@@ -77,8 +69,37 @@ document.addEventListener('turbo:submit-end', async (event) => {
URL.revokeObjectURL(url)
})
window.customElements.define('template-builder', class extends HTMLElement {
const safeRegisterElement = (name, element, options = {}) => !window.customElements.get(name) && window.customElements.define(name, element, options)
safeRegisterElement('toggle-visible', ToggleVisible)
safeRegisterElement('disable-hidden', DisableHidden)
safeRegisterElement('turbo-modal', TurboModal)
safeRegisterElement('file-dropzone', FileDropzone)
safeRegisterElement('menu-active', MenuActive)
safeRegisterElement('clipboard-copy', ClipboardCopy)
safeRegisterElement('dynamic-list', DynamicList)
safeRegisterElement('download-button', DownloadButton)
safeRegisterElement('set-origin-url', SetOriginUrl)
safeRegisterElement('set-timezone', SetTimezone)
safeRegisterElement('autoresize-textarea', AutoresizeTextarea)
safeRegisterElement('submitters-autocomplete', SubmittersAutocomplete)
safeRegisterElement('folder-autocomplete', FolderAutocomplete)
safeRegisterElement('signature-form', SignatureForm)
safeRegisterElement('submit-form', SubmitForm)
safeRegisterElement('prompt-password', PromptPassword)
safeRegisterElement('emails-textarea', EmailsTextarea)
safeRegisterElement('toggle-cookies', ToggleCookies)
safeRegisterElement('toggle-on-submit', ToggleOnSubmit)
safeRegisterElement('password-input', PasswordInput)
safeRegisterElement('search-input', SearchInput)
safeRegisterElement('toggle-attribute', ToggleAttribute)
safeRegisterElement('linked-input', LinkedInput)
safeRegisterElement('checkbox-group', CheckboxGroup)
safeRegisterElement('template-builder', class extends HTMLElement {
connectedCallback () {
document.addEventListener('turbo:submit-end', this.onSubmit)
this.appElem = document.createElement('div')
this.appElem.classList.add('md:h-screen')
@@ -86,13 +107,50 @@ window.customElements.define('template-builder', class extends HTMLElement {
this.app = createApp(TemplateBuilder, {
template: reactive(JSON.parse(this.dataset.template)),
backgroundColor: '#faf7f5',
locale: this.dataset.locale,
withPhone: this.dataset.withPhone === 'true',
withLogo: this.dataset.withLogo !== 'false',
editable: this.dataset.editable !== 'false',
authenticityToken: document.querySelector('meta[name="csrf-token"]')?.content,
withPayment: this.dataset.withPayment === 'true',
isPaymentConnected: this.dataset.isPaymentConnected === 'true',
withFormula: this.dataset.withFormula === 'true',
withSendButton: this.dataset.withSendButton !== 'false',
withSignYourselfButton: this.dataset.withSignYourselfButton !== 'false',
withConditions: this.dataset.withConditions === 'true',
currencies: (this.dataset.currencies || '').split(',').filter(Boolean),
acceptFileTypes: this.dataset.acceptFileTypes,
isDirectUpload: this.dataset.isDirectUpload === 'true'
acceptFileTypes: this.dataset.acceptFileTypes
})
this.component = this.app.mount(this.appElem)
this.appendChild(this.appElem)
}
onSubmit = (e) => {
if (e.detail.success && e.detail?.formSubmission?.formElement?.id === 'submitters_form') {
e.detail.fetchResponse.response.json().then((data) => {
this.component.template.submitters = data.submitters
})
}
}
disconnectedCallback () {
document.removeEventListener('turbo:submit-end', this.onSubmit)
this.app?.unmount()
this.appElem?.remove()
}
})
safeRegisterElement('import-list', class extends HTMLElement {
connectedCallback () {
this.appElem = document.createElement('div')
this.app = createApp(ImportList, {
template: JSON.parse(this.dataset.template),
multitenant: this.dataset.multitenant === 'true',
authenticityToken: document.querySelector('meta[name="csrf-token"]')?.content
})
this.app.mount(this.appElem)
+19 -6
View File
@@ -51,6 +51,10 @@ button[disabled] .enabled {
@apply input input-bordered bg-white;
}
.base-input-slim {
@apply input input-bordered bg-white h-10;
}
.base-textarea {
@apply textarea textarea-bordered bg-white rounded-3xl;
}
@@ -75,16 +79,21 @@ button[disabled] .enabled {
@apply select base-input w-full font-normal;
}
.tooltip-bottom-start:before {
transform: translateX(-30%);
.tooltip-bottom-end:before {
transform: translateX(-95%);
top: var(--tooltip-offset);
left: 100%;
right: auto;
bottom: auto;
}
.tooltip-bottom-start:after {
transform: translateX(-75%);
.tooltip-pre:before {
white-space: pre;
text-align: left;
}
.tooltip-bottom-end:after {
transform: translateX(-25%);
border-color: transparent transparent var(--tooltip-color) transparent;
top: var(--tooltip-tail-offset);
left: 50%;
@@ -95,7 +104,7 @@ button[disabled] .enabled {
.autocomplete {
background: white;
z-index: 1000;
font: 14px/22px "-apple-system", BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
font: 16px/25px "-apple-system", BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
overflow: auto;
box-sizing: border-box;
@apply border border-base-300 mt-1 rounded-md;
@@ -106,7 +115,7 @@ button[disabled] .enabled {
}
.autocomplete > div {
@apply px-2 py-1 font-normal text-sm;
@apply px-2 py-1.5 font-normal;
}
.autocomplete .group {
@@ -125,3 +134,7 @@ button[disabled] .enabled {
outline-offset: 3px;
outline-color: hsl(var(--bc) / 0.2);
}
select:required:invalid {
color: gray !important;
}
+114
View File
@@ -0,0 +1,114 @@
import SignaturePad from 'signature_pad'
import { cropCanvasAndExportToPNG } from './submission_form/crop_canvas'
window.customElements.define('draw-signature', class extends HTMLElement {
connectedCallback () {
const scale = 3
this.canvas.width = this.canvas.parentNode.clientWidth * scale
this.canvas.height = this.canvas.parentNode.clientHeight * scale
this.canvas.getContext('2d').scale(scale, scale)
this.pad = new SignaturePad(this.canvas)
this.pad.addEventListener('endStroke', () => {
this.updateSubmitButtonVisibility()
})
this.clearButton.addEventListener('click', (e) => {
e.preventDefault()
this.clearSignaturePad()
})
this.form.addEventListener('submit', (e) => {
e.preventDefault()
this.submitButton.disabled = true
this.submitImage().then((data) => {
this.valueInput.value = data.uuid
return fetch(this.form.action, {
method: 'PUT',
body: new FormData(this.form)
}).then((response) => {
this.form.classList.add('hidden')
this.success.classList.remove('hidden')
return response
})
}).finally(() => {
this.submitButton.disabled = false
})
})
}
clearSignaturePad () {
this.pad.clear()
this.updateSubmitButtonVisibility()
}
updateSubmitButtonVisibility () {
if (this.pad.isEmpty()) {
this.submitButton.style.display = 'none'
this.placeholderButton.style.display = 'block'
} else {
this.submitButton.style.display = 'block'
this.placeholderButton.style.display = 'none'
}
}
async submitImage () {
return new Promise((resolve, reject) => {
cropCanvasAndExportToPNG(this.canvas, { errorOnTooSmall: true }).then(async (blob) => {
const file = new File([blob], 'signature.png', { type: 'image/png' })
const formData = new FormData()
formData.append('file', file)
formData.append('submitter_slug', this.dataset.slug)
formData.append('name', 'attachments')
formData.append('remember_signature', 'true')
return fetch('/api/attachments', {
method: 'POST',
body: formData
}).then((resp) => resp.json()).then((attachment) => {
return resolve(attachment)
})
}).catch((error) => {
return reject(error)
})
})
}
get submitButton () {
return this.querySelector('button[type="submit"]')
}
get clearButton () {
return this.querySelector('button[aria-label="Clear"]')
}
get placeholderButton () {
return this.querySelector('button[disabled]')
}
get canvas () {
return this.querySelector('canvas')
}
get valueInput () {
return this.querySelector('input[name^="values"]')
}
get form () {
return this.querySelector('form')
}
get success () {
return this.querySelector('#success')
}
})
@@ -7,7 +7,7 @@ export default class extends HTMLElement {
resize () {
if (this.textarea.clientHeight < this.textarea.scrollHeight) {
this.textarea.style.height = `${this.textarea.scrollHeight}px`
this.textarea.style.height = `${this.textarea.scrollHeight + 5}px`
}
}
+15
View File
@@ -0,0 +1,15 @@
export default class extends HTMLElement {
connectedCallback () {
this.items.forEach((item) => {
item.addEventListener('change', (e) => {
this.items.forEach((item) => {
item.checked = item === e.target && e.target.checked
})
})
})
}
get items () {
return this.querySelectorAll('input[type="checkbox"]')
}
}
+13 -8
View File
@@ -17,13 +17,18 @@ export default targetable(class extends HTMLElement {
this.toggleState()
fetch(this.dataset.src).then((response) => response.json()).then((urls) => {
const isSafariIos = /iPhone|iPad|iPod/i.test(navigator.userAgent)
fetch(this.dataset.src).then(async (response) => {
if (response.ok) {
const urls = await response.json()
const isSafariIos = /iPhone|iPad|iPod/i.test(navigator.userAgent)
if (isSafariIos && urls.length > 1) {
this.downloadSafariIos(urls)
if (isSafariIos && urls.length > 1) {
this.downloadSafariIos(urls)
} else {
this.downloadUrls(urls)
}
} else {
this.downloadUrls(urls)
alert('Failed to download files')
}
})
}
@@ -48,9 +53,9 @@ export default targetable(class extends HTMLElement {
fileRequests.reduce(
(prevPromise, request) => prevPromise.then(() => request()),
Promise.resolve()
)
this.toggleState()
).finally(() => {
this.toggleState()
})
}
downloadSafariIos (urls) {
@@ -0,0 +1,56 @@
const emailRegexp = /([^@;,<>\s]+@[^@;,<>\s]+)/g
export default class extends HTMLElement {
connectedCallback () {
if (this.dataset.limit) {
this.textarea.addEventListener('input', () => {
const emails = this.textarea.value.match(emailRegexp) || []
this.updateCounter(emails.length)
})
}
}
updateCounter (count) {
let counter = document.getElementById('emails_counter')
let bulkMessage = document.getElementById('bulk_message')
if (count < 2) {
counter?.remove()
return
}
if ((count + 10) > this.dataset.limit) {
if (!counter) {
counter = document.createElement('span')
counter.id = 'emails_counter'
counter.classList.add('text-xs', 'right-0', 'absolute')
counter.style.bottom = '-15px'
this.textarea.parentNode.append(counter)
}
counter.innerText = `${count} / ${this.dataset.limit}`
}
if (this.dataset.bulkEnabled !== 'true') {
if (!bulkMessage) {
bulkMessage = document.createElement('span')
bulkMessage.id = 'bulk_message'
bulkMessage.classList.add('text-xs', 'left-0', 'absolute')
bulkMessage.style.bottom = '-15px'
this.textarea.parentNode.append(bulkMessage)
}
bulkMessage.innerHTML = '<a class="link" data-turbo="false" href="/upgrade">Upgrade</a> to bulk send multiple recipients'
}
}
get textarea () {
return this.querySelector('textarea')
}
}
+3 -57
View File
@@ -9,10 +9,6 @@ export default actionable(targetable(class extends HTMLElement {
]
connectedCallback () {
if (this.dataset.isDirectUpload === 'true') {
import('@rails/activestorage')
}
this.addEventListener('drop', this.onDrop)
this.addEventListener('dragover', (e) => e.preventDefault())
@@ -48,61 +44,11 @@ export default actionable(targetable(class extends HTMLElement {
this.classList.toggle('opacity-50')
}
async uploadFiles (files) {
uploadFiles () {
this.toggleLoading()
if (this.dataset.isDirectUpload === 'true') {
const { DirectUpload } = await import('@rails/activestorage')
await Promise.all(
Array.from(files).map(async (file) => {
const upload = new DirectUpload(
file,
'/direct_uploads',
this.input
)
return new Promise((resolve, reject) => {
upload.create((error, blob) => {
if (error) {
console.error(error)
return reject(error)
} else {
return resolve(blob)
}
})
}).catch((error) => {
console.error(error)
})
})
).then((blobs) => {
if (this.dataset.submitOnUpload) {
this.querySelectorAll('[name="blob_signed_ids[]"]').forEach((e) => e.remove())
}
blobs.forEach((blob) => {
const input = document.createElement('input')
input.type = 'hidden'
input.name = 'blob_signed_ids[]'
input.value = blob.signed_id
this.append(input)
})
if (this.dataset.submitOnUpload === 'true') {
this.closest('form').querySelector('button[type="submit"]').click()
}
}).finally(() => {
if (this.dataset.submitOnUpload !== 'true') {
this.toggleLoading()
}
})
} else {
if (this.dataset.submitOnUpload) {
this.closest('form').querySelector('button[type="submit"]').click()
}
if (this.dataset.submitOnUpload) {
this.closest('form').querySelector('button[type="submit"]').click()
}
}
}))
@@ -20,7 +20,7 @@ export default class extends HTMLElement {
fetch = (text, resolve) => {
const queryParams = new URLSearchParams({ q: text })
fetch('/api/template_folders_autocomplete?' + queryParams).then(async (resp) => {
fetch('/template_folders_autocomplete?' + queryParams).then(async (resp) => {
const items = await resp.json()
resolve(items)
+31
View File
@@ -0,0 +1,31 @@
export default class extends HTMLElement {
connectedCallback () {
if (this.target) {
this.input.value = this.target.value
this.target.addEventListener('input', (e) => {
this.input.value = e.target.value
})
this.target.addEventListener('linked-input.update', (e) => {
this.input.value = e.target.value
})
}
}
get input () {
return this.querySelector('input')
}
get target () {
if (this.dataset.targetId) {
const listItem = this.closest('[data-targets="dynamic-list.items"]')
if (listItem) {
return listItem.querySelector(`#${this.dataset.targetId}`)
} else {
return document.getElementById(this.dataset.targetId)
}
}
}
}
+35
View File
@@ -0,0 +1,35 @@
import { target, targetable } from '@github/catalyst/lib/targetable'
export default targetable(class extends HTMLElement {
static [target.static] = [
'visiblePasswordIcon',
'hiddenPasswordIcon',
'passwordInput',
'togglePasswordVisibility'
]
connectedCallback () {
this.togglePasswordVisibility.addEventListener('click', this.handleTogglePasswordVisibility)
document.addEventListener('turbo:submit-start', this.setInitialPasswordType)
}
disconnectedCallback () {
this.togglePasswordVisibility.removeEventListener('click', this.handleTogglePasswordVisibility)
document.removeEventListener('turbo:submit-start', this.setInitialPasswordType)
}
handleTogglePasswordVisibility = () => {
this.passwordInput.type = this.passwordInput.type === 'password' ? 'text' : 'password'
this.toggleIcon()
}
setInitialPasswordType = () => {
this.passwordInput.type = 'password'
this.toggleIcon()
}
toggleIcon = () => {
this.visiblePasswordIcon.classList.toggle('hidden', this.passwordInput.type === 'password')
this.hiddenPasswordIcon.classList.toggle('hidden', this.passwordInput.type === 'text')
}
})
+25
View File
@@ -0,0 +1,25 @@
export default class extends HTMLElement {
connectedCallback () {
this.input.addEventListener('focus', () => {
if (this.title) {
this.title.classList.add('hidden', 'md:block')
this.input.classList.add('w-60')
}
})
this.input.addEventListener('blur', (e) => {
if (this.title && !e.target.value) {
this.title.classList.remove('hidden')
this.input.classList.remove('w-60')
}
})
}
get input () {
return this.querySelector('input')
}
get title () {
return document.querySelector(this.dataset.title)
}
}
+6 -2
View File
@@ -5,8 +5,12 @@ export default targetable(class extends HTMLElement {
static [target.static] = ['canvas', 'input', 'clear', 'button']
async connectedCallback () {
this.canvas.width = this.canvas.parentNode.parentNode.clientWidth
this.canvas.height = this.canvas.parentNode.parentNode.clientWidth / 3
const scale = 3
this.canvas.width = this.canvas.parentNode.clientWidth * scale
this.canvas.height = this.canvas.parentNode.clientHeight * scale
this.canvas.getContext('2d').scale(scale, scale)
const { default: SignaturePad } = await import('signature_pad')
@@ -7,6 +7,7 @@ export default class extends HTMLElement {
preventSubmit: 1,
minLength: 1,
showOnFocus: true,
debounceWaitMs: 200,
onSelect: this.onSelect,
render: this.render,
fetch: this.fetch
@@ -23,10 +24,13 @@ export default class extends HTMLElement {
if (input && item[field]) {
input.value = item[field]
input.dispatchEvent(new CustomEvent('linked-input.update', { bubbles: true }))
}
if (textarea && item[field]) {
textarea.value = textarea.value.replace(/[^;,\s]+$/, item[field] + ' ')
textarea.dispatchEvent(new Event('input', { bubbles: true }))
}
})
}
@@ -37,16 +41,16 @@ export default class extends HTMLElement {
if (q) {
const queryParams = new URLSearchParams({ q, field: this.dataset.field })
fetch('/api/submitters_autocomplete?' + queryParams).then(async (resp) => {
this.currentFetch ||= fetch('/submitters_autocomplete?' + queryParams)
this.currentFetch.then(async (resp) => {
const items = await resp.json()
if (q.length < 3) {
resolve(items.filter((e) => e[this.dataset.field].startsWith(q)))
} else {
resolve(items)
}
resolve(items)
}).catch(() => {
resolve([])
}).finally(() => {
this.currentFetch = null
})
} else {
resolve([])
@@ -0,0 +1,28 @@
export default class extends HTMLElement {
connectedCallback () {
this.input.addEventListener('change', (event) => {
if (this.dataset.attribute) {
this.target[this.dataset.attribute] = event.target.checked
}
if (this.dataset.className) {
this.target.classList.toggle(this.dataset.className, event.target.value !== this.dataset.value)
if (this.dataset.className === 'hidden' && this.target.tagName === 'INPUT') {
this.target.disabled = event.target.value !== this.dataset.value
}
}
if (this.dataset.attribute === 'disabled') {
this.target.value = ''
}
})
}
get input () {
return this.querySelector('input[type="checkbox"]') || this.querySelector('select')
}
get target () {
return document.getElementById(this.dataset.targetId)
}
}
+17
View File
@@ -0,0 +1,17 @@
export default class extends HTMLElement {
connectedCallback () {
this.button.addEventListener('click', () => {
const expirationDate = new Date()
expirationDate.setFullYear(expirationDate.getFullYear() + 10)
const expires = expirationDate.toUTCString()
document.cookie = this.dataset.key + '=' + this.dataset.value + '; expires=' + expires + '; path=/'
})
}
get button () {
return this.querySelector('button')
}
}

Some files were not shown because too many files have changed in this diff Show More