From 9b7745c565b13c38b38a1d0479b3c84c02b040db Mon Sep 17 00:00:00 2001 From: Pete Matsyburka Date: Wed, 11 Feb 2026 17:32:04 +0200 Subject: [PATCH 01/54] hide resend on archived --- app/views/submissions/show.html.erb | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/app/views/submissions/show.html.erb b/app/views/submissions/show.html.erb index a40d0c9a7..8e578df1e 100644 --- a/app/views/submissions/show.html.erb +++ b/app/views/submissions/show.html.erb @@ -68,7 +68,7 @@ <% end %> <% end %> - <% elsif @submission.submitters.to_a.size == 1 && !@submission.expired? && !@submission.submitters.to_a.first.declined_at? && !@submission.archived_at? %> + <% elsif @submission.submitters.to_a.size == 1 && !@submission.expired? && !@submission.submitters.to_a.first.declined_at? && !@submission.archived_at? && !@submission.template&.archived_at? %> <%= render 'shared/clipboard_copy', text: submit_form_url(slug: @submission.submitters.to_a.first.slug, host: form_link_host), class: 'base-button', icon_class: 'w-6 h-6 text-white', copy_title: t('copy_share_link'), copied_title: t('copied_to_clipboard') %> <% end %> @@ -159,7 +159,7 @@ <%= (@submission.template_submitters || @submission.template.submitters).find { |e| e['uuid'] == submitter&.uuid }&.dig('name') || "#{(index + 1).ordinalize} Submitter" %> - <% if signed_in? && can?(:update, @submission) && submitter && !submitter.completed_at? && !submitter.declined_at? && !@submission.archived_at? && !@submission.expired? && !submitter.start_form_submission_events.any? %> + <% if signed_in? && can?(:update, @submission) && submitter && !submitter.completed_at? && !submitter.declined_at? && !@submission.archived_at? && !@submission.template&.archived_at? && !@submission.expired? && !submitter.start_form_submission_events.any? %> <%= link_to edit_submitter_path(submitter), class: 'shrink-0 inline md:hidden md:group-hover:inline', data: { turbo_frame: 'modal' } do %> <%= svg_icon('pencil', class: 'w-5 h-5') %> @@ -225,15 +225,15 @@ <% end %> - <% if signed_in? && submitter && submitter.email && !submitter.completed_at && !@submission.archived_at? && can?(:update, @submission) && Accounts.can_send_emails?(current_account) && !@submission.expired? && !submitter.declined_at? %> + <% if signed_in? && submitter && submitter.email && !submitter.completed_at && !@submission.archived_at? && !@submission.template&.archived_at? && can?(:update, @submission) && Accounts.can_send_emails?(current_account) && !@submission.expired? && !submitter.declined_at? %>
<%= button_to button_title(title: submitter.sent_at? ? t('re_send_email') : t('send_email'), disabled_with: t('sending')), submitter_send_email_index_path(submitter_slug: submitter.slug), class: 'btn btn-sm btn-primary w-full' %>
<% end %> - <% if signed_in? && submitter && submitter.phone && !submitter.completed_at && !@submission.archived_at? && can?(:update, @submission) && !@submission.expired? && !submitter.declined_at? %> + <% if signed_in? && submitter && submitter.phone && !submitter.completed_at && !@submission.archived_at? && !@submission.template&.archived_at? && can?(:update, @submission) && !@submission.expired? && !submitter.declined_at? %> <%= render 'submissions/send_sms_button', submitter: %> <% end %> - <% if signed_in? && submitter && !submitter.completed_at? && !@submission.archived_at? && can?(:create, @submission) && !@submission.expired? && !submitter.declined_at? %> + <% if signed_in? && submitter && !submitter.completed_at? && !@submission.archived_at? && !@submission.template&.archived_at? && can?(:create, @submission) && !@submission.expired? && !submitter.declined_at? %>
<%= t('sign_in_person') %> From 8f6f418f54908433ccbd765a1979cc10632068ab Mon Sep 17 00:00:00 2001 From: Pete Matsyburka Date: Thu, 12 Feb 2026 14:27:15 +0200 Subject: [PATCH 02/54] fix font size --- lib/submissions/generate_result_attachments.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/submissions/generate_result_attachments.rb b/lib/submissions/generate_result_attachments.rb index 72eeb9f10..580b9be15 100644 --- a/lib/submissions/generate_result_attachments.rb +++ b/lib/submissions/generate_result_attachments.rb @@ -162,7 +162,7 @@ def generate_pdfs(submitter) pdf.trailer.info[:DocumentID] = document_id pdf.pages.each do |page| - font_size = (([page.box.width, page.box.height].min / A4_SIZE[0].to_f) * 9).to_i + font_size = [(([page.box.width, page.box.height].min / A4_SIZE[0].to_f) * 9).to_i, 4].max cnv = page.canvas(type: :overlay) text = From 7a2c37454e548f1049564757d6f88ba03eaa27fc Mon Sep 17 00:00:00 2001 From: Pete Matsyburka Date: Thu, 12 Feb 2026 15:52:04 +0200 Subject: [PATCH 03/54] update favicon --- app/views/pwa/manifest.json.erb | 2 +- app/views/shared/_meta.html.erb | 1 + public/apple-icon-180x180.png | Bin 10383 -> 10808 bytes public/apple-touch-icon-precomposed.png | Bin 11203 -> 11668 bytes public/apple-touch-icon.png | Bin 11203 -> 11668 bytes public/favicon-16x16.png | Bin 695 -> 807 bytes public/favicon-32x32.png | Bin 1638 -> 1820 bytes public/favicon-96x96.png | Bin 5375 -> 5584 bytes public/favicon.ico | Bin 1150 -> 1150 bytes public/favicon.svg | 5 +++++ 10 files changed, 7 insertions(+), 1 deletion(-) create mode 100644 public/favicon.svg diff --git a/app/views/pwa/manifest.json.erb b/app/views/pwa/manifest.json.erb index d70431204..680220618 100644 --- a/app/views/pwa/manifest.json.erb +++ b/app/views/pwa/manifest.json.erb @@ -4,7 +4,7 @@ "id": "/", "icons": [ { - "src": "/logo.svg", + "src": "/favicon.svg", "type": "image/svg+xml", "sizes": "any" }, diff --git a/app/views/shared/_meta.html.erb b/app/views/shared/_meta.html.erb index 4a9c201f4..a6d492f9d 100644 --- a/app/views/shared/_meta.html.erb +++ b/app/views/shared/_meta.html.erb @@ -27,4 +27,5 @@ + diff --git a/public/apple-icon-180x180.png b/public/apple-icon-180x180.png index 54ae5fcff1002b097c581f150b1ca66ae04ba9ea..08c0c3b8d9b308db38f08f20c35980ce0bb336bc 100644 GIT binary patch literal 10808 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%0Ac~SiN8Th zaZX=I3Hp1a?*eh3X%{%_o}d&dEyxeqKq=Oo6oA|bqup|A)j;aByA!jQ!E?S&8M828 z@5?8|!y39*6pjy}pVz+hn|O)z_wx?~prQ|lW!8Rz_^s^5q}z7Wk=T3OTprTV*%Bth zWAi>s4I0eXbCoecx`u^B3C%B3Q)C&uUU|xQLPaFhVpUF}4}%{NEhJ z>GIF$xfkO&gkyY=j^2(^=>Apl?(9zW2as&thCIk4aFNP@O~IV+KztR$2}_ zyhj0*EaD;Tu=Q49bH^1|>A7b{14(Zdjf9(&AX$)nY*Q9a=*nJBFO!^Nwz7CEjxwrR zDly7fE-o%B1ld@*NIY?P+kw(kgP{;fIWsa3$^*Tpp;{Z1eazp`OZ)!oROiv<$cn^V z=g(PqfRx%iNO66aw$&38+(yoKSjRA4DP8QI0Q-CHR;8Iri708GwrhRlvEd+*M9TM- zvYZ}3a#>Lc@7L5y3RrQqa$Vl95Kqv$3|e`x&!Wj6efI1adVITCBN@@Ued@REWG#N~ zsDMP@D&pgf*W5~kQ-Q({pvaQsGQt%%1th&TlhU=GbR-8oroz0x`Q$+lW_ANSVxDQq zQm3xhg_fKSaXC&F(f`yM14iep-wQo2b183ZeY(^YR98wA*2IGW+@7Y`HyJcvMd@!G z8v{dVy7`Zg8PuVAO`YrTW5tpeY?^gCV`~z}h4V!J^sbNr-fBWWmemz<{Lh9l@3(Ei zyzvcmKk$RoKiFMTw#)qLdNTkjDhMM?JM{Q8tU@5wv${B9Qa{KDu0mRvkeySY98v7 zMCQ?UHif%l;JYAtvsMrMjW4D|8r9{TRbqMt7k##iLG);Q9Rtw%b!pYh(t6Z}Wj>hYhrgaq^p~aeK-DPQG|;*(kmq zw{@QWYsv(uWC7(EPa-*UUYi8VJs&SE;R;nCd6M|*KTaw1`Eq);A_k7Gpf z9Gw)A#AZ<=)KR}d4g2;wD$j}2gB|K0VC_eKxEd5s~8MhT$@_>20VWF{^-xRF-(J4p+z z=a{N=S>z8X?-(B@Ab_w5{$A=_ry=!tXT|7RNakmmY5j;|6tQ&yogo4>KSh&K@|X#M zcRk*}eI>`h=rpqv0d%;gDS*jd$eGJ6XM z8{v9~&fHA|@?~x-9&J9hHk=}(q|2Wyy-R+wntq?!v!{StBl9R$<_@k)0b(HaT$j6mk2jm)9_F5|@Xgk&31Z~qTPBz^ zF8K${XW$k>RALf_%?0u)6&U_ZqAvXlY_o-Ms&J@zr3oKF?RQor~%3V^{EVKgdfb9854au$lhmK c#J{3^VZ5h`c;nFnZv_P?zf+U1ku?kbA6R|5I{*Lx literal 10383 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 diff --git a/public/apple-touch-icon-precomposed.png b/public/apple-touch-icon-precomposed.png index d6a6d23c3a3a97645a40ee67caf0f0e6604c9deb..67113d91047ec1fd06ff18cfe02f1b2283035960 100644 GIT binary patch literal 11668 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 JE>pA$`5!46%?bbj literal 11203 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 diff --git a/public/apple-touch-icon.png b/public/apple-touch-icon.png index d6a6d23c3a3a97645a40ee67caf0f0e6604c9deb..67113d91047ec1fd06ff18cfe02f1b2283035960 100644 GIT binary patch literal 11668 zcmW-nbzBr*7sh9IS-Pcj=?>|VMp9Z(njfNcNQ=ONv>=Em4NHd-(p^hQBi#r{BMs8; z_s4FWN`ljz^_#~1&J@0KRFOhnpLl|mp^|DV-*$@Ncu?qhC=ZQ~)%IwXB z5gp4r;g|H&PZcn1W54qEJk>J}C1>l4S4hCo4behczurnOSvrWbMy3#sFCIKK*7=*h zHk(nhb6u9%__Tbf>B1kUaiz1&Uv;;!kmL6i=eBCmPol(gR^m#|{O8K@?D>5dRv7X4 z@-mk?LI5McgKdU#gH{B$<7vebl$bN1n+4up?%5*f%Dof$YIM7!<#oANoz5SyY#5I~J~{lmT{d zKHhkZ(ieHmKfhvc8S2JB5qA5=iG7H_AGlwX0^dq&pNV@u$uRjz4giFYWZ0j(3Yrt- zET}24!vr1oKWR*aSP?*4PbJI;uy|*!$L5$yx2qw#77|h&Ho}&g*e*GUr0i{2jqopM zMYQf!Hp1Wd)S`N*xJJojbQ_9#aUPnDOXA8AddDE&ICa;BLxKBN4b}Xm&T5>YG$V>F zgBkchK|vKoUk5oQ0){8&vLs$nat2K9inWPb{ot;f8ZLSgL5Mt_g{N5PHKgeuVM2@`MXe^J7fw$)?8lgR_5l)N<%r(GQMr&6LIB6)db}*VLJ#7~rED>CFguiQ z(u7VUBnKkyB$zahqpNVT;{ZYXeH|>cQgA+S>3A2CgIk*MOCP!1WcYGK1*!1p#I7MDd2;C6y>&O9S_o4u!QMWd$XI& zIRlAK*A%I40yxt|#TGfXv)J2POs;=+bCKyb zmlv*&?S-j`nn*vu+2;l{Hnf{FLS%>#N{*I|*lOgEjI<`JHE9!RQvQ8rKw~p@q#*h) z1>3q%X}~r!uk19>`o;b)XW|QpuFR{SUI<@$bH;}KSPjm4Vb@>bPr;8x*ApNspW8f< z&A6jhJcXOb7<)(~Q9pu-V^ZfV8ho9g7b)k_7djHf>RfGt+n)2X{ zBB@cZRo=g~8F=f(f34R3HZB5a++B3G8d4bNsv9XwV+ZpKV_l^yjM|>_Y@SObR%sCO`BiXhB8hqqBo1>? zl@VWwif|i_c@2cQulNZPP!T9CVPh9;eb!!Jlk2GUAm8(!LSD(xwd|2i5UbS7`24zB zG{G-Z2k0_9#;6n%1xVIX&CD-kl2t8YLO)U}Bu%C@1 zM>0Ji%~a3ta~Cf9olc|x;wBX42M6BG+2CxDvBotKs;`y3v(nb{uW#oRN^jc>wOMAb zkGlgbmB@=OPQy9J(hyWbhy(^eK7gU`OTA8(-UCYVP{)%j^Lwvvm@(k{54PCJgdK|- zQe=>lCVYH7#-~Q(!xzbamlsHuM6diaqj4LS;qzxcCS24NZEVb7bFd2K;LJ$9Sm<&6 z@8L5oDC+N6R4yXH704?Q#T=uKmpNi*)k!7nn7IpEB#VHPmk%TBR*R;}x)^Y2!vrPo z8_uLB7&OiR38^M4L6WVHgpT6C)-SWUJmh!wj@$nw6lYfw(qeD0CKPKKgNh4qLqW-vZ z45yMci14#Z;K?i{i~qU)nQI`h)W7;gTn^wA`ZnLD8{lNKggx0CX>-*l<)`x0I@p0H zyaWS)NA_mclX3SEleUngjAzMj@C60O#}CfGl_%4&T70%+WT|7MrRTgcBa5Q?T`?Xs zuITmW>C+gB5uU$1z@OssMwf=I%fqjNSsq-j<4j|(E+P`A77yyVFkKnH(C-j-P3T{D z6;22aJ_4mct2s*digL;OL3pg{h^j?cnLmT;xU(Ybqe_?i`QqS0n)hGdV$1&9bS6)Z1M{WzC~=m3@gN}b{7kyChEP#aD3U78+lt`p zvf|pDH%lc-%sYp3^F%9i&0F>J5RGGqyxhi{lLwIPX}Rd?;)~wyAtI4dneLqptkKT~ z6QsiAp|<261#a_Ozr2lqB;2V$4^1nHm0xJzvn23uEr<%WmJo9E4fg9&E@=8vuaN?( zn`By8ZbU?;fV?t&r3+7}pP`eS_ZrDa6pKj{aS-vsk6nMHB=BK~r;jPLbr4MrAQ6a@ zFLEr_e=%Ix=XqMgx%;Nd_zQy(i1Ngt#(K)H^aTJ4_{bITK%V9+3jip_yRVtC#w7Q3 zoB8)1DSe#c?-I7JWNlNRrhJ_YCZpl5CQiYy?w*vq+YMRPOnVaR%ScWpo2xa! zIHx(mQ&A&y<8R?5bROx7Bv7YvvR1SNa178i$du(z{ON1F{gGT&+AIC#$pENix{l(> z>Sku*;|k+v8KV5{1m8U9=Nj70gx($!`%TTKmCI+jn) z-N1CU_dDtn$s}g`n=?KlQb@`;V?-;54>=t)I>~!c+JthOh=mZD(A>X#MEYH$WzU7` zb;OC_B$FHcC86j6IaH&`xdFcNBBRHv3Yy9ChNbe)Uuk9Gi`90f=Uh=2Dp$FYP?SAh zstoluB3STT4vPyu?w7nnJabW|GMVNft-PbOW9y?Nh~YZIoA0H%;T%?o(d(-a+(zf; z^jaAgo5XL0Ova-_p)Y#tNnf&qQ5$XI#NsFNS5ITQD-Ym6HJBdiNjF~96>+J{yxg9E;25Y!kE|$^SY+wwDiT= z^EYzbClnvdEeyT2(rox^M1>WzAtei8kOb|*9(Pv-45`0p*{<-jD_)uoR90KjahGfc zO5QZtj-yvW?;A%lBb>fY&#{%IH|&hFE6Z=3a`^6wQQMZtkksP+GBcF%P}JE9wS-jQ z>=Q#CG5V2TS-?^Tt5sk4!@ilyzVq7M)TN^+UDShjBt|ZU@h~r)dEL}QCJ*jD7xFMX zkH!c>^EcJM#fmO@N(beym(AC8e{G0bNpb*GnDAbEE)KU#nHjxgv1 zhD@fq{rU2$M))s(Ea~fuUy^H$HD1rtASiK~16aF^x@M-Qa93V(w1Q)JT_qK{fZPPr z=HG1Mm@!HiF3u0NTY?jhb1kg|lz-8XyHbYC=^;6$Mtgc5OET$4kvmKy;iI@5)KS`n z+iKV{+PviX2-Xm)GH&}KDp%m$cY)r=3S<{aprGJ-srLb+)O&ToIUHXZ)z@6N4v!fs z^C>>xr^kkuFKrR;UXGz8oSiVS6~LwXy2RV|C;Xbyr3__IM_g3B0rQ$#YlV&6t$Bp^ zTGy)o&E%RC^=li^DgyJkRyCOe4Z@}L&`~VK3_=>9+k+z=hPz=N-E~s7H~Pb6s`Osr zciM^6q?sI5X8(W4%FT!Q+6{QhjITFHWyx{_GsJ)0SQWm*1R$4>PF9q+>eDQuV;b*y z6&E)J01dP1=1IC8_fAuEnGFwnK8xS#6FM0OMbzrP!U$aI*~3@SDIp7;QWf^2m+?Z$ zyIu1z61!HZpu%rSke^nsq)uEWkPxa-P?J}-M16T(D zrwdleQaaY=U8zYEX;MAmk^MO*xVxUeV$GZ&eS*?L1(Mugk%NQN1l(4Q3qp``Y#EUJ#aA$NWpKiLUQmBtyXrJKE-u?iqlN4 zioO~xw4kKBu6s>FNTCG=q7VFAnw)K|ib5GL=x19la-8`>zuZ9O^<}1Yi8q=VPi8+o zf6_%Tv8gcmv-cPOow}oO@~GD?Y<%=ckU)wtyqSb#+S>mEW5`Lnn8dj&PVqDAYi~C3 z-+SwLu1K%lAYa2D+hpqErk<~ChLhrBfPZ8@y zgnopp(Rh%1=m-atL%=^kCjO{Hd+bp@6MKts9Y=d()M2K2-f8-LP^I5OW8`t?=#kq^ zb<`{cyn&(=it47zcba)x9h`z4RqBwqRT<{;bEc0(kE0#ZSjK z@f9~pgA~gB4lW0t>GwaqWBr&Ve{5T7z+!@d)NxJ6MqYkJi~NNeJ#Y^W7;z9O3$k<+ zXMQ~Wi0BdHTj~_?dlG#q4|Ywpywglj55&(+JY&mjp#* za;i!A#bxEF0HAe^@xtgxhbkQ~eS0a=YwAu`lOvtx2ga?NYYpXkGOJr0@! z;eS3aNZ>H%na@D!ByDu~P#Yr|T_j96VEEO)`Amqw14zcq7-^Z4oB9`Q%eQz%Nh7Z< zv=A8fZ4E?VlBcQ0Ynj7-EZn$wDU#45wtr_T#=lm}n%Y+`V73BqW{Atuw(A=076dDd z^Uo2~Wbq>F8nwXJWA^GdSIZ>B5)uxHJl%cC?7RbucRqWSkeqp+mr3xBMT4r4k#c^$Ohs zL~An!CD1!U_ELjW?^T z&-8tzb(N%M_22iL9jyHoZc{n)4|8L@RiIVHAh9<%h`5!O_2(++ut|2-wK6;jL;`^9 zm@fq~^0SxexCNVzjIA8H@`$B$Q5Q=kd@X|6u=yBUo3|CKHp4RHO6H-Lf|f=^;?k!9 zQ{*_qr{S-k9s?XOd15mD&wW=6mokH0WZeFGReQ7AdK0T$axrtZC+;@~;%NO-1ua;| z%spM-`Qw+;;S1IX(vvD0C>uKX6GWc8zuA+8{Z;kA&)y!dgHBU-Lr3xdqMg2~-X@jj zzP)pPlJ@jrg3adlG)+%T&^o&xZVUp5&`Sc;?$TZ6UFHK-tLOK@Q!aMl<=MD3S^UbE zt4+UT>J4rolA`^>8y+bY>6uUxq~EgHGE3Z~@?f#$5oy(zC#vmW*gka~1YN<&%2(U+ z?WeTb@A;m%BC|GQb)fOx4y7%VcT(rh_H|st4!>@Gn#jx{R;2Q08K!{}6JZVyD-xr!0?{E0N z+S9oL3HWU?kz zN$Hg(@ZrZZGj24{<+{YfGPTe1uKyitdVu=qGWFHNU6`i>sI;B6yF6!D{~gHvA^`;3 z%2&S^h4x#__>|(l)hE6sj~!`G77x*EbbeLv=Y(=Xpo~P>2#*1ROqiX_}|&)LX*WjI8;DtLB|4J6CWlWRkZWWL|)lAW3ZS4F)Waan2t%TEL7-=gEfk~R{L34O{c)?uC7QaYqfY zVoF3s8)V{5w+ifYiyH&BMZ=3d#y}wQM~YzBL-U;+0y63^IYbR(o0FfVP`W>Z=b)b_#uR=$EPse)-eR6eqkEqi`icL)!Q?2nL)v|tFtE5 zX3XtKRR7EVe4oP^&+StXzI2qjV|{KJ{I4lAL0^FcE(_8135lR)C-b-Iiz~H*$tR_} z%X?_7vi+2D2t2&?nwOEzmpk0a@1oQcixJYX-9R#0h+zpR%U#CW|HPJm@nR<>?M?SV z(>Yv%*ddw*NA_G_%Ry+2)Ve+4sS8w)!?{bzhkUwx8S7CmcQGI6Yuh$R*lEoV@qj6l z7f`xgqh~K16I8K22uhP-(Om!0g^X4)R^5h@JoqRW)9~4(u*YK+`-LV!6}_wI3G4zG zHN#ih*7-K|_KZWs9QSFuk*_w!eL5J8KztyYE|SaKfQe$kwoP#UkWQ}aC35t(y%>{P zai!#=8j6g8U>TaZ7NHnOwq1GcL9ME#r8HFQY38)Gz zAT@Q!UZrS~k%@^~pIh$ITWQHH&*Box?MfT<`7(|7$(SbAKBP04*}zVZ|9**t`e1h-p(EPRvXy%%GUCW zlFcmokJTECO*4;W=r)G`kV8H(?AL`0D)Z+|F{p-u!HD)ig zh%s1onyDA>D>zqRoT*ycv-$(gql!q13*Ywuifj;E_1Q`+TP2?%(DiINf;dmR;lbjc z%w@8-8N}vwvRm%=@72=Y$VC?66|FqZwNv>$(-!t9Bh_j@C8!n~D4%bJ)1AZ8uj}9H zM8uNj{8Zx!<{58489f=Rllt5mZ>OyS)@`F6Q>KFJnO~CdC%iSHn4No$)*iFp12Zkd zp%flP6c9lW?2E&vzJFMibZ0HdP*QA#49;;iM{O8-UN0J`wYg9bF-_O2fN{9<`vP(O z{ozIb2{H;yC6WcoM9gVF0>;9$zc+8zui)}+ucpPm3}*Z#hc|@V zidZh<)PLBEZNz^_s;It>bM+S+`OyZ+gKP$8~1>ZqxM$bRNm$nq0D&5CZ@}dJPnQ$3YUO1Rot^ z+Qnv%5K~><;2ck2Sr`cV^lMzyztbJfGpO z@kWl6%nH>*#_$8YgTTrsJbOg<-z9M_Bkgty!{iuW900+Kjim_c^mgfeE!@l-S8pn> z8G`Ku0op;ZuV1i`@37Y38`Q6gOX4MXPLE@={1Hf1&X?&W>NE(%UW(2=H2U0W>`zx& z+Hr8LDwxUy;`AP6urA7*;8mk;K@AKBC#Jx-swkB`9JKBOp!?(41+~VTWOcAw`oQJV zk8uLgt{BpkBET_))0}~f>rWTmP=Nw!JGUhIb8|~35XJGnR77VR8_FoL(&IOvt@`LU zd_MFq8F)B#BvU21Dp?}UO5Ju!N=;?g9+EFiUd)~;Op+=FMf04JPmMv^EA3NCc<7ZW z-*$^J%!?m|MoM89u+`!^gZ&m_*6RyZgPJ>#LsWXTN>4oLH2mL3bH1%3!UpGUO{T0cV0){mWH~9r>HK zYmGkH${zu85E&nKbB8@wYQ-}$8F)#mj%6N6L(pv<{8`D7O{0(JGE9Z3$*#JA_LqBu z1w2Cbzsl0gah@SCS4eI(l!+tHefx>({+jmh(F>`xSP$tlcmx%G^R;l<%@OG!95k)16|o7AJ?*5ODE;;gxnugXpW@0>^3;o|>V9 zM4O!zP(+Y|9vtGAHdM&|Z9WO@5o1nspw(5q0$Eu@Db1>!Vw4bytP>Eqv02>*2B-zE zxiKL$JvJ!m@iylwgcFoQ+8|b0x;(fJNkf~BGQ|UfIB)nfj~%60%2@aY2BU@n5Jm(} za}nb07~i}bi03$%tiYBAUV}HtO>Cyh(BV4*Uq*AR%`X`WsM$XX`6I$Zh*uT!m~u?v z5r6&riyDcSgK|78!vTxOl(|OHc94#NU$VEs*iZmiK??)*GWxXBE$DJDb|$8@WxucJ zyMr;fs##m$W*vk^O?{*x2yf4S`ied-#AIyOk2`@;nGCpm|DHx(&03is9(>hYbbBi4 z@+cSDVw84dRAsN{O+sI$bVnmwz{CfWcYmgPy{*W+)! zpNfD%*07JupY*(tf2!Qj+QiM&L$0?d5*Bq@aBASuzgYlWhx=Qy^~bfpH9%6_g`oW} zvob3MucRpw|9Z)Y)F1@xwT<6?F>ecu3opN1Py{YfBOwH7bFIT!MW!gtoKsc6jAGW- zA6Bv~@WO&dlt4;u#l3qUS*U-_@G=9wJ>tsmHN74_>KW zuNA=%o7-MBiB=ZXZLYCRe>o=ZFP^f~W>;S=ss>qAE~@;Ux%hT8@Ee4G1o8%Mh5`X0 znz=Hd2a%n*IN0R|Yc)YVaL!?UWcg44+Rx?=13;)2Y-@1(9N7cOg+&WOkSCk;dh`3t z&?4wSPn-#z(0VKq6g?z&(d8jm{lH(50n7^C3&~o1%>r;+(m(yO!-Kh!{apDK@-m$Z zG9nqcK=z=89t3;3S6#n3gYNi6$^r@v)|vHY-(P*^8+feGzDx)viXwBdtoaXe*xqQZ zmxm8~9s+L!<~A_0#i1)k!OZitx|dhEf#fRAA4Senk?_@t-H^3j0!Uy-QrB1#TEtVP znY=B>`pi_~ZjkQ@V>rF9KxgA+6-Dg9X$zK8(iom7qipxdauWk?#*|Tt4XYa-0Q~Dv z0+0ssbnhsL{C9CNc*oIU8qaRyX2*=HVY`O|#b%az4?z`Ea+; zZWQB(X37sMv|?v_)!zE5X4!pKA7*p+d`Em(I2on6JGL6jB5v5xdhJN{8a$P{&-sOn zbfb5C?%$MLijuG;W2TQAPW2gM-fcI-X=lWup(u@$Zz`smV&Z1W;-SEuhPIw!0K-h1 zDIV7&+N;cf`DTm$zVO|ZVI;vRnWOpW0Y>K5n{~ADQ``gUrqL(&47&(+R`jTUlukUaZt`VgsnZtm5uy*u8PrxZMM@Lir(LBn_#P4|H#Fb0ieh zBQT;x%KXK@?Hcg6u&lUmc4Wwi+bAXkE^6IdSc5!%#8Mt?6nA%F)f^g8=#MS7@oYLf7_WN4K8d<5KwG5>}FPySOL*uuBdhf6CrCt>WcQlRPmPWk>2z+CclX1JJMiWUF zaeZbXteHNxYR-BVU>~pJe7_&rg1?Y|t-wkjbNQ?YnIieJt?Pxy z-cgZak?L66l(k=shUt>(NHE4Bq02O^_(daly+kTt?~3X=_H z1jcY#rIm#`wj!y_lO)%)0^3K|#eX!rbm28wkBH{a1t#dapTK|3-N048|9Gx|52_G_ zE7zr0PiU(&PX07(g@319g12J-3U^{(f~(D4YS=TS1tD&PWytM8zXTg!!J9bP)5h5H zUq@3^uQ7#ztq7{)@UN{pBqY%wP{p&Fzsyg~oxx!<-sIzq<|miX_>Tqv4m-cBvc!ED znO6>`ZJ&u26(swU5KysyN0TF&->q6^zvAb{FtcG9%6Gl8&<4k0VJ}`9HPP55bhUMN zQ>Q9vZ*UbR4gV04Of|2~9}Mm>i145v0`#8Jg=@ls-E9AOsO)SOS2J%w0N{P?m&9SJ zX`e?-rQi#N6jtLkEcb+y5WXzq|58Yj<16zbqJB;1o z=?YJ{s1>|5ibwD^v(44$L?zi7w>l2#q-PT2sjT^pLJjrefB4}=&wM7ykq0O1kd(C) zj9H?!Be=vuRY4w|p%K+IBHGU{+DKAOi7i_(Q)Rs`H;$`A{-B82 zH|2~CzpWyibL;`2eRV&-wzqISXyt6|BcSKQ70!K_c#FR~HG-geprFi!;{3E)iN3oP z9|b2e{%A3Ad>~dQ(uJ3yvqgJA6dIg3rF2Fhy>rrD`HN-Z?IG*inmg%D872f{lrgo@+N%?1P zFYF#E8*zUU+%F3PLq+slT}n+#C-nz?AvuBXTkbWl{h8TZps17tKe)Jl*c~3m4u7vQ zvh|f*o~0aK_`=HZ`?3GHuEclR@Mrso0Yv_*+|8Kj&Yf~v!ilxefCCeez!O=Je3?1b zDzG3HGU}{u3%^tGnyvB!0LoLl!!2wy4$`I5D#)ZpB}-y&yfm-BoIuiT+j~@45H=}4 z3TDB^+dr;aQ68CVSl093Nx_QvsiVmjR)woetMaV(XP&eWD8%d<)KHKw@og()KQBK~tlHdXJxcChrK278V80nEPqoVcJy zf%k@<@mPR^C_XEa=kH4b(o7XCjvGl`{xsRe23bF*BTjNf3Uh%IXEbn&h-l6HvH!M& z$^O;%_A_A$pE)}_Og->_~~=QnegT|_2; z&VVDa-}X-yK;uQ1W*onkdnyVTJM3%9W03URkO&gA0tE^-sNGDw_`cRP9?J|8?;zfP zbmX#cH=drtSBEaR@aFJG5TWQKT?C2Rj-uR|-ZD(a(|@AC97A51&4Xh}TY&p;#Q=hZU`zKE!PJ6~508q)JL% zUr>Bn_qmGvdv?aRYuA|bS0A--WwS{h~~Ir zb@3Ip5Ua__Hph||R((a0hb#5_A?`6Ya8^0f2mqAW76lB)FYZ>EhYYBnZ4*OAwPwG) z_vcDDeueu6%dl8ba!tl%X^3_+fA_h$D1b@h2)vL!OH#z z?=|spCKxIO?a|})3FDRp1&(?O)tnb*=)n{%PPj5D=h1Hht39^>y8^Hc`-yqS71r^H z%uWs}Ln9xY0qvH9&z@KusLappH#*gOMgtO`a;xw#->ncTZXE!pqu|VFb}*FjK}u`t z#Z6BOI6zq1xEI}vwXb=%sv&Od zwpBd^JP9t``Rg>qW*PRK)zyFb1k~{;i95o1YmYNne8_VM3nuI|5akP?;$5bJM%)E8(jaoac>C8sxuqPPLK3wwf( z;~(KPrD;>|*NsqwV*krnQN0Z7WJPzeA|X-?7C;D)=QVBR1fsbrh!XcGBP$uB>qpie z-icWKC|MDDVE%WE97r!p7Vh3dRxt)A>O`i)6dNBzFC~uha4b JE>pA$`5!46%?bbj literal 11203 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 diff --git a/public/favicon-16x16.png b/public/favicon-16x16.png index 45a2768fd88b62119ac4407a4288a7e796568efe..e38921b8ffd46d9106ab82888c15ffc76a3a7189 100644 GIT binary patch delta 765 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 delta 1603 zcmV-J2E6&44(1GyHh&vQL_t(og`JmwY?SpK$6ufCb4O{9(rH8)sqM8DbOUq_ono@e0Da!pb2?0(y=pFehkzIk~ z@fBr=yyBB9{C>3T)b07?0D(Z@7qugwOih}FQt^McG9H(&y)GCb#VS z&WBDl71}tJPaZfeyLLJLbKkdbn-6?pI#yA69SWqgfhOKYFkrb=DOB>KpB7vJ8yaRaHbe}|EAAh6R;!+ z)D@g9>{sOGaH8`Q;Y8;(2J1RS`ubz5uLr2z`hWUS;16DSspX{aH5k+AC$P{1#mE)J z*!C}h+F~Ip{1cZk-%yn}mx|l=yr`G6|BY!hs=CB~c8vK)F#*FZkw;MOF6IXW zrCE%=JJ=HW)j(@>q3@_p96FNa7qd4f#$9FvuJ!rDsRbj2GLLLrS*k)DZ$r%hL3mKm z_kV|48t$JkpuV#vkvswX*9)yRz#`8#mVdtUVOu5ZMfGjT?c&*BzKZM^ibWnR;2ZQ~ z47p-d{Aw2c1|rY7B3+jtU}B{5%s?!9`y``Zl$4AicU;Q6sx}EDZ4Ju>u1}AR{V^Qk zHDKTpMOdP`E#IKZm7bE5M&Ow52+S#6G=F6Y$H0S(DW*wJa8KLiT5iC0}I9Q-e4^H6;WO8nRi|t zPo6UXj%{}U{vFVAEEcWIt$_3?T7Nzr!Ax+G&!T~M@=inAah$E5IH=p!{Q)8!p7RP@ z^?7Y-vL+O`M?jWcE_rXPzFd@ft~x&gY9?|@!f)xa9Z}{&>1M#c3mS*3KYC6V7GNxda{qu0(jyGyPr5a<42aVDa`>$6eR@GQxiVRDblLq1MRG zvb1#$Wv^|Otj|v@#yx2{na<%@{YACI$8bw@8Ojd1ry4F`~;>qldIjJqLefr># z>$+bgSDc#0Wc)Bq``cFE z_UVH|lO|G7d8~esqk%mF`G1A!3}8g%TUA2~ej~efIUmNO^Ujaz0PvUUQ0ILT#~p5M ztaEWT02_db)1w>(T^oL?>!LC~{ij*5q)k*?@j9lwK_jXp6*(@VcLO(q_JC9Z)p?;A zl%HkB%N}36{nhb8JTrcr9*adQZHS}#z*HgA-~!4H$91;kf52Rp06ZV5wlx)&lWA0u zh@iJI(MMbb7=$yZ4x@4yw8ydL?5*4S+S%FU{Xen0U~+(W1tkCg002ovPDHLkV1na2 B5&HlD diff --git a/public/favicon-96x96.png b/public/favicon-96x96.png index a1eee7461778d0adcdcf83a2151fb3bd75f683fa..600a59f4dc10f47d0366fc22f8c325fe3250abd8 100644 GIT binary patch delta 5581 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 Ik1&M&3nlxpRR910 literal 1150 zcmai!%WKq76vl6TQ9-a9A5=k?1w|`$p-7Y4!GFM=_&`J`3NG4Ck|GF(BKW{|QMA55 z)IUOZTGtkFQ!A*G8%H-)YNuL7aVGvwlB;o?Vumj#=bZ05x%ZxvQL2q!M~C9RRh?X+ z)Ow{<7a&9=i(`}`_rI&xtL$(+bItY53$O_0+`vpZetg{-GP{=2ZwseaY2RFhUk5*2 zKf397<`B;n9hlSDhk0(ir+;c?v5pR-c5;uvCu*E^gScPvVyRKGT>PVOehOFKXXCOq zVsm}Nm7Ulz6(-9hw}1G}TFbGPy^O;7!1^vpO!kD|aD(W$_}Y(eh+XR9Yz}BI8bDJ| zwb@y{*I|;LF7ew|>jdU7{O4(X-^9H0o)bjJ`pR(+v1u>?o@y_?Yc=?(9&MTDq!mho z)lJmBOztK4qJt>3ddG{;>2lOXyw*xf?6GiJ%PDGn#$TjA=dHd}u596q9<!Pt-nP0~@lf)N@`}=~oo!Wc79qmE(d7pSupD@Qy_6}B8TfJEE<(q-q_=o>TFS&_k zZLyB_qi*4V8Sou^O`n<5zxJ1l=J`S + + + + From 9377766e520c1f578b678a0717544b098de84c7b Mon Sep 17 00:00:00 2001 From: Pete Matsyburka Date: Sat, 14 Feb 2026 07:57:53 +0200 Subject: [PATCH 04/54] fix url download --- app/controllers/templates_uploads_controller.rb | 2 +- lib/download_utils.rb | 10 +++++----- lib/submitters/normalize_values.rb | 2 +- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/app/controllers/templates_uploads_controller.rb b/app/controllers/templates_uploads_controller.rb index e8c00aeaa..ddee4e90d 100644 --- a/app/controllers/templates_uploads_controller.rb +++ b/app/controllers/templates_uploads_controller.rb @@ -56,7 +56,7 @@ def save_template!(template, url_params) def create_file_params_from_url tempfile = Tempfile.new tempfile.binmode - tempfile.write(DownloadUtils.call(params[:url]).body) + tempfile.write(DownloadUtils.call(params[:url], validate: true).body) tempfile.rewind filename = URI.decode_www_form_component(params[:filename]) if params[:filename].present? diff --git a/lib/download_utils.rb b/lib/download_utils.rb index dce5427a0..668c6d967 100644 --- a/lib/download_utils.rb +++ b/lib/download_utils.rb @@ -35,16 +35,16 @@ module DownloadUtils module_function - def call(url) + def call(url, validate: Docuseal.multitenant?) uri = begin URI(url) rescue URI::Error Addressable::URI.parse(url).normalize end - validate_uri!(uri) if Docuseal.multitenant? + validate_uri!(uri) if validate - resp = conn.get(uri) + resp = conn(validate:).get(uri) raise UnableToDownload, "Error loading: #{uri}" if resp.status >= 400 @@ -56,10 +56,10 @@ def validate_uri!(uri) raise UnableToDownload, "Error loading: #{uri}. Can't download from localhost." if uri.host.in?(LOCALHOSTS) end - def conn + def conn(validate: Docuseal.multitenant?) Faraday.new do |faraday| faraday.response :follow_redirects, callback: lambda { |_, new_env| - validate_uri!(new_env[:url]) if Docuseal.multitenant? + validate_uri!(new_env[:url]) if validate } end end diff --git a/lib/submitters/normalize_values.rb b/lib/submitters/normalize_values.rb index 16cb268c6..1eee3dfdd 100644 --- a/lib/submitters/normalize_values.rb +++ b/lib/submitters/normalize_values.rb @@ -236,7 +236,7 @@ def find_or_create_blob_from_url(account, url) return blob if blob - data = DownloadUtils.call(url).body + data = DownloadUtils.call(url, validate: true).body checksum = Digest::MD5.base64digest(data) From 680ab9dbed36421058368c561514049eec763c4e Mon Sep 17 00:00:00 2001 From: Pete Matsyburka Date: Sat, 14 Feb 2026 10:18:58 +0200 Subject: [PATCH 05/54] raise invalid param --- lib/submitters.rb | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/submitters.rb b/lib/submitters.rb index c557dd3e5..47fbf0146 100644 --- a/lib/submitters.rb +++ b/lib/submitters.rb @@ -14,6 +14,7 @@ module Submitters UnableToSendCode = Class.new(StandardError) InvalidOtp = Class.new(StandardError) MaliciousFileExtension = Class.new(StandardError) + ArgumentError = Class.new(StandardError) DANGEROUS_EXTENSIONS = Set.new(%w[ exe com bat cmd scr pif vbs vbe js jse wsf wsh msi msp @@ -133,7 +134,7 @@ def create_attachment!(submitter, params) filename: file.original_filename, content_type: file.content_type) else - ActiveStorage::Blob.find_signed(params[:blob_signed_id]) + raise ArgumentError, 'file param is missing' end ActiveStorage::Attachment.create!( From 34ea639c25fce1e8cccb0e3c19f9e24119eaf21c Mon Sep 17 00:00:00 2001 From: Pete Matsyburka Date: Sat, 14 Feb 2026 11:05:05 +0200 Subject: [PATCH 06/54] escape wildcard query --- lib/submissions.rb | 5 +++-- lib/template_folders.rb | 4 +++- lib/templates.rb | 4 +++- 3 files changed, 9 insertions(+), 4 deletions(-) diff --git a/lib/submissions.rb b/lib/submissions.rb index aaf18ea4a..8fd26bf0a 100644 --- a/lib/submissions.rb +++ b/lib/submissions.rb @@ -18,7 +18,8 @@ def search(current_user, submissions, keyword, search_values: false, search_temp def plain_search(submissions, keyword, search_values: false, search_template: false) return submissions if keyword.blank? - term = "%#{keyword.downcase}%" + sanitized = ActiveRecord::Base.sanitize_sql_like(keyword.downcase) + term = "%#{sanitized}%" arel_table = Submitter.arel_table @@ -31,7 +32,7 @@ def plain_search(submissions, keyword, search_values: false, search_template: fa if search_template submissions = submissions.left_joins(:template) - arel = arel.or(Template.arel_table[:name].lower.matches("%#{keyword.downcase}%")) + arel = arel.or(Template.arel_table[:name].lower.matches("%#{sanitized}%")) end submissions.joins(:submitters).where(arel).group(:id) diff --git a/lib/template_folders.rb b/lib/template_folders.rb index 00a6fc02a..d4a3af9e3 100644 --- a/lib/template_folders.rb +++ b/lib/template_folders.rb @@ -20,7 +20,9 @@ def filter_by_full_name(template_folders, name) def search(folders, keyword) return folders if keyword.blank? - folders.where(TemplateFolder.arel_table[:name].lower.matches("%#{keyword.downcase}%")) + sanitized = ActiveRecord::Base.sanitize_sql_like(keyword.downcase) + + folders.where(TemplateFolder.arel_table[:name].lower.matches("%#{sanitized}%")) end def filter_active_folders(template_folders, templates) diff --git a/lib/templates.rb b/lib/templates.rb index 73aaef807..2abd93ddd 100644 --- a/lib/templates.rb +++ b/lib/templates.rb @@ -52,7 +52,9 @@ def search(current_user, templates, keyword) def plain_search(templates, keyword) return templates if keyword.blank? - templates.where(Template.arel_table[:name].lower.matches("%#{keyword.downcase}%")) + sanitized = ActiveRecord::Base.sanitize_sql_like(keyword.downcase) + + templates.where(Template.arel_table[:name].lower.matches("%#{sanitized}%")) end def fulltext_search(current_user, templates, keyword) From ca0acb34d612d4d9cdfcb34d3f1c7180ff5e2a38 Mon Sep 17 00:00:00 2001 From: Pete Matsyburka Date: Sat, 14 Feb 2026 11:27:00 +0200 Subject: [PATCH 07/54] use url for open modal --- app/views/layouts/application.html.erb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/layouts/application.html.erb b/app/views/layouts/application.html.erb index 89646c06d..5558e8696 100644 --- a/app/views/layouts/application.html.erb +++ b/app/views/layouts/application.html.erb @@ -22,7 +22,7 @@ <% if params[:modal].present? %> <% url_params = Rails.application.routes.recognize_path(params[:modal], method: :get) %> <% if url_params[:action] == 'new' %> - + <% end %> <% end %> From ed8c313bd47948de988198779749b393760b9633 Mon Sep 17 00:00:00 2001 From: Pete Matsyburka Date: Sat, 14 Feb 2026 11:40:59 +0200 Subject: [PATCH 08/54] timestamp controller multitenant --- config/routes.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/routes.rb b/config/routes.rb index 4447a2399..2703f1368 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -56,7 +56,7 @@ resources :account_custom_fields, only: %i[create] resources :user_configs, only: %i[create] resources :encrypted_user_configs, only: %i[destroy] - resources :timestamp_server, only: %i[create] + resources :timestamp_server, only: %i[create] unless Docuseal.multitenant? resources :dashboard, only: %i[index] resources :setup, only: %i[index create] resource :newsletter, only: %i[show update] From fe6baba8bf1b3a66aa39e787b463ce7433742a0d Mon Sep 17 00:00:00 2001 From: Pete Matsyburka Date: Sat, 14 Feb 2026 12:10:21 +0200 Subject: [PATCH 09/54] fix erb lint --- app/views/submissions/show.html.erb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/submissions/show.html.erb b/app/views/submissions/show.html.erb index 8e578df1e..5d19f351f 100644 --- a/app/views/submissions/show.html.erb +++ b/app/views/submissions/show.html.erb @@ -225,7 +225,7 @@

<% end %> - <% if signed_in? && submitter && submitter.email && !submitter.completed_at && !@submission.archived_at? && !@submission.template&.archived_at? && can?(:update, @submission) && Accounts.can_send_emails?(current_account) && !@submission.expired? && !submitter.declined_at? %> + <% if signed_in? && submitter && submitter.email && !submitter.completed_at && !@submission.archived_at? && !@submission.template&.archived_at? && can?(:update, @submission) && Accounts.can_send_emails?(current_account) && !@submission.expired? && !submitter.declined_at? %>
<%= button_to button_title(title: submitter.sent_at? ? t('re_send_email') : t('send_email'), disabled_with: t('sending')), submitter_send_email_index_path(submitter_slug: submitter.slug), class: 'btn btn-sm btn-primary w-full' %>
From 347be0137d93e09e647687c0a1fca35ec85b9ded Mon Sep 17 00:00:00 2001 From: Pete Matsyburka Date: Sat, 14 Feb 2026 16:31:34 +0200 Subject: [PATCH 10/54] refactor 2fa --- .../submissions_download_controller.rb | 28 +++++++----- app/controllers/submit_form_controller.rb | 23 +++------- .../submit_form_decline_controller.rb | 4 +- .../submit_form_download_controller.rb | 3 +- .../submit_form_draw_signature_controller.rb | 3 +- .../submit_form_invite_controller.rb | 3 +- .../submit_form_values_controller.rb | 8 ++-- lib/submitters/authorized_for_form.rb | 45 +++++++++++++++++++ 8 files changed, 84 insertions(+), 33 deletions(-) create mode 100644 lib/submitters/authorized_for_form.rb diff --git a/app/controllers/submissions_download_controller.rb b/app/controllers/submissions_download_controller.rb index 4bcd3237e..8e76039b5 100644 --- a/app/controllers/submissions_download_controller.rb +++ b/app/controllers/submissions_download_controller.rb @@ -27,20 +27,18 @@ def index Submissions::EnsureResultGenerated.call(last_submitter) - if last_submitter.completed_at < TTL.ago && !signature_valid && !current_user_submitter?(last_submitter) - Rollbar.info("TTL: #{last_submitter.id}") if defined?(Rollbar) + if !signature_valid && !current_user_submitter?(last_submitter) + return head :not_found unless Submitters::AuthorizedForForm.call(@submitter, current_user, request) - return head :not_found + if last_submitter.completed_at < TTL.ago + Rollbar.info("TTL: #{last_submitter.id}") if defined?(Rollbar) + + return head :not_found + end end if params[:combined] == 'true' - url = build_combined_url(@submitter) - - if url - render json: [url] - else - head :not_found - end + respond_with_combined(last_submitter) else render json: build_urls(last_submitter) end @@ -48,6 +46,16 @@ def index private + def respond_with_combined(submitter) + url = build_combined_url(submitter) + + if url + render json: [url] + else + head :not_found + end + end + def current_user_submitter?(submitter) current_user && current_user.account.submitters.exists?(id: submitter.id) end diff --git a/app/controllers/submit_form_controller.rb b/app/controllers/submit_form_controller.rb index f723aa32f..a9e2f88cd 100644 --- a/app/controllers/submit_form_controller.rb +++ b/app/controllers/submit_form_controller.rb @@ -9,7 +9,7 @@ class SubmitFormController < ApplicationController before_action :load_submitter, only: %i[show update completed] before_action :maybe_render_locked_page, only: :show - before_action :maybe_require_link_2fa, only: %i[show update] + before_action :maybe_require_link_2fa, only: %i[show] CONFIG_KEYS = [].freeze @@ -17,7 +17,7 @@ def show submission = @submitter.submission return redirect_to submit_form_completed_path(@submitter.slug) if @submitter.completed_at? - return render :email_2fa if require_email_2fa?(@submitter) + return render :email_2fa unless Submitters::AuthorizedForForm.pass_email_2fa?(@submitter, request) @form_configs = Submitters::FormConfigs.call(@submitter, CONFIG_KEYS) @@ -48,7 +48,7 @@ def show end def update - if require_email_2fa?(@submitter) + unless Submitters::AuthorizedForForm.call(@submitter, current_user, request) return render json: { error: I18n.t('verification_required_refresh_the_page_and_pass_2fa') }, status: :unprocessable_content end @@ -84,7 +84,9 @@ def update def completed raise ActionController::RoutingError, I18n.t('not_found') if @submitter.account.archived_at? - redirect_to submit_form_path(params[:submit_form_slug]) if require_email_2fa?(@submitter) + return if Submitters::AuthorizedForForm.call(@submitter, current_user, request) + + redirect_to submit_form_path(params[:submit_form_slug]) end def success; end @@ -92,10 +94,7 @@ def success; end private def maybe_require_link_2fa - return if @submitter.submission.source != 'link' - return unless @submitter.submission.template&.preferences&.dig('shared_link_2fa') == true - return if cookies.encrypted[:email_2fa_slug] == @submitter.slug - return if @submitter.email == current_user&.email && current_user&.account_id == @submitter.account_id + return if Submitters::AuthorizedForForm.pass_link_2fa?(@submitter, current_user, request) redirect_to start_form_path(@submitter.submission.template.slug) end @@ -117,12 +116,4 @@ def build_attachments_index(submission) ActiveStorage::Attachment.where(record: submission.submitters, name: :attachments) .preload(:blob).index_by(&:uuid) end - - def require_email_2fa?(submitter) - return false if submitter.submission.template&.preferences&.dig('require_email_2fa') != true && - submitter.preferences['require_email_2fa'] != true - return false if cookies.encrypted[:email_2fa_slug] == submitter.slug - - true - end end diff --git a/app/controllers/submit_form_decline_controller.rb b/app/controllers/submit_form_decline_controller.rb index 918903fe7..a8f969c33 100644 --- a/app/controllers/submit_form_decline_controller.rb +++ b/app/controllers/submit_form_decline_controller.rb @@ -11,7 +11,9 @@ def create submitter.completed_at? || submitter.submission.archived_at? || submitter.submission.expired? || - submitter.submission.template&.archived_at? + submitter.submission.template&.archived_at? || + !Submitters::AuthorizedForForm.call(submitter, current_user, + request) ApplicationRecord.transaction do submitter.update!(declined_at: Time.current) diff --git a/app/controllers/submit_form_download_controller.rb b/app/controllers/submit_form_download_controller.rb index d6e0b6921..3ebdc5e2c 100644 --- a/app/controllers/submit_form_download_controller.rb +++ b/app/controllers/submit_form_download_controller.rb @@ -17,7 +17,8 @@ def index @submitter.submission.template&.archived_at? || AccountConfig.exists?(account_id: @submitter.account_id, key: AccountConfig::ALLOW_TO_PARTIAL_DOWNLOAD_KEY, - value: false) + value: false) || + !Submitters::AuthorizedForForm.call(@submitter, current_user, request) last_completed_submitter = @submitter.submission.submitters .where.not(id: @submitter.id) diff --git a/app/controllers/submit_form_draw_signature_controller.rb b/app/controllers/submit_form_draw_signature_controller.rb index 773eb9e71..5ba141c1a 100644 --- a/app/controllers/submit_form_draw_signature_controller.rb +++ b/app/controllers/submit_form_draw_signature_controller.rb @@ -12,7 +12,8 @@ def show return redirect_to submit_form_completed_path(@submitter.slug) if @submitter.completed_at? - if @submitter.submission.template&.archived_at? || @submitter.submission.archived_at? + if @submitter.submission.template&.archived_at? || @submitter.submission.archived_at? || + !Submitters::AuthorizedForForm.call(@submitter, current_user, request) return redirect_to submit_form_path(@submitter.slug) end diff --git a/app/controllers/submit_form_invite_controller.rb b/app/controllers/submit_form_invite_controller.rb index ab1f26c34..bcc848ae9 100644 --- a/app/controllers/submit_form_invite_controller.rb +++ b/app/controllers/submit_form_invite_controller.rb @@ -45,7 +45,8 @@ def can_invite?(submitter) !submitter.completed_at? && !submitter.submission.archived_at? && !submitter.submission.expired? && - !submitter.submission.template&.archived_at? + !submitter.submission.template&.archived_at? && + Submitters::AuthorizedForForm.call(submitter, current_user, request) end def filter_invite_submitters(submitter, key = 'invite_by_uuid') diff --git a/app/controllers/submit_form_values_controller.rb b/app/controllers/submit_form_values_controller.rb index e1a6b9ab7..affd37ba4 100644 --- a/app/controllers/submit_form_values_controller.rb +++ b/app/controllers/submit_form_values_controller.rb @@ -7,10 +7,12 @@ class SubmitFormValuesController < ApplicationController 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? || + return render json: {} if submitter.completed_at? || + submitter.declined_at? || + submitter.submission.template&.archived_at? || submitter.submission.archived_at? || - submitter.submission.expired? + submitter.submission.expired? || + !Submitters::AuthorizedForForm.call(submitter, current_user, request) value = submitter.values[params['field_uuid']] attachment = submitter.attachments.where(created_at: params[:after]..).find_by(uuid: value) if value.present? diff --git a/lib/submitters/authorized_for_form.rb b/lib/submitters/authorized_for_form.rb new file mode 100644 index 000000000..27fa411cc --- /dev/null +++ b/lib/submitters/authorized_for_form.rb @@ -0,0 +1,45 @@ +# frozen_string_literal: true + +module Submitters + module AuthorizedForForm + Unauthorized = Class.new(StandardError) + + module_function + + def call(submitter, current_user, request) + pass_email_2fa?(submitter, request) && pass_link_2fa?(submitter, current_user, request) + end + + def pass_email_2fa?(submitter, request) + return false unless submitter + + return true if submitter.submission.template&.preferences&.dig('require_email_2fa') != true && + submitter.preferences['require_email_2fa'] != true + return true if request.cookie_jar.encrypted[:email_2fa_slug] == submitter.slug + + return true if request.params[:two_factor_token].present? && + Submitter.signed_id_verifier.verified(request.params[:two_factor_token], + purpose: :email_two_factor) == submitter.slug + + false + end + + def pass_link_2fa?(submitter, current_user, request) + return false unless submitter + + return true if submitter.submission.source != 'link' + return true unless submitter.submission.template&.preferences&.dig('shared_link_2fa') == true + return true if request.cookie_jar.encrypted[:email_2fa_slug] == submitter.slug + return true if submitter.email == current_user&.email && current_user&.account_id == submitter.account_id + + if request.params[:two_factor_token].present? + link_2fa_key = [submitter.email.downcase.squish, submitter.submission.template.slug].join(':') + + return true if Submitter.signed_id_verifier.verified(request.params[:two_factor_token], + purpose: :email_two_factor) == link_2fa_key + end + + false + end + end +end From 40052a2d7cfe65addac0bd0df8c90121bd90389d Mon Sep 17 00:00:00 2001 From: Pete Matsyburka Date: Sat, 14 Feb 2026 17:42:08 +0200 Subject: [PATCH 11/54] use query_params --- app/views/shared/_search_input.html.erb | 2 +- app/views/submissions_dashboard/index.html.erb | 6 +++--- .../submissions_filters/_applied_filters.html.erb | 10 +++++----- app/views/submissions_filters/_filter_modal.html.erb | 2 +- app/views/templates/show.html.erb | 6 +++--- app/views/webhook_settings/show.html.erb | 6 +++--- 6 files changed, 16 insertions(+), 16 deletions(-) diff --git a/app/views/shared/_search_input.html.erb b/app/views/shared/_search_input.html.erb index 78f8208d2..8457b2af1 100644 --- a/app/views/shared/_search_input.html.erb +++ b/app/views/shared/_search_input.html.erb @@ -6,7 +6,7 @@ <% end %> <% if params[:q].present? %>
diff --git a/app/views/submissions_dashboard/index.html.erb b/app/views/submissions_dashboard/index.html.erb index 286e0b910..d61943d64 100644 --- a/app/views/submissions_dashboard/index.html.erb +++ b/app/views/submissions_dashboard/index.html.erb @@ -35,19 +35,19 @@ <% if is_show_tabs %>
@@ -27,7 +27,7 @@ <%= svg_icon('user', class: 'w-5 h-5 shrink-0') %> <%= current_account.users.accessible_by(current_ability).where(account: current_account).find_by(email: params[:author])&.full_name || 'NA' %> <% end %> - <%= link_to url_for(params.to_unsafe_h.except(:author)), class: 'rounded-lg ml-1 hover:bg-base-content hover:text-white' do %> + <%= link_to url_for(params: request.query_parameters.except('author')), class: 'rounded-lg ml-1 hover:bg-base-content hover:text-white' do %> <%= svg_icon('x', class: 'w-5 h-5') %> <% end %>
@@ -46,7 +46,7 @@ <% end %> <% end %> - <%= link_to url_for(params.to_unsafe_h.except(:completed_at_from, :completed_at_to)), class: 'rounded-lg ml-1 hover:bg-base-content hover:text-white' do %> + <%= link_to url_for(params: request.query_parameters.except('completed_at_from', 'completed_at_to')), class: 'rounded-lg ml-1 hover:bg-base-content hover:text-white' do %> <%= svg_icon('x', class: 'w-5 h-5') %> <% end %> @@ -65,7 +65,7 @@ <% end %> <% end %> - <%= link_to url_for(params.to_unsafe_h.except(:created_at_to, :created_at_from)), class: 'rounded-lg ml-1 hover:bg-base-content hover:text-white' do %> + <%= link_to url_for(params: request.query_parameters.except('created_at_to', 'created_at_from')), class: 'rounded-lg ml-1 hover:bg-base-content hover:text-white' do %> <%= svg_icon('x', class: 'w-5 h-5') %> <% end %> diff --git a/app/views/submissions_filters/_filter_modal.html.erb b/app/views/submissions_filters/_filter_modal.html.erb index af4a9b86d..8794197a2 100644 --- a/app/views/submissions_filters/_filter_modal.html.erb +++ b/app/views/submissions_filters/_filter_modal.html.erb @@ -10,7 +10,7 @@ <% if params[:with_remove] %>
- <%= link_to t('remove_filter'), "#{params[:path]}?#{params.to_unsafe_h.slice(:q).merge(local_assigns[:default_params]).to_query}", class: 'link', data: { turbo_frame: :_top } %> + <%= link_to t('remove_filter'), "#{params[:path]}?#{request.query_parameters.slice('q').merge(local_assigns[:default_params]).to_query}", class: 'link', data: { turbo_frame: :_top } %>
<% end %> <% end %> diff --git a/app/views/templates/show.html.erb b/app/views/templates/show.html.erb index 82ffb69eb..65ee77177 100644 --- a/app/views/templates/show.html.erb +++ b/app/views/templates/show.html.erb @@ -30,7 +30,7 @@ <% if is_show_tabs %>
- +
<%= svg_icon('list', class: 'w-5 h-5') %> <%= t('all') %> @@ -41,7 +41,7 @@
<% end %>
- +
<%= svg_icon('clock', class: 'w-5 h-5') %> <%= t('pending') %> @@ -52,7 +52,7 @@
<% end %>
- +
<%= svg_icon('circle_check', class: 'w-5 h-5') %> <%= t('completed') %> diff --git a/app/views/webhook_settings/show.html.erb b/app/views/webhook_settings/show.html.erb index 37a15640b..a8f16dd59 100644 --- a/app/views/webhook_settings/show.html.erb +++ b/app/views/webhook_settings/show.html.erb @@ -85,9 +85,9 @@

<%= t('events_log') %>

- <%= link_to t('all'), url_for(params.to_unsafe_h.except(:status)), style: 'margin-bottom: -1px', class: "tab h-10 text-base #{params[:status].blank? ? 'tab-active tab-bordered' : 'pb-[3px]'}" %> - <%= link_to t('succeeded'), url_for(params.to_unsafe_h.merge(status: 'success')), style: 'margin-bottom: -1px', class: "tab h-10 text-base #{params[:status] == 'success' ? 'tab-active tab-bordered' : 'pb-[3px]'}" %> - <%= link_to t('failed'), url_for(params.to_unsafe_h.merge(status: 'error')), style: 'margin-bottom: -1px', class: "tab h-10 text-base #{params[:status] == 'error' ? 'tab-active tab-bordered' : 'pb-[3px]'}" %> + <%= link_to t('all'), url_for(params: request.query_parameters.except('status')), style: 'margin-bottom: -1px', class: "tab h-10 text-base #{params[:status].blank? ? 'tab-active tab-bordered' : 'pb-[3px]'}" %> + <%= link_to t('succeeded'), url_for(params: request.query_parameters.merge('status' => 'success')), style: 'margin-bottom: -1px', class: "tab h-10 text-base #{params[:status] == 'success' ? 'tab-active tab-bordered' : 'pb-[3px]'}" %> + <%= link_to t('failed'), url_for(params: request.query_parameters.merge('status' => 'error')), style: 'margin-bottom: -1px', class: "tab h-10 text-base #{params[:status] == 'error' ? 'tab-active tab-bordered' : 'pb-[3px]'}" %>
<% if @webhook_events.present? %>
From 871ef6dda65a0b8678f2092e516d85ae58d23029 Mon Sep 17 00:00:00 2001 From: Pete Matsyburka Date: Sat, 14 Feb 2026 18:10:16 +0200 Subject: [PATCH 12/54] add fetch options --- app/javascript/submission_form/completed.vue | 10 +++++++++- app/javascript/submission_form/form.vue | 10 +++++++++- app/javascript/submission_form/invite_form.vue | 8 +++++++- lib/submitters/authorized_for_form.rb | 12 ++++++------ 4 files changed, 31 insertions(+), 9 deletions(-) diff --git a/app/javascript/submission_form/completed.vue b/app/javascript/submission_form/completed.vue index 708843bfe..c87f9c4f8 100644 --- a/app/javascript/submission_form/completed.vue +++ b/app/javascript/submission_form/completed.vue @@ -161,6 +161,11 @@ export default { required: false, default: false }, + fetchOptions: { + type: Object, + required: false, + default: () => ({}) + }, completedButton: { type: Object, required: false, @@ -214,7 +219,10 @@ export default { download () { this.isDownloading = true - fetch(this.baseUrl + `/submitters/${this.submitterSlug}/download`).then(async (response) => { + fetch(this.baseUrl + `/submitters/${this.submitterSlug}/download`, { + method: 'GET', + ...this.fetchOptions + }).then(async (response) => { if (response.ok) { const urls = await response.json() const isMobileSafariIos = 'ontouchstart' in window && navigator.maxTouchPoints > 0 && /AppleWebKit/i.test(navigator.userAgent) diff --git a/app/javascript/submission_form/form.vue b/app/javascript/submission_form/form.vue index e3a3faddf..e2bdcf428 100644 --- a/app/javascript/submission_form/form.vue +++ b/app/javascript/submission_form/form.vue @@ -530,6 +530,7 @@ v-else-if="isInvite" :submitters="inviteSubmitters" :optional-submitters="optionalInviteSubmitters" + :fetch-options="fetchOptions" :submitter-slug="submitterSlug" :authenticity-token="authenticityToken" :url="baseUrl + submitPath + '/invite'" @@ -543,6 +544,7 @@ :has-signature-fields="stepFields.some((fields) => fields.some((f) => ['signature', 'initials'].includes(f.type)))" :has-multiple-documents="hasMultipleDocuments" :completed-button="completedRedirectUrl ? {} : completedButton" + :fetch-options="fetchOptions" :completed-message="completedRedirectUrl ? {} : completedMessage" :with-send-copy-button="withSendCopyButton && !completedRedirectUrl" :with-download-button="withDownloadButton && !completedRedirectUrl && !dryRun" @@ -678,6 +680,11 @@ export default { required: false, default: () => [] }, + fetchOptions: { + type: Object, + required: false, + default: () => ({}) + }, optionalInviteSubmitters: { type: Array, required: false, @@ -1467,7 +1474,8 @@ export default { } else { return fetch(this.baseUrl + this.submitPath, { method: 'POST', - body: formData || new FormData(this.$refs.form) + body: formData || new FormData(this.$refs.form), + ...this.fetchOptions }).then((response) => { if (response.status === 200) { currentFieldUuids.forEach((fieldUuid) => { diff --git a/app/javascript/submission_form/invite_form.vue b/app/javascript/submission_form/invite_form.vue index 10c3927dc..3189b0d8d 100644 --- a/app/javascript/submission_form/invite_form.vue +++ b/app/javascript/submission_form/invite_form.vue @@ -78,6 +78,11 @@ export default { type: Array, required: true }, + fetchOptions: { + type: Object, + required: false, + default: () => ({}) + }, optionalSubmitters: { type: Array, required: false, @@ -108,7 +113,8 @@ export default { return fetch(this.url, { method: 'POST', - body: new FormData(this.$refs.form) + body: new FormData(this.$refs.form), + ...this.fetchOptions }).then((response) => { if (response.status === 200) { this.$emit('success') diff --git a/lib/submitters/authorized_for_form.rb b/lib/submitters/authorized_for_form.rb index 27fa411cc..81048a162 100644 --- a/lib/submitters/authorized_for_form.rb +++ b/lib/submitters/authorized_for_form.rb @@ -17,9 +17,10 @@ def pass_email_2fa?(submitter, request) submitter.preferences['require_email_2fa'] != true return true if request.cookie_jar.encrypted[:email_2fa_slug] == submitter.slug - return true if request.params[:two_factor_token].present? && - Submitter.signed_id_verifier.verified(request.params[:two_factor_token], - purpose: :email_two_factor) == submitter.slug + token = request.params[:two_factor_token].presence || request.headers['x-two-factor-token'].presence + + return true if token.present? && + Submitter.signed_id_verifier.verified(token, purpose: :email_two_factor) == submitter.slug false end @@ -32,11 +33,10 @@ def pass_link_2fa?(submitter, current_user, request) return true if request.cookie_jar.encrypted[:email_2fa_slug] == submitter.slug return true if submitter.email == current_user&.email && current_user&.account_id == submitter.account_id - if request.params[:two_factor_token].present? + if (token = request.params[:two_factor_token].presence || request.headers['x-two-factor-token'].presence) link_2fa_key = [submitter.email.downcase.squish, submitter.submission.template.slug].join(':') - return true if Submitter.signed_id_verifier.verified(request.params[:two_factor_token], - purpose: :email_two_factor) == link_2fa_key + return true if Submitter.signed_id_verifier.verified(token, purpose: :email_two_factor) == link_2fa_key end false From 12c5b909e04b5de9ddcb55a2f1a3d05e9dfb1bff Mon Sep 17 00:00:00 2001 From: Pete Matsyburka Date: Sat, 14 Feb 2026 19:13:05 +0200 Subject: [PATCH 13/54] use cancan --- app/controllers/submissions_download_controller.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/controllers/submissions_download_controller.rb b/app/controllers/submissions_download_controller.rb index 8e76039b5..eb216bc5a 100644 --- a/app/controllers/submissions_download_controller.rb +++ b/app/controllers/submissions_download_controller.rb @@ -57,7 +57,7 @@ def respond_with_combined(submitter) end def current_user_submitter?(submitter) - current_user && current_user.account.submitters.exists?(id: submitter.id) + current_user && current_ability.can?(:read, submitter) end def build_urls(submitter) From 61c5ee22a016dd10d3776fdd5a0f4ba6c2b34612 Mon Sep 17 00:00:00 2001 From: Pete Matsyburka Date: Sun, 15 Feb 2026 09:11:40 +0200 Subject: [PATCH 14/54] hide form credentials --- app/views/email_smtp_settings/index.html.erb | 2 +- app/views/storage_settings/_aws_form.html.erb | 2 +- app/views/storage_settings/_azure_form.html.erb | 2 +- app/views/storage_settings/_google_cloud_form.html.erb | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/app/views/email_smtp_settings/index.html.erb b/app/views/email_smtp_settings/index.html.erb index 7e556a472..821d83359 100644 --- a/app/views/email_smtp_settings/index.html.erb +++ b/app/views/email_smtp_settings/index.html.erb @@ -22,7 +22,7 @@
<%= ff.label :password, 'Password', class: 'label' %> - <%= ff.password_field :password, value: value['password'], class: 'base-input' %> + <%= ff.password_field :password, class: 'base-input', required: value['password'].present?, placeholder: value['password'].present? ? '*************' : '' %>
diff --git a/app/views/storage_settings/_aws_form.html.erb b/app/views/storage_settings/_aws_form.html.erb index b08fd4680..e90077dda 100644 --- a/app/views/storage_settings/_aws_form.html.erb +++ b/app/views/storage_settings/_aws_form.html.erb @@ -8,7 +8,7 @@
<%= fff.label :secret_access_key, class: 'label' %> - <%= fff.password_field :secret_access_key, value: configs['secret_access_key'], required: true, class: 'base-input' %> + <%= fff.password_field :secret_access_key, required: true, class: 'base-input', placeholder: configs['secret_access_key'].present? ? '*************' : '' %>
diff --git a/app/views/storage_settings/_azure_form.html.erb b/app/views/storage_settings/_azure_form.html.erb index 22901b7cc..00a95be9f 100644 --- a/app/views/storage_settings/_azure_form.html.erb +++ b/app/views/storage_settings/_azure_form.html.erb @@ -13,7 +13,7 @@
<%= fff.label :storage_access_key, 'Storage Access Key', class: 'label' %> - <%= fff.password_field :storage_access_key, value: configs['storage_access_key'], required: true, class: 'base-input' %> + <%= fff.password_field :storage_access_key, required: true, class: 'base-input', placeholder: configs['storage_access_key'].present? ? '*************' : '' %>
<% end %> <% end %> diff --git a/app/views/storage_settings/_google_cloud_form.html.erb b/app/views/storage_settings/_google_cloud_form.html.erb index 11ce03998..3b8531f61 100644 --- a/app/views/storage_settings/_google_cloud_form.html.erb +++ b/app/views/storage_settings/_google_cloud_form.html.erb @@ -13,7 +13,7 @@
<%= fff.label :credentials, 'Credentials (JSON key content)', class: 'label' %> - <%= fff.text_area :credentials, value: configs['credentials'], required: true, class: 'base-textarea w-full font-mono', rows: 4 %> + <%= fff.text_area :credentials, required: true, class: 'base-textarea w-full font-mono', rows: 4, placeholder: configs['credentials'].present? ? "{\n**REDACTED**\n}" : '' %>
<% end %> <% end %> From bdd33c7d6bcd5bc11ff2e3e9492e81838e505905 Mon Sep 17 00:00:00 2001 From: Pete Matsyburka Date: Sun, 15 Feb 2026 09:33:23 +0200 Subject: [PATCH 15/54] fix spec --- spec/jobs/send_form_completed_webhook_request_job_spec.rb | 4 ++++ spec/jobs/send_form_declined_webhook_request_job_spec.rb | 4 ++++ spec/jobs/send_form_started_webhook_request_job_spec.rb | 4 ++++ spec/jobs/send_form_viewed_webhook_request_job_spec.rb | 4 ++++ .../send_submission_completed_webhook_request_job_spec.rb | 4 ++++ spec/jobs/send_submission_created_webhook_request_job_spec.rb | 4 ++++ spec/jobs/send_submission_expired_webhook_request_job_spec.rb | 4 ++++ spec/jobs/send_template_created_webhook_request_job_spec.rb | 4 ++++ spec/jobs/send_template_updated_webhook_request_job_spec.rb | 4 ++++ spec/rails_helper.rb | 1 + spec/system/email_settings_spec.rb | 1 - 11 files changed, 37 insertions(+), 1 deletion(-) diff --git a/spec/jobs/send_form_completed_webhook_request_job_spec.rb b/spec/jobs/send_form_completed_webhook_request_job_spec.rb index b60a3d9b9..6eb7cdf8d 100644 --- a/spec/jobs/send_form_completed_webhook_request_job_spec.rb +++ b/spec/jobs/send_form_completed_webhook_request_job_spec.rb @@ -16,6 +16,10 @@ end describe '#perform' do + around do |example| + freeze_time { example.run } + end + before do stub_request(:post, webhook_url.url).to_return(status: 200) end diff --git a/spec/jobs/send_form_declined_webhook_request_job_spec.rb b/spec/jobs/send_form_declined_webhook_request_job_spec.rb index 8e9d2d0d5..99f26eef0 100644 --- a/spec/jobs/send_form_declined_webhook_request_job_spec.rb +++ b/spec/jobs/send_form_declined_webhook_request_job_spec.rb @@ -16,6 +16,10 @@ end describe '#perform' do + around do |example| + freeze_time { example.run } + end + before do stub_request(:post, webhook_url.url).to_return(status: 200) end diff --git a/spec/jobs/send_form_started_webhook_request_job_spec.rb b/spec/jobs/send_form_started_webhook_request_job_spec.rb index e09f42052..54a5c521f 100644 --- a/spec/jobs/send_form_started_webhook_request_job_spec.rb +++ b/spec/jobs/send_form_started_webhook_request_job_spec.rb @@ -16,6 +16,10 @@ end describe '#perform' do + around do |example| + freeze_time { example.run } + end + before do stub_request(:post, webhook_url.url).to_return(status: 200) end diff --git a/spec/jobs/send_form_viewed_webhook_request_job_spec.rb b/spec/jobs/send_form_viewed_webhook_request_job_spec.rb index 310263410..5cbed3c30 100644 --- a/spec/jobs/send_form_viewed_webhook_request_job_spec.rb +++ b/spec/jobs/send_form_viewed_webhook_request_job_spec.rb @@ -16,6 +16,10 @@ end describe '#perform' do + around do |example| + freeze_time { example.run } + end + before do stub_request(:post, webhook_url.url).to_return(status: 200) end diff --git a/spec/jobs/send_submission_completed_webhook_request_job_spec.rb b/spec/jobs/send_submission_completed_webhook_request_job_spec.rb index 97ec0b69d..72764b04f 100644 --- a/spec/jobs/send_submission_completed_webhook_request_job_spec.rb +++ b/spec/jobs/send_submission_completed_webhook_request_job_spec.rb @@ -13,6 +13,10 @@ end describe '#perform' do + around do |example| + freeze_time { example.run } + end + before do stub_request(:post, webhook_url.url).to_return(status: 200) end diff --git a/spec/jobs/send_submission_created_webhook_request_job_spec.rb b/spec/jobs/send_submission_created_webhook_request_job_spec.rb index e80b97a6f..62a1d4328 100644 --- a/spec/jobs/send_submission_created_webhook_request_job_spec.rb +++ b/spec/jobs/send_submission_created_webhook_request_job_spec.rb @@ -13,6 +13,10 @@ end describe '#perform' do + around do |example| + freeze_time { example.run } + end + before do stub_request(:post, webhook_url.url).to_return(status: 200) end diff --git a/spec/jobs/send_submission_expired_webhook_request_job_spec.rb b/spec/jobs/send_submission_expired_webhook_request_job_spec.rb index dbce55ba7..541eb73ba 100644 --- a/spec/jobs/send_submission_expired_webhook_request_job_spec.rb +++ b/spec/jobs/send_submission_expired_webhook_request_job_spec.rb @@ -13,6 +13,10 @@ end describe '#perform' do + around do |example| + freeze_time { example.run } + end + before do stub_request(:post, webhook_url.url).to_return(status: 200) end diff --git a/spec/jobs/send_template_created_webhook_request_job_spec.rb b/spec/jobs/send_template_created_webhook_request_job_spec.rb index e5ce6f103..696d47d43 100644 --- a/spec/jobs/send_template_created_webhook_request_job_spec.rb +++ b/spec/jobs/send_template_created_webhook_request_job_spec.rb @@ -12,6 +12,10 @@ end describe '#perform' do + around do |example| + freeze_time { example.run } + end + before do stub_request(:post, webhook_url.url).to_return(status: 200) end diff --git a/spec/jobs/send_template_updated_webhook_request_job_spec.rb b/spec/jobs/send_template_updated_webhook_request_job_spec.rb index f13675a10..c0ecec8bb 100644 --- a/spec/jobs/send_template_updated_webhook_request_job_spec.rb +++ b/spec/jobs/send_template_updated_webhook_request_job_spec.rb @@ -12,6 +12,10 @@ end describe '#perform' do + around do |example| + freeze_time { example.run } + end + before do stub_request(:post, webhook_url.url).to_return(status: 200) end diff --git a/spec/rails_helper.rb b/spec/rails_helper.rb index db91da56e..ecca6019a 100644 --- a/spec/rails_helper.rb +++ b/spec/rails_helper.rb @@ -54,6 +54,7 @@ config.include FactoryBot::Syntax::Methods config.include Devise::Test::IntegrationHelpers config.include SigningFormHelper + config.include ActiveSupport::Testing::TimeHelpers config.before(:each, type: :system) do if ENV['HEADLESS'] == 'false' diff --git a/spec/system/email_settings_spec.rb b/spec/system/email_settings_spec.rb index f7d683b88..fe842a1d5 100644 --- a/spec/system/email_settings_spec.rb +++ b/spec/system/email_settings_spec.rb @@ -61,7 +61,6 @@ expect(page).to have_field('Host', with: encrypted_config.value['host']) expect(page).to have_field('Port', with: encrypted_config.value['port']) expect(page).to have_field('Username', with: encrypted_config.value['username']) - expect(page).to have_field('Password', with: encrypted_config.value['password']) expect(page).to have_field('Domain', with: encrypted_config.value['domain']) expect(page).to have_select('Authentication', selected: 'Plain') expect(page).to have_field('Send from Email', with: encrypted_config.value['from_email']) From a999109a5ccb962d91910755d84b3a14b8dc7300 Mon Sep 17 00:00:00 2001 From: Pete Matsyburka Date: Sun, 15 Feb 2026 12:18:53 +0200 Subject: [PATCH 16/54] add port check --- app/jobs/send_test_webhook_request_job.rb | 2 +- lib/download_utils.rb | 2 +- lib/send_webhook_request.rb | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/app/jobs/send_test_webhook_request_job.rb b/app/jobs/send_test_webhook_request_job.rb index 158363ecf..fe5cabcee 100644 --- a/app/jobs/send_test_webhook_request_job.rb +++ b/app/jobs/send_test_webhook_request_job.rb @@ -26,7 +26,7 @@ def perform(params = {}) Addressable::URI.parse(webhook_url.url).normalize end - raise HttpsError, 'Only HTTPS is allowed.' if uri.scheme != 'https' + raise HttpsError, 'Only HTTPS is allowed.' if uri.scheme != 'https' || uri.port != 443 raise LocalhostError, "Can't send to localhost." if uri.host.in?(SendWebhookRequest::LOCALHOSTS) end diff --git a/lib/download_utils.rb b/lib/download_utils.rb index 668c6d967..3758239cd 100644 --- a/lib/download_utils.rb +++ b/lib/download_utils.rb @@ -52,7 +52,7 @@ def call(url, validate: Docuseal.multitenant?) end def validate_uri!(uri) - raise UnableToDownload, "Error loading: #{uri}. Only HTTPS is allowed." if uri.scheme != 'https' + raise UnableToDownload, "Error loading: #{uri}. Only HTTPS is allowed." if uri.scheme != 'https' || uri.port != 443 raise UnableToDownload, "Error loading: #{uri}. Can't download from localhost." if uri.host.in?(LOCALHOSTS) end diff --git a/lib/send_webhook_request.rb b/lib/send_webhook_request.rb index a3474eaf9..d41c8cbc0 100644 --- a/lib/send_webhook_request.rb +++ b/lib/send_webhook_request.rb @@ -22,7 +22,7 @@ def call(webhook_url, event_uuid:, event_type:, record:, data:, attempt: 0) end if Docuseal.multitenant? - raise HttpsError, 'Only HTTPS is allowed.' if uri.scheme != 'https' && + raise HttpsError, 'Only HTTPS is allowed.' if (uri.scheme != 'https' || uri.port != 443) && !AccountConfig.exists?(key: :allow_http, account_id: webhook_url.account_id) raise LocalhostError, "Can't send to localhost." if uri.host.in?(LOCALHOSTS) From 75316d8d873ab72a3c46ef8b2783661c8c8ab4e1 Mon Sep 17 00:00:00 2001 From: Pete Matsyburka Date: Sun, 15 Feb 2026 22:02:00 +0200 Subject: [PATCH 17/54] fix draw custom field --- app/javascript/template_builder/page.vue | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/javascript/template_builder/page.vue b/app/javascript/template_builder/page.vue index 550658c4d..8580198cb 100644 --- a/app/javascript/template_builder/page.vue +++ b/app/javascript/template_builder/page.vue @@ -261,7 +261,7 @@ export default { }, computed: { isSelectMode () { - return this.isSelectModeRef.value && !this.drawFieldType && this.editable && !this.drawField + return this.isSelectModeRef.value && !this.drawFieldType && this.editable && !this.drawField && !this.drawCustomField }, pageSelectedAreas () { if (!this.selectedAreasRef.value) return [] From aa77e3a8d372f1617f4f1cb7b4103f190cd850ec Mon Sep 17 00:00:00 2001 From: Pete Matsyburka Date: Mon, 16 Feb 2026 08:08:41 +0200 Subject: [PATCH 18/54] adjust port check --- app/jobs/send_test_webhook_request_job.rb | 2 +- lib/download_utils.rb | 3 ++- lib/send_webhook_request.rb | 2 +- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/app/jobs/send_test_webhook_request_job.rb b/app/jobs/send_test_webhook_request_job.rb index fe5cabcee..0116cdafb 100644 --- a/app/jobs/send_test_webhook_request_job.rb +++ b/app/jobs/send_test_webhook_request_job.rb @@ -26,7 +26,7 @@ def perform(params = {}) Addressable::URI.parse(webhook_url.url).normalize end - raise HttpsError, 'Only HTTPS is allowed.' if uri.scheme != 'https' || uri.port != 443 + raise HttpsError, 'Only HTTPS is allowed.' if uri.scheme != 'https' || [443, nil].exclude?(uri.port) raise LocalhostError, "Can't send to localhost." if uri.host.in?(SendWebhookRequest::LOCALHOSTS) end diff --git a/lib/download_utils.rb b/lib/download_utils.rb index 3758239cd..8b352502b 100644 --- a/lib/download_utils.rb +++ b/lib/download_utils.rb @@ -52,7 +52,8 @@ def call(url, validate: Docuseal.multitenant?) end def validate_uri!(uri) - raise UnableToDownload, "Error loading: #{uri}. Only HTTPS is allowed." if uri.scheme != 'https' || uri.port != 443 + raise UnableToDownload, "Error loading: #{uri}. Only HTTPS is allowed." if uri.scheme != 'https' || + [443, nil].exclude?(uri.port) raise UnableToDownload, "Error loading: #{uri}. Can't download from localhost." if uri.host.in?(LOCALHOSTS) end diff --git a/lib/send_webhook_request.rb b/lib/send_webhook_request.rb index d41c8cbc0..87de376a4 100644 --- a/lib/send_webhook_request.rb +++ b/lib/send_webhook_request.rb @@ -22,7 +22,7 @@ def call(webhook_url, event_uuid:, event_type:, record:, data:, attempt: 0) end if Docuseal.multitenant? - raise HttpsError, 'Only HTTPS is allowed.' if (uri.scheme != 'https' || uri.port != 443) && + raise HttpsError, 'Only HTTPS is allowed.' if (uri.scheme != 'https' || [443, nil].exclude?(uri.port)) && !AccountConfig.exists?(key: :allow_http, account_id: webhook_url.account_id) raise LocalhostError, "Can't send to localhost." if uri.host.in?(LOCALHOSTS) From 3b396f84219b6104dced12484484161923abba60 Mon Sep 17 00:00:00 2001 From: Pete Matsyburka Date: Mon, 16 Feb 2026 10:00:51 +0200 Subject: [PATCH 19/54] fix redirect --- app/controllers/submit_form_controller.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/controllers/submit_form_controller.rb b/app/controllers/submit_form_controller.rb index a9e2f88cd..5a6a0ae52 100644 --- a/app/controllers/submit_form_controller.rb +++ b/app/controllers/submit_form_controller.rb @@ -16,8 +16,8 @@ class SubmitFormController < ApplicationController def show submission = @submitter.submission - return redirect_to submit_form_completed_path(@submitter.slug) if @submitter.completed_at? return render :email_2fa unless Submitters::AuthorizedForForm.pass_email_2fa?(@submitter, request) + return redirect_to submit_form_completed_path(@submitter.slug) if @submitter.completed_at? @form_configs = Submitters::FormConfigs.call(@submitter, CONFIG_KEYS) From 2c736a0eedb0bc59590459a6f62be17c39e81cbb Mon Sep 17 00:00:00 2001 From: Pete Matsyburka Date: Tue, 17 Feb 2026 08:42:21 +0200 Subject: [PATCH 20/54] validate readonly field --- lib/submitters/submit_values.rb | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/lib/submitters/submit_values.rb b/lib/submitters/submit_values.rb index dc370791a..4ecb2b928 100644 --- a/lib/submitters/submit_values.rb +++ b/lib/submitters/submit_values.rb @@ -403,7 +403,13 @@ def replace_default_variables(value, attrs, submission, with_time: false) end end - def validate_value!(_value, _field, _params, _submitter, _request) + def validate_value!(_value, field, _params, submitter, _request) + if field['readonly'] == true + Rollbar.warning("Readonly field #{submitter.id}: #{field['uuid']}") if defined?(Rollbar) + + raise ValidationError, 'Read-only field' + end + true end end From 43fbc427708671b0e43ae5357391614e47f67c58 Mon Sep 17 00:00:00 2001 From: Pete Matsyburka Date: Tue, 17 Feb 2026 10:04:11 +0200 Subject: [PATCH 21/54] fix reason field --- lib/submitters/submit_values.rb | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/lib/submitters/submit_values.rb b/lib/submitters/submit_values.rb index 4ecb2b928..4b7f4dcb8 100644 --- a/lib/submitters/submit_values.rb +++ b/lib/submitters/submit_values.rb @@ -45,8 +45,8 @@ def update_submitter!(submitter, params, request, validate_required: true) assign_completed_attributes(submitter, request, validate_required:) if params[:completed] == 'true' ApplicationRecord.transaction do - maybe_set_signature_reason!(values, submitter, params) - validate_values!(values, submitter, params, request) + reason_field = maybe_set_signature_reason!(values, submitter, params) + validate_values!(reason_field ? values.except(reason_field['uuid']) : values, submitter, params, request) if (touch_attachment_uuid = params[:touch_attachment_uuid].presence) ActiveStorage::Attachment.where(uuid: touch_attachment_uuid, record: submitter).touch_all(:created_at) @@ -119,6 +119,8 @@ def maybe_set_signature_reason!(values, submitter, params) end submitter.submission.save! + + reason_field end def normalized_values(params) From 848f01edf81f0926be877bc996c8e8af47a74fbf Mon Sep 17 00:00:00 2001 From: Pete Matsyburka Date: Tue, 17 Feb 2026 10:19:29 +0200 Subject: [PATCH 22/54] add spec --- spec/system/signing_form_spec.rb | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/spec/system/signing_form_spec.rb b/spec/system/signing_form_spec.rb index 073ef79fc..7af76c2f5 100644 --- a/spec/system/signing_form_spec.rb +++ b/spec/system/signing_form_spec.rb @@ -626,6 +626,34 @@ end end + context 'when the signature step with signing reason' do + let(:template) { create(:template, account:, author:, only_field_types: %w[signature]) } + let(:submission) { create(:submission, template:) } + let(:submitter) do + create(:submitter, submission:, uuid: template.submitters.first['uuid'], account:) + end + + before do + create(:account_config, account:, key: AccountConfig::REQUIRE_SIGNING_REASON_KEY, value: true) + end + + it 'completes the form with signing reason selected' do + visit submit_form_path(slug: submitter.slug) + + find('#expand_form_button').click + draw_canvas + select 'Approved' + click_button 'Sign and Complete' + + expect(page).to have_content('Document has been signed!') + + submitter.reload + + expect(submitter.completed_at).to be_present + expect(field_value(submitter, 'Signature')).to be_present + end + end + context 'when the number step' do let(:template) { create(:template, account:, author:, only_field_types: %w[number]) } let(:submission) { create(:submission, template:) } From e48652f425616e1809792bfd7bce2225af987f27 Mon Sep 17 00:00:00 2001 From: Pete Matsyburka Date: Tue, 17 Feb 2026 10:29:00 +0200 Subject: [PATCH 23/54] fix rubocop --- .rubocop.yml | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/.rubocop.yml b/.rubocop.yml index bda7960e8..019c504b5 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -106,3 +106,10 @@ Rails/StrongParametersExpect: Rails/RedirectBackOrTo: Enabled: false + +Rails/UnknownEnv: + Environments: + - development + - test + - production + - local From 118f4a231be1799f124e3e4ec24fc225d3f06f85 Mon Sep 17 00:00:00 2001 From: Pete Matsyburka Date: Tue, 17 Feb 2026 17:45:46 +0200 Subject: [PATCH 24/54] detailed time format --- app/models/account_config.rb | 1 + app/views/submissions/_value.html.erb | 3 ++- app/views/submissions/show.html.erb | 5 ++-- app/views/submit_form/show.html.erb | 2 +- config/locales/i18n.yml | 27 +++++++++++++++++++ lib/submissions/generate_audit_trail.rb | 6 ++++- .../generate_preview_attachments.rb | 4 ++- .../generate_result_attachments.rb | 12 ++++++--- lib/submitters/form_configs.rb | 4 ++- 9 files changed, 54 insertions(+), 10 deletions(-) diff --git a/app/models/account_config.rb b/app/models/account_config.rb index 276194daf..083aea074 100644 --- a/app/models/account_config.rb +++ b/app/models/account_config.rb @@ -49,6 +49,7 @@ class AccountConfig < ApplicationRecord WITH_AUDIT_VALUES_KEY = 'with_audit_values' WITH_AUDIT_SENDER_KEY = 'with_audit_sender' WITH_SUBMITTER_TIMEZONE_KEY = 'with_submitter_timezone' + WITH_TIMESTAMP_SECONDS_KEY = 'with_timestamp_seconds' REQUIRE_SIGNING_REASON_KEY = 'require_signing_reason' REUSE_SIGNATURE_KEY = 'reuse_signature' WITH_FIELD_LABELS_KEY = 'with_field_labels' diff --git a/app/views/submissions/_value.html.erb b/app/views/submissions/_value.html.erb index a0b3205db..5654080c0 100644 --- a/app/views/submissions/_value.html.erb +++ b/app/views/submissions/_value.html.erb @@ -28,7 +28,8 @@ <% end %>
<% timezone = local_assigns[:with_submitter_timezone] ? (submitter.timezone || local_assigns[:timezone]) : local_assigns[:timezone] %> - <%= l(attachment.created_at.in_time_zone(timezone), format: :long, locale: local_assigns[:locale]) %> <%= TimeUtils.timezone_abbr(timezone, attachment.created_at) %> + <% time_format = local_assigns[:with_timestamp_seconds] ? :detailed : :long %> + <%= l(attachment.created_at.in_time_zone(timezone), format: time_format, locale: local_assigns[:locale]) %> <%= TimeUtils.timezone_abbr(timezone, attachment.created_at) %>
<% end %> diff --git a/app/views/submissions/show.html.erb b/app/views/submissions/show.html.erb index 5d19f351f..7940f3377 100644 --- a/app/views/submissions/show.html.erb +++ b/app/views/submissions/show.html.erb @@ -2,10 +2,11 @@ <%= render 'submissions/preview_tags' %> <% end %> <% font_scale = 1040.0 / PdfUtils::US_LETTER_W %> -<% configs = AccountConfig.where(account_id: @submission.account_id, key: [AccountConfig::COMBINE_PDF_RESULT_KEY, AccountConfig::WITH_SIGNATURE_ID, AccountConfig::WITH_SUBMITTER_TIMEZONE_KEY, AccountConfig::WITH_SIGNATURE_ID_REASON_KEY]) %> +<% configs = AccountConfig.where(account_id: @submission.account_id, key: [AccountConfig::COMBINE_PDF_RESULT_KEY, AccountConfig::WITH_SIGNATURE_ID, AccountConfig::WITH_SUBMITTER_TIMEZONE_KEY, AccountConfig::WITH_SIGNATURE_ID_REASON_KEY, AccountConfig::WITH_TIMESTAMP_SECONDS_KEY]) %> <% with_signature_id = configs.find { |e| e.key == AccountConfig::WITH_SIGNATURE_ID }&.value == true %> <% is_combined_enabled = configs.find { |e| e.key == AccountConfig::COMBINE_PDF_RESULT_KEY }&.value == true && !@submission.template_fields&.any? { |f| f['type'] == 'verification' } %> <% with_submitter_timezone = configs.find { |e| e.key == AccountConfig::WITH_SUBMITTER_TIMEZONE_KEY }&.value == true %> +<% with_timestamp_seconds = configs.find { |e| e.key == AccountConfig::WITH_TIMESTAMP_SECONDS_KEY }&.value == true %> <% with_signature_id_reason = configs.find { |e| e.key == AccountConfig::WITH_SIGNATURE_ID_REASON_KEY }&.value != false %>
@@ -125,7 +126,7 @@ <% else %> - <%= render 'submissions/value', page_width: width, page_height: height, font_scale:, area:, field:, attachments_index:, value: mask.present? ? Array.wrap(value).map { |e| TextUtils.mask_value(e, mask) }.join(', ') : value, locale: @submission.account.locale, timezone: @submission.account.timezone, submitter:, with_signature_id:, with_submitter_timezone:, with_signature_id_reason: %> + <%= render 'submissions/value', page_width: width, page_height: height, font_scale:, area:, field:, attachments_index:, value: mask.present? ? Array.wrap(value).map { |e| TextUtils.mask_value(e, mask) }.join(', ') : value, locale: @submission.account.locale, timezone: @submission.account.timezone, submitter:, with_signature_id:, with_submitter_timezone:, with_timestamp_seconds:, with_signature_id_reason: %> <% end %> <% elsif field['readonly'] != true && submitter && !submitter.completed_at? %> <% submitters_order_index ||= (@submission.template_submitters || @submission.template.submitters).each_with_index.to_h { |s, i| [s['uuid'], i] } %> diff --git a/app/views/submit_form/show.html.erb b/app/views/submit_form/show.html.erb index 9e73f00d9..f2bfedd31 100644 --- a/app/views/submit_form/show.html.erb +++ b/app/views/submit_form/show.html.erb @@ -80,7 +80,7 @@ <% next if field['conditions'].present? && values[field['uuid']].blank? && field['submitter_uuid'] != @submitter.uuid %> <% next if field['conditions'].present? && field['submitter_uuid'] == @submitter.uuid %> <% next if field.dig('preferences', 'formula').present? && field['submitter_uuid'] == @submitter.uuid %> - <%= render 'submissions/value', page_width: width, page_height: height, font_scale:, area:, field:, attachments_index: @attachments_index, value: field.dig('preferences', 'mask').present? ? TextUtils.mask_value(value, field.dig('preferences', 'mask')) : value, locale: @submitter.account.locale, timezone: @submitter.account.timezone, submitter: submitters_index[field['submitter_uuid']], with_signature_id: @form_configs[:with_signature_id], with_submitter_timezone: @form_configs[:with_submitter_timezone], with_signature_id_reason: @form_configs[:with_signature_id_reason] %> + <%= render 'submissions/value', page_width: width, page_height: height, font_scale:, area:, field:, attachments_index: @attachments_index, value: field.dig('preferences', 'mask').present? ? TextUtils.mask_value(value, field.dig('preferences', 'mask')) : value, locale: @submitter.account.locale, timezone: @submitter.account.timezone, submitter: submitters_index[field['submitter_uuid']], with_signature_id: @form_configs[:with_signature_id], with_submitter_timezone: @form_configs[:with_submitter_timezone], with_timestamp_seconds: @form_configs[:with_timestamp_seconds], with_signature_id_reason: @form_configs[:with_signature_id_reason] %> <% end %>
diff --git a/config/locales/i18n.yml b/config/locales/i18n.yml index c95a793a0..7fc423863 100644 --- a/config/locales/i18n.yml +++ b/config/locales/i18n.yml @@ -1002,6 +1002,9 @@ en: &en events: range_with_total: "%{from}-%{to} of %{count} events" range_without_total: "%{from}-%{to} events" + time: + formats: + detailed: "%B %d, %Y %H:%M:%S" es: &es knowledge_based_authentication: Autenticación basada en el conocimiento @@ -1986,6 +1989,9 @@ es: &es events: range_with_total: "%{from}-%{to} de %{count} eventos" range_without_total: "%{from}-%{to} eventos" + time: + formats: + detailed: "%-d de %B de %Y %H:%M:%S" it: &it knowledge_based_authentication: Autenticazione basata sulla conoscenza @@ -2971,6 +2977,9 @@ it: &it events: range_with_total: "%{from}-%{to} di %{count} eventi" range_without_total: "%{from}-%{to} eventi" + time: + formats: + detailed: "%d %B %Y %H:%M:%S" fr: &fr knowledge_based_authentication: Authentification basée sur la connaissance @@ -3952,6 +3961,9 @@ fr: &fr events: range_with_total: "%{from}-%{to} sur %{count} événements" range_without_total: "%{from}-%{to} événements" + time: + formats: + detailed: "%A %d %B %Y %Hh%Mm%Ss" pt: &pt knowledge_based_authentication: Autenticação baseada em conhecimento @@ -4936,6 +4948,9 @@ pt: &pt events: range_with_total: "%{from}-%{to} de %{count} eventos" range_without_total: "%{from}-%{to} eventos" + time: + formats: + detailed: "%A, %d de %B de %Y, %H:%M:%Sh" de: &de knowledge_based_authentication: Wissensbasierte Authentifizierung @@ -5920,6 +5935,9 @@ de: &de events: range_with_total: "%{from}-%{to} von %{count} Ereignissen" range_without_total: "%{from}-%{to} Ereignisse" + time: + formats: + detailed: "%A, %d. %B %Y, %H:%M:%S Uhr" pl: require_phone_2fa_to_open: Wymagaj uwierzytelniania telefonicznego 2FA do otwarcia @@ -7289,6 +7307,9 @@ nl: &nl events: range_with_total: "%{from}-%{to} van %{count} gebeurtenissen" range_without_total: "%{from}-%{to} gebeurtenissen" + time: + formats: + detailed: "%d %B %Y %H:%M:%S" ar: require_phone_2fa_to_open: "تطلب فتح عبر تحقق الهاتف ذو العاملين" @@ -7586,12 +7607,18 @@ en-US: date: formats: default: "%m/%d/%Y" + time: + formats: + detailed: "%B %d, %Y %I:%M:%S %p" en-GB: <<: *en date: formats: default: "%d/%m/%Y" + time: + formats: + detailed: "%d %B, %Y %H:%M:%S" es-ES: <<: *es diff --git a/lib/submissions/generate_audit_trail.rb b/lib/submissions/generate_audit_trail.rb index 92fa783c7..ed80a86de 100644 --- a/lib/submissions/generate_audit_trail.rb +++ b/lib/submissions/generate_audit_trail.rb @@ -116,6 +116,7 @@ def build_audit_trail(submission) configs = submission.account.account_configs.where(key: [AccountConfig::WITH_AUDIT_VALUES_KEY, AccountConfig::WITH_SIGNATURE_ID, AccountConfig::WITH_FILE_LINKS_KEY, + AccountConfig::WITH_TIMESTAMP_SECONDS_KEY, AccountConfig::WITH_AUDIT_SENDER_KEY, AccountConfig::WITH_SUBMITTER_TIMEZONE_KEY]) @@ -126,6 +127,7 @@ def build_audit_trail(submission) with_audit_values = configs.find { |c| c.key == AccountConfig::WITH_AUDIT_VALUES_KEY }&.value != false with_audit_sender = configs.find { |c| c.key == AccountConfig::WITH_AUDIT_SENDER_KEY }&.value == true with_submitter_timezone = configs.find { |c| c.key == AccountConfig::WITH_SUBMITTER_TIMEZONE_KEY }&.value == true + with_timestamp_seconds = configs.find { |c| c.key == AccountConfig::WITH_TIMESTAMP_SECONDS_KEY }&.value == true timezone = account.timezone timezone = last_submitter.timezone || account.timezone if with_submitter_timezone @@ -489,8 +491,10 @@ def build_audit_trail(submission) end end + time_format = with_timestamp_seconds ? :detailed : :long + [ - "#{I18n.l(event.event_timestamp.in_time_zone(timezone), format: :long, locale: account.locale)} " \ + "#{I18n.l(event.event_timestamp.in_time_zone(timezone), format: time_format, locale: account.locale)} " \ "#{TimeUtils.timezone_abbr(timezone, event.event_timestamp)}", composer.document.layout.formatted_text_box(text_box) ] diff --git a/lib/submissions/generate_preview_attachments.rb b/lib/submissions/generate_preview_attachments.rb index ff5a85a8e..a5e226323 100644 --- a/lib/submissions/generate_preview_attachments.rb +++ b/lib/submissions/generate_preview_attachments.rb @@ -15,6 +15,7 @@ def call(submission, values_hash: nil, submitter: nil, merge: false) configs = submission.account.account_configs.where(key: [AccountConfig::FLATTEN_RESULT_PDF_KEY, AccountConfig::WITH_SIGNATURE_ID, AccountConfig::WITH_SUBMITTER_TIMEZONE_KEY, + AccountConfig::WITH_TIMESTAMP_SECONDS_KEY, AccountConfig::WITH_FILE_LINKS_KEY, AccountConfig::WITH_SIGNATURE_ID_REASON_KEY]) @@ -22,6 +23,7 @@ def call(submission, values_hash: nil, submitter: nil, merge: false) with_file_links = configs.find { |c| c.key == AccountConfig::WITH_FILE_LINKS_KEY }&.value == true is_flatten = configs.find { |c| c.key == AccountConfig::FLATTEN_RESULT_PDF_KEY }&.value != false with_submitter_timezone = configs.find { |c| c.key == AccountConfig::WITH_SUBMITTER_TIMEZONE_KEY }&.value == true + with_timestamp_seconds = configs.find { |c| c.key == AccountConfig::WITH_TIMESTAMP_SECONDS_KEY }&.value == true with_signature_id_reason = configs.find { |c| c.key == AccountConfig::WITH_SIGNATURE_ID_REASON_KEY }&.value != false @@ -37,7 +39,7 @@ def call(submission, values_hash: nil, submitter: nil, merge: false) GenerateResultAttachments.fill_submitter_fields(s, submission.account, pdfs_index, with_signature_id:, is_flatten:, with_headings: index.zero?, with_submitter_timezone:, with_file_links:, - with_signature_id_reason:) + with_signature_id_reason:, with_timestamp_seconds:) end template = submission.template diff --git a/lib/submissions/generate_result_attachments.rb b/lib/submissions/generate_result_attachments.rb index 580b9be15..1f0eb4eed 100644 --- a/lib/submissions/generate_result_attachments.rb +++ b/lib/submissions/generate_result_attachments.rb @@ -140,11 +140,13 @@ def generate_pdfs(submitter) configs = submitter.account.account_configs.where(key: [AccountConfig::FLATTEN_RESULT_PDF_KEY, AccountConfig::WITH_SIGNATURE_ID, AccountConfig::WITH_FILE_LINKS_KEY, + AccountConfig::WITH_TIMESTAMP_SECONDS_KEY, AccountConfig::WITH_SUBMITTER_TIMEZONE_KEY, AccountConfig::WITH_SIGNATURE_ID_REASON_KEY]) with_signature_id = configs.find { |c| c.key == AccountConfig::WITH_SIGNATURE_ID }&.value == true is_flatten = configs.find { |c| c.key == AccountConfig::FLATTEN_RESULT_PDF_KEY }&.value != false + with_timestamp_seconds = configs.find { |c| c.key == AccountConfig::WITH_TIMESTAMP_SECONDS_KEY }&.value == true with_submitter_timezone = configs.find { |c| c.key == AccountConfig::WITH_SUBMITTER_TIMEZONE_KEY }&.value == true with_file_links = configs.find { |c| c.key == AccountConfig::WITH_FILE_LINKS_KEY }&.value == true with_signature_id_reason = @@ -195,11 +197,13 @@ def generate_pdfs(submitter) fill_submitter_fields(submitter, submitter.account, pdfs_index, with_signature_id:, is_flatten:, with_submitter_timezone:, with_file_links:, + with_timestamp_seconds:, with_signature_id_reason:) end def fill_submitter_fields(submitter, account, pdfs_index, with_signature_id:, is_flatten:, with_headings: nil, - with_submitter_timezone: false, with_signature_id_reason: true, with_file_links: nil) + with_submitter_timezone: false, with_signature_id_reason: true, + with_timestamp_seconds: false, with_file_links: nil) cell_layouters = Hash.new do |hash, valign| hash[valign] = HexaPDF::Layout::TextLayouter.new(text_valign: valign.to_sym, text_align: :center) end @@ -320,13 +324,15 @@ def fill_submitter_fields(submitter, account, pdfs_index, with_signature_id:, is timezone = submitter.account.timezone timezone = submitter.timezone || submitter.account.timezone if with_submitter_timezone + time_format = with_timestamp_seconds ? :detailed : :long + if with_signature_id_reason || field.dig('preferences', 'reasons').present? "#{"#{I18n.t('reason')}: " if reason_value}#{reason_value || I18n.t('digitally_signed_by')} " \ "#{submitter.name}#{" <#{submitter.email}>" if submitter.email.present?}\n" \ - "#{I18n.l(attachment.created_at.in_time_zone(timezone), format: :long)} " \ + "#{I18n.l(attachment.created_at.in_time_zone(timezone), format: time_format)} " \ "#{TimeUtils.timezone_abbr(timezone, attachment.created_at)}" else - "#{I18n.l(attachment.created_at.in_time_zone(timezone), format: :long)} " \ + "#{I18n.l(attachment.created_at.in_time_zone(timezone), format: time_format)} " \ "#{TimeUtils.timezone_abbr(timezone, attachment.created_at)}" end end diff --git a/lib/submitters/form_configs.rb b/lib/submitters/form_configs.rb index 38d18993a..4db19b32d 100644 --- a/lib/submitters/form_configs.rb +++ b/lib/submitters/form_configs.rb @@ -15,6 +15,7 @@ module FormConfigs AccountConfig::ALLOW_TO_PARTIAL_DOWNLOAD_KEY, AccountConfig::ALLOW_TYPED_SIGNATURE, AccountConfig::WITH_SUBMITTER_TIMEZONE_KEY, + AccountConfig::WITH_TIMESTAMP_SECONDS_KEY, AccountConfig::WITH_SIGNATURE_ID_REASON_KEY, *(Docuseal.multitenant? ? [] : [AccountConfig::POLICY_LINKS_KEY])].freeze @@ -35,6 +36,7 @@ def call(submitter, keys = []) require_signing_reason = find_safe_value(configs, AccountConfig::REQUIRE_SIGNING_REASON_KEY) == true enforce_signing_order = find_safe_value(configs, AccountConfig::ENFORCE_SIGNING_ORDER_KEY) == true with_submitter_timezone = find_safe_value(configs, AccountConfig::WITH_SUBMITTER_TIMEZONE_KEY) == true + with_timestamp_seconds = find_safe_value(configs, AccountConfig::WITH_TIMESTAMP_SECONDS_KEY) == true with_signature_id_reason = find_safe_value(configs, AccountConfig::WITH_SIGNATURE_ID_REASON_KEY) != false with_field_labels = find_safe_value(configs, AccountConfig::WITH_FIELD_LABELS_KEY) != false policy_links = find_safe_value(configs, AccountConfig::POLICY_LINKS_KEY) @@ -43,7 +45,7 @@ def call(submitter, keys = []) reuse_signature:, with_decline:, with_partial_download:, policy_links:, enforce_signing_order:, completed_message:, require_signing_reason:, prefill_signature:, with_submitter_timezone:, - with_signature_id_reason:, with_signature_id:, with_field_labels: } + with_signature_id_reason:, with_signature_id:, with_field_labels:, with_timestamp_seconds: } keys.each do |key| attrs[key.to_sym] = configs.find { |e| e.key == key.to_s }&.value From 1b41af798da48d6af30031316667ce08c238922f Mon Sep 17 00:00:00 2001 From: Pete Matsyburka Date: Tue, 17 Feb 2026 19:42:47 +0200 Subject: [PATCH 25/54] update --- Gemfile.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Gemfile.lock b/Gemfile.lock index 962c2364a..5e359ad08 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -405,7 +405,7 @@ GEM puma (7.2.0) nio4r (~> 2.0) racc (1.8.1) - rack (3.2.4) + rack (3.2.5) rack-proxy (0.7.7) rack rack-session (2.1.1) From 1d2394e31e0e7d96f9eb35fea47d9bb30bbf39d8 Mon Sep 17 00:00:00 2001 From: Pete Matsyburka Date: Thu, 19 Feb 2026 14:08:27 +0200 Subject: [PATCH 26/54] size limit --- lib/templates/create_attachments.rb | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/lib/templates/create_attachments.rb b/lib/templates/create_attachments.rb index 10b08ac78..b6297a768 100644 --- a/lib/templates/create_attachments.rb +++ b/lib/templates/create_attachments.rb @@ -18,6 +18,7 @@ module CreateAttachments ].freeze ANNOTATIONS_SIZE_LIMIT = 6.megabytes + MAX_ZIP_SIZE = 100.megabytes InvalidFileType = Class.new(StandardError) PdfEncrypted = Class.new(StandardError) @@ -72,9 +73,15 @@ def extract_zip_files(files) Array.wrap(files).each do |file| if file.content_type == ZIP_CONTENT_TYPE || file.content_type == X_ZIP_CONTENT_TYPE + total_size = 0 + Zip::File.open(file.tempfile).each do |entry| next if entry.directory? + total_size += entry.size + + raise InvalidFileType, 'zip_too_large' if total_size > MAX_ZIP_SIZE + tempfile = Tempfile.new(entry.name) tempfile.binmode entry.get_input_stream { |in_stream| IO.copy_stream(in_stream, tempfile) } From 739e2abdf80b4591ce5dc504a23a54944a2db6c4 Mon Sep 17 00:00:00 2001 From: Pete Matsyburka Date: Thu, 19 Feb 2026 14:24:02 +0200 Subject: [PATCH 27/54] vips size limit --- Dockerfile | 1 + 1 file changed, 1 insertion(+) diff --git a/Dockerfile b/Dockerfile index f6412c784..b1341fcf3 100644 --- a/Dockerfile +++ b/Dockerfile @@ -48,6 +48,7 @@ ENV RAILS_ENV=production ENV BUNDLE_WITHOUT="development:test" ENV LD_PRELOAD=/lib/libgcompat.so.0 ENV OPENSSL_CONF=/etc/openssl_legacy.cnf +ENV VIPS_MAX_COORD=10000 WORKDIR /app From 825322d489d9f9f3e39b62ee96063f717741bf2c Mon Sep 17 00:00:00 2001 From: Pete Matsyburka Date: Thu, 19 Feb 2026 15:54:15 +0200 Subject: [PATCH 28/54] adjust for custom domain --- app/controllers/application_controller.rb | 4 --- .../submissions_download_controller.rb | 4 +-- .../submit_form_download_controller.rb | 2 +- .../template_documents_controller.rb | 2 +- app/mailers/submitter_mailer.rb | 17 +++++++----- app/views/submissions/show.html.erb | 2 +- .../documents_copy_email.html.erb | 2 +- .../invitation_email.html.erb | 4 +-- app/views/templates/_submission.html.erb | 4 +-- app/views/templates_share_link/show.html.erb | 4 +-- config/initializers/active_storage.rb | 8 ++++++ lib/replace_email_variables.rb | 26 ++++++++++++------- 12 files changed, 48 insertions(+), 31 deletions(-) diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 50823ca7b..592c006d3 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -42,10 +42,6 @@ class ApplicationController < ActionController::Base end def default_url_options - if request.domain == 'docuseal.com' - return { host: 'docuseal.com', protocol: ENV['FORCE_SSL'].present? ? 'https' : 'http' } - end - Docuseal.default_url_options end diff --git a/app/controllers/submissions_download_controller.rb b/app/controllers/submissions_download_controller.rb index eb216bc5a..39dee1653 100644 --- a/app/controllers/submissions_download_controller.rb +++ b/app/controllers/submissions_download_controller.rb @@ -65,7 +65,7 @@ def build_urls(submitter) key: AccountConfig::DOCUMENT_FILENAME_FORMAT_KEY)&.value Submitters.select_attachments_for_download(submitter).map do |attachment| - ActiveStorage::Blob.proxy_url( + ActiveStorage::Blob.proxy_path( attachment.blob, expires_at: FILES_TTL.from_now.to_i, filename: Submitters.build_document_filename(submitter, attachment.blob, filename_format) @@ -83,7 +83,7 @@ def build_combined_url(submitter) filename_format = AccountConfig.find_or_initialize_by(account_id: submitter.account_id, key: AccountConfig::DOCUMENT_FILENAME_FORMAT_KEY)&.value - ActiveStorage::Blob.proxy_url( + ActiveStorage::Blob.proxy_path( attachment.blob, expires_at: FILES_TTL.from_now.to_i, filename: Submitters.build_document_filename(submitter, attachment.blob, filename_format) diff --git a/app/controllers/submit_form_download_controller.rb b/app/controllers/submit_form_download_controller.rb index 3ebdc5e2c..af9cbeb43 100644 --- a/app/controllers/submit_form_download_controller.rb +++ b/app/controllers/submit_form_download_controller.rb @@ -33,7 +33,7 @@ def index end urls = attachments.map do |attachment| - ActiveStorage::Blob.proxy_url(attachment.blob, expires_at: FILES_TTL.from_now.to_i) + ActiveStorage::Blob.proxy_path(attachment.blob, expires_at: FILES_TTL.from_now.to_i) end render json: urls diff --git a/app/controllers/template_documents_controller.rb b/app/controllers/template_documents_controller.rb index b29a18f60..51fc41118 100644 --- a/app/controllers/template_documents_controller.rb +++ b/app/controllers/template_documents_controller.rb @@ -6,7 +6,7 @@ class TemplateDocumentsController < ApplicationController FILES_TTL = 5.minutes def index - render json: @template.schema_documents.map { |d| ActiveStorage::Blob.proxy_url(d.blob, expires_at: FILES_TTL.from_now.to_i) } + render json: @template.schema_documents.map { |d| ActiveStorage::Blob.proxy_path(d.blob, expires_at: FILES_TTL.from_now.to_i) } end def create diff --git a/app/mailers/submitter_mailer.rb b/app/mailers/submitter_mailer.rb index 18d2f5703..976ddc78d 100644 --- a/app/mailers/submitter_mailer.rb +++ b/app/mailers/submitter_mailer.rb @@ -14,12 +14,7 @@ def invitation_email(submitter) @email_message = submitter.account.email_messages.find_by(uuid: submitter.preferences['email_message_uuid']) end - template_submitters_index = - if @email_message.blank? - build_submitter_preferences_index(@submitter) - else - {} - end + template_submitters_index = @email_message.blank? ? build_submitter_preferences_index(@submitter) : {} @body = @email_message&.body.presence || template_submitters_index.dig(@submitter.uuid, 'request_email_body').presence || @@ -36,6 +31,8 @@ def invitation_email(submitter) reply_to = build_submitter_reply_to(@submitter) + maybe_set_custom_domain(@submitter) + I18n.with_locale(@current_account.locale) do subject = build_invite_subject(@subject, @email_config, submitter) @@ -133,6 +130,8 @@ def documents_copy_email(submitter, to: nil, sig: false) assign_message_metadata('submitter_documents_copy', @submitter) reply_to = build_submitter_reply_to(submitter, email_config: @email_config, documents_copy_email: true) + maybe_set_custom_domain(@submitter) + I18n.with_locale(@current_account.locale) do subject = @subject.present? ? ReplaceEmailVariables.call(@subject, submitter:) : I18n.t(:your_document_copy) @@ -262,4 +261,10 @@ def from_address_for_submitter(submitter) def fetch_config_email_body(email_config, _submitter = nil) email_config ? email_config.value['body'].presence : nil end + + def maybe_set_custom_domain(submitter) + if Docuseal.multitenant? && (config = AccountConfig.find_by(account_id: submitter.account_id, key: :custom_domain)) + @custom_domain = config.value + end + end end diff --git a/app/views/submissions/show.html.erb b/app/views/submissions/show.html.erb index 7940f3377..acbaedad4 100644 --- a/app/views/submissions/show.html.erb +++ b/app/views/submissions/show.html.erb @@ -21,7 +21,7 @@ <%= button_to button_title(title: t('unarchive'), disabled_with: t('unarchive')[0..-2], icon: svg_icon('rotate', class: 'w-6 h-6')), submission_unarchive_index_path(@submission), class: 'btn btn-primary btn-ghost text-base hidden md:flex' %> <% end %> <% if @submission.audit_trail.present? %> -
+ <%= svg_icon('external_link', class: 'w-6 h-6') %> diff --git a/app/views/submitter_mailer/documents_copy_email.html.erb b/app/views/submitter_mailer/documents_copy_email.html.erb index 3117481d0..9e230cf28 100644 --- a/app/views/submitter_mailer/documents_copy_email.html.erb +++ b/app/views/submitter_mailer/documents_copy_email.html.erb @@ -5,7 +5,7 @@

<%= t('please_check_the_copy_of_your_name_in_the_email_attachments', name: @submitter.submission.name || @submitter.submission.template.name) %>

<%= t('alternatively_you_can_review_and_download_your_copy_using_the_link_below') %>

- <%= link_to @submitter.submission.name || @submitter.submission.template.name, submissions_preview_url(@submitter.submission.slug, { sig: @sig }.compact) %> + <%= link_to @submitter.submission.name || @submitter.submission.template.name, submissions_preview_url(@submitter.submission.slug, { sig: @sig, host: @custom_domain || ENV.fetch('EMAIL_HOST', Docuseal.default_url_options[:host]) }.compact) %>

<%= t('thanks') %>,
<%= @current_account.name %> diff --git a/app/views/submitter_mailer/invitation_email.html.erb b/app/views/submitter_mailer/invitation_email.html.erb index 06e38f30b..408016df0 100644 --- a/app/views/submitter_mailer/invitation_email.html.erb +++ b/app/views/submitter_mailer/invitation_email.html.erb @@ -1,12 +1,12 @@ <% if @body.present? %> <%= render 'custom_content', content: @body, submitter: @submitter %> <% if !@body.match?(ReplaceEmailVariables::SUBMITTER_LINK) && !@body.match?(ReplaceEmailVariables::SUBMITTER_ID) && !@body.match?(ReplaceEmailVariables::SUBMISSION_LINK) && !@body.match?(ReplaceEmailVariables::TEMPLATE_ID) && !@submitter.submission.source.in?(%w[api embed]) %> -

<%= link_to nil, submit_form_url(slug: @submitter.slug, t: SubmissionEvents.build_tracking_param(@submitter, 'click_email')) %>

+

<%= link_to nil, submit_form_url(slug: @submitter.slug, t: SubmissionEvents.build_tracking_param(@submitter, 'click_email'), host: @custom_domain || ENV.fetch('EMAIL_HOST', Docuseal.default_url_options[:host])) %>

<% end %> <% else %>

<%= t('hi_there') %>,

<%= I18n.t(@submitter.with_signature_fields? ? :you_have_been_invited_to_sign_the_name : :you_have_been_invited_to_submit_the_name_form, name: @submitter.submission.name || @submitter.submission.template.name) %>

-

<%= link_to I18n.t(@submitter.with_signature_fields? ? :review_and_sign : :review_and_submit), submit_form_url(slug: @submitter.slug, t: SubmissionEvents.build_tracking_param(@submitter, 'click_email'), host: ENV.fetch('EMAIL_HOST', Docuseal.default_url_options[:host])) %>

+

<%= link_to I18n.t(@submitter.with_signature_fields? ? :review_and_sign : :review_and_submit), submit_form_url(slug: @submitter.slug, t: SubmissionEvents.build_tracking_param(@submitter, 'click_email'), host: @custom_domain || ENV.fetch('EMAIL_HOST', Docuseal.default_url_options[:host])) %>

<%= t('please_contact_us_by_replying_to_this_email_if_you_have_any_questions') %>

<%= t('thanks') %>,
<%= @current_account.name %> diff --git a/app/views/templates/_submission.html.erb b/app/views/templates/_submission.html.erb index 3c3348ecb..bcfcd66c6 100644 --- a/app/views/templates/_submission.html.erb +++ b/app/views/templates/_submission.html.erb @@ -81,7 +81,7 @@ <% elsif !submission.archived_at? && !template&.archived_at? && !submission.expired? && !submitter.declined_at? %> <% if current_user.email == submitter.email %>

- + <% if t('sign_now').length < 12 %> <%= svg_icon('writing_sign', class: 'w-4 h-4 stroke-2') %> @@ -167,7 +167,7 @@ <% elsif !template&.archived_at? && !submission.archived_at? && !is_submission_completed && !submission.expired? && !submitter.declined_at? %>
<% if current_user.email == submitter.email %> - + <% if t('sign_now').length < 12 %> <%= svg_icon('writing_sign', class: 'w-4 h-4 stroke-2') %> diff --git a/app/views/templates_share_link/show.html.erb b/app/views/templates_share_link/show.html.erb index 1511b4c20..43149a290 100644 --- a/app/views/templates_share_link/show.html.erb +++ b/app/views/templates_share_link/show.html.erb @@ -17,9 +17,9 @@ <% end %>
- + - <%= render 'shared/clipboard_copy', icon: 'copy', text: start_form_url(slug: @template.slug), class: 'base-button', icon_class: 'w-6 h-6 text-white', copy_title: t('copy'), copied_title: t('copied') %> + <%= render 'shared/clipboard_copy', icon: 'copy', text: start_form_url(slug: @template.slug, host: form_link_host), class: 'base-button', icon_class: 'w-6 h-6 text-white', copy_title: t('copy'), copied_title: t('copied') %>
<% end %> diff --git a/config/initializers/active_storage.rb b/config/initializers/active_storage.rb index 78637ddbc..2bcdc57bb 100644 --- a/config/initializers/active_storage.rb +++ b/config/initializers/active_storage.rb @@ -10,6 +10,7 @@ def signed_uuid end end +# rubocop:disable Metrics/BlockLength ActiveSupport.on_load(:active_storage_blob) do attribute :uuid, :string, default: -> { SecureRandom.uuid } attribute :io_data, :string, default: '' @@ -22,6 +23,12 @@ def self.proxy_url(blob, expires_at: nil, filename: nil, host: nil) ) end + def self.proxy_path(blob, expires_at: nil, filename: nil) + Rails.application.routes.url_helpers.blobs_proxy_path( + signed_uuid: blob.signed_uuid(expires_at:), filename: filename || blob.filename + ) + end + def uuid super || begin new_uuid = SecureRandom.uuid @@ -40,6 +47,7 @@ def delete service.delete(key) end end +# rubocop:enable Metrics/BlockLength ActiveStorage::LogSubscriber.detach_from(:active_storage) if Rails.env.production? diff --git a/lib/replace_email_variables.rb b/lib/replace_email_variables.rb index 4222e676e..392c585d9 100644 --- a/lib/replace_email_variables.rb +++ b/lib/replace_email_variables.rb @@ -89,8 +89,10 @@ def call(text, submitter:, tracking_event_type: 'click_email', html_escape: fals # rubocop:enable Metrics def build_documents_links_text(submitter, sig = nil) + url_options = build_url_options_for(submitter) + Rails.application.routes.url_helpers.submissions_preview_url( - submitter.submission.slug, { sig:, **Docuseal.default_url_options }.compact + submitter.submission.slug, { sig:, **url_options }.compact ) end @@ -139,14 +141,9 @@ def replace(text, var, html_escape: false) end def build_submitter_link(submitter, tracking_event_type) - if tracking_event_type == 'click_email' - url_options = - if EMAIL_HOST.present? - { host: EMAIL_HOST, protocol: ENV['FORCE_SSL'].present? ? 'https' : 'http' } - else - Docuseal.default_url_options - end + url_options = build_url_options_for(submitter, is_email: tracking_event_type == 'click_email') + if tracking_event_type == 'click_email' Rails.application.routes.url_helpers.submit_form_url( slug: submitter.slug, t: SubmissionEvents.build_tracking_param(submitter, 'click_email'), @@ -156,11 +153,22 @@ def build_submitter_link(submitter, tracking_event_type) Rails.application.routes.url_helpers.submit_form_url( slug: submitter.slug, c: SubmissionEvents.build_tracking_param(submitter, 'click_sms'), - **Docuseal.default_url_options + **url_options ) end end + def build_url_options_for(submitter, is_email: true) + if Docuseal.multitenant? && + (config = AccountConfig.find_by(account_id: submitter.account_id, key: :custom_domain)) + { host: config.value, protocol: 'https' } + elsif is_email && EMAIL_HOST.present? + { host: EMAIL_HOST, protocol: ENV['FORCE_SSL'].present? ? 'https' : 'http' } + else + Docuseal.default_url_options + end + end + def build_submission_link(submission) Rails.application.routes.url_helpers.submission_url(submission, **Docuseal.default_url_options) end From 65c275ac175789ac101de23485ef460bd1fb8402 Mon Sep 17 00:00:00 2001 From: Pete Matsyburka Date: Fri, 20 Feb 2026 10:16:06 +0200 Subject: [PATCH 29/54] rename --- .../templates_prefillable_fields/_form.html.erb | 2 +- config/locales/i18n.yml | 14 +++++++------- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/app/views/templates_prefillable_fields/_form.html.erb b/app/views/templates_prefillable_fields/_form.html.erb index bdfd92f78..a9eda3364 100644 --- a/app/views/templates_prefillable_fields/_form.html.erb +++ b/app/views/templates_prefillable_fields/_form.html.erb @@ -4,7 +4,7 @@ <%= form_for '', url: template_prefillable_fields_path(template), method: :post, data: { close_on_submit: false } do |f| %>
<%= f.hidden_field :prefillable, value: 'true' %> - <%= f.label :field_uuid, t(:invite_form_fields), class: 'label' %> + <%= f.label :field_uuid, t(:sender_form_fields), class: 'label' %>
<%= select_tag :field_uuid, options_for_select(select_fields), prompt: t(:select_field), class: 'base-select w-full join-item', dir: 'auto', required: true %> <%= f.button button_title(title: t('add'), disabled_with: t('add')), class: 'base-button join-item !px-6' %> diff --git a/config/locales/i18n.yml b/config/locales/i18n.yml index 7fc423863..4b3f7777b 100644 --- a/config/locales/i18n.yml +++ b/config/locales/i18n.yml @@ -44,7 +44,7 @@ en: &en click_here_to_send_a_reset_password_email_html: ' to send a reset password email.' edit_order: Edit Order expirable_file_download_links: Expirable file download links - invite_form_fields: Invite form fields + sender_form_fields: Sender form fields default_parties: Default parties authenticate_embedded_form_preview_with_token: Authenticate embedded form preview with token stripe_integration: Stripe Integration @@ -1025,7 +1025,7 @@ es: &es party: Parte edit_order: Edita Pedido select: Seleccionar - invite_form_fields: Invitar campos del formulario + sender_form_fields: Campos del formulario del remitente pro: Pro default_parties: Partes predeterminadas authenticate_embedded_form_preview_with_token: Autenticar vista previa del formulario incrustado con token @@ -2012,7 +2012,7 @@ it: &it party: Parte edit_order: Modifica Ordine select: Seleziona - invite_form_fields: Invita campi modulo + sender_form_fields: Campi del modulo del mittente pro: Pro default_parties: Parti predefiniti authenticate_embedded_form_preview_with_token: "Autentica l'anteprima del modulo incorporato con il token" @@ -3000,7 +3000,7 @@ fr: &fr party: Partie edit_order: Modifier l’ordre select: Sélectionner - invite_form_fields: Champs du formulaire d’invitation + sender_form_fields: Champs du formulaire de l’expéditeur pro: Pro default_parties: Parties par défaut authenticate_embedded_form_preview_with_token: Authentifier l’aperçu du formulaire intégré avec un jeton @@ -3984,7 +3984,7 @@ pt: &pt party: Parte edit_order: Edita Pedido select: Selecionar - invite_form_fields: Convidar campos do formulário + sender_form_fields: Campos do formulário do remetente pro: Pro default_parties: Partes padrão authenticate_embedded_form_preview_with_token: Autenticar visualização incorporada do formulário com token @@ -4980,7 +4980,7 @@ de: &de click_here_to_send_a_reset_password_email_html: ', um eine E-Mail zum Zurücksetzen des Passworts zu senden.' edit_order: Bestellung bearbeiten expirable_file_download_links: Ablaufbare Datei-Download-Links - invite_form_fields: Einladungsformular-Felder + sender_form_fields: Absenderformular-Felder default_parties: Standardparteien authenticate_embedded_form_preview_with_token: Eingebettete Formularvorschau mit Token authentifizieren stripe_integration: Stripe-Integration @@ -6355,7 +6355,7 @@ nl: &nl click_here_to_send_a_reset_password_email_html: om een e-mail voor wachtwoordherstel te verzenden. edit_order: Volgorde bewerken expirable_file_download_links: Verlopende downloadlinks voor bestanden - invite_form_fields: Velden van uitnodigingsformulier + sender_form_fields: Velden van afzenderformulier default_parties: Standaard partijen authenticate_embedded_form_preview_with_token: Preview van ingesloten formulier authenticeren met token stripe_integration: Stripe-integratie From ba84741a64aa19434798aebed58503c7e4185532 Mon Sep 17 00:00:00 2001 From: Pete Matsyburka Date: Fri, 20 Feb 2026 12:27:39 +0200 Subject: [PATCH 30/54] invite party via field --- app/controllers/api/submissions_controller.rb | 5 +- app/controllers/api/submitters_controller.rb | 7 ++- app/controllers/api/templates_controller.rb | 3 +- .../submit_form_invite_controller.rb | 4 +- app/controllers/templates_controller.rb | 3 +- .../templates_recipients_controller.rb | 4 +- app/views/submissions/_detailed_form.html.erb | 2 +- app/views/submissions/_email_form.html.erb | 2 +- app/views/submissions/_phone_form.html.erb | 2 +- .../_recipients.html.erb | 12 +++-- config/locales/i18n.yml | 7 +++ lib/submissions/create_from_submitters.rb | 9 +++- lib/submitters/submit_values.rb | 48 ++++++++++++++++++- lib/templates.rb | 1 + lib/templates/clone.rb | 36 +++++++------- 15 files changed, 114 insertions(+), 31 deletions(-) diff --git a/app/controllers/api/submissions_controller.rb b/app/controllers/api/submissions_controller.rb index 22af24d92..77b4e6154 100644 --- a/app/controllers/api/submissions_controller.rb +++ b/app/controllers/api/submissions_controller.rb @@ -172,7 +172,10 @@ def create_submissions(template, params) Submissions::NormalizeParamUtils.save_default_value_attachments!(attachments, submitters) submitters.each do |submitter| - SubmissionEvents.create_with_tracking_data(submitter, 'api_complete_form', request) if submitter.completed_at? + if submitter.completed_at? + Submitters::SubmitValues.maybe_invite_via_field(submitter, request) + SubmissionEvents.create_with_tracking_data(submitter, 'api_complete_form', request) + end end submissions diff --git a/app/controllers/api/submitters_controller.rb b/app/controllers/api/submitters_controller.rb index e56eb8b8f..f28bf5e11 100644 --- a/app/controllers/api/submitters_controller.rb +++ b/app/controllers/api/submitters_controller.rb @@ -34,6 +34,7 @@ def show render json: Submitters::SerializeForApi.call(@submitter, with_template: true, with_events: true, params:) end + # rubocop:disable Metrics/MethodLength def update if @submitter.completed_at? return render json: { error: 'Submitter has already completed the submission.' }, status: :unprocessable_content @@ -60,7 +61,10 @@ def update @submitter.submission.save! - SubmissionEvents.create_with_tracking_data(@submitter, 'api_complete_form', request) if @submitter.completed_at? + if @submitter.completed_at? + Submitters::SubmitValues.maybe_invite_via_field(@submitter, request) + SubmissionEvents.create_with_tracking_data(@submitter, 'api_complete_form', request) + end end if @submitter.completed_at? @@ -78,6 +82,7 @@ def update render json: { error: e.message }, status: :unprocessable_content end + # rubocop:enable Metrics/MethodLength def submitter_params submitter_params = params.key?(:submitter) ? params.require(:submitter) : params diff --git a/app/controllers/api/templates_controller.rb b/app/controllers/api/templates_controller.rb index c8211b7fa..c3f1dd421 100644 --- a/app/controllers/api/templates_controller.rb +++ b/app/controllers/api/templates_controller.rb @@ -107,7 +107,8 @@ def template_params :external_id, :shared_link, { - submitters: [%i[name uuid is_requester invite_by_uuid optional_invite_by_uuid linked_to_uuid email order]], + submitters: [%i[name uuid is_requester invite_by_uuid invite_via_field_uuid + optional_invite_by_uuid linked_to_uuid email order]], fields: [[:uuid, :submitter_uuid, :name, :type, :required, :readonly, :default_value, :title, :description, :prefillable, diff --git a/app/controllers/submit_form_invite_controller.rb b/app/controllers/submit_form_invite_controller.rb index bcc848ae9..413e2b9a1 100644 --- a/app/controllers/submit_form_invite_controller.rb +++ b/app/controllers/submit_form_invite_controller.rb @@ -19,7 +19,9 @@ def create next unless attrs next if attrs[:email].blank? - submitter.submission.submitters.create!(**attrs, account_id: submitter.account_id) + email = Submissions.normalize_email(attrs[:email]) + + submitter.submission.submitters.create!(uuid: attrs[:uuid], email:, account_id: submitter.account_id) SubmissionEvents.create_with_tracking_data(submitter, 'invite_party', request, { uuid: submitter.uuid }) end diff --git a/app/controllers/templates_controller.rb b/app/controllers/templates_controller.rb index 39b450445..0d6db6f7d 100644 --- a/app/controllers/templates_controller.rb +++ b/app/controllers/templates_controller.rb @@ -97,7 +97,8 @@ def template_params :name, { schema: [[:attachment_uuid, :google_drive_file_id, :name, { conditions: [%i[field_uuid value action operation]] }]], - submitters: [%i[name uuid is_requester linked_to_uuid invite_by_uuid optional_invite_by_uuid email order]], + submitters: [%i[name uuid is_requester linked_to_uuid invite_via_field_uuid + invite_by_uuid optional_invite_by_uuid email order]], fields: [[:uuid, :submitter_uuid, :name, :type, :required, :readonly, :default_value, :title, :description, :prefillable, diff --git a/app/controllers/templates_recipients_controller.rb b/app/controllers/templates_recipients_controller.rb index 17a4bbb85..64fa58b25 100644 --- a/app/controllers/templates_recipients_controller.rb +++ b/app/controllers/templates_recipients_controller.rb @@ -22,7 +22,7 @@ def create def submitters_params permit_params = { submitters: [%i[name uuid is_requester optional_invite_by_uuid - invite_by_uuid linked_to_uuid email option order]] } + invite_by_uuid invite_via_field_uuid linked_to_uuid email option order]] } params.require(:template).permit(permit_params).fetch(:submitters, {}).values.filter_map do |s| next if s[:uuid].blank? @@ -36,6 +36,7 @@ def submitters_params s[:order] = s[:order].to_i if s[:order].present? s.delete(:invite_by_uuid) if s[:invite_by_uuid].blank? s.delete(:optional_invite_by_uuid) if s[:optional_invite_by_uuid].blank? + s.delete(:invite_via_field_uuid) if s[:invite_via_field_uuid].blank? normalize_option_value(s) end @@ -53,6 +54,7 @@ def normalize_option_value(attrs) attrs.delete(:email) attrs.delete(:linked_to_uuid) attrs.delete(:invite_by_uuid) + attrs.delete(:invite_via_field_uuid) attrs.delete(:optional_invite_by_uuid) when /\Alinked_to_(.*)\z/ attrs[:linked_to_uuid] = ::Regexp.last_match(-1) diff --git a/app/views/submissions/_detailed_form.html.erb b/app/views/submissions/_detailed_form.html.erb index ac4c26078..7e38fecb5 100644 --- a/app/views/submissions/_detailed_form.html.erb +++ b/app/views/submissions/_detailed_form.html.erb @@ -1,6 +1,6 @@ <% has_phone_field = false %> <%= form_for '', url: template_submissions_path(template), html: { class: 'space-y-4', autocomplete: 'off' }, data: { turbo_frame: :_top } do |f| %> - <% submitters = template.submitters.reject { |e| e['invite_by_uuid'].present? || e['optional_invite_by_uuid'].present? } %> + <% submitters = template.submitters.reject { |e| e['invite_by_uuid'].present? || e['optional_invite_by_uuid'].present? || e['invite_via_field_uuid'].present? } %>
diff --git a/app/views/submissions/_email_form.html.erb b/app/views/submissions/_email_form.html.erb index a88e3a0b6..2fee67063 100644 --- a/app/views/submissions/_email_form.html.erb +++ b/app/views/submissions/_email_form.html.erb @@ -1,5 +1,5 @@ <%= form_for '', url: template_submissions_path(template), html: { class: 'space-y-4', autocomplete: 'off' }, data: { turbo_frame: :_top } do |f| %> - <% submitters = template.submitters.reject { |e| e['invite_by_uuid'].present? || e['optional_invite_by_uuid'].present? } %> + <% submitters = template.submitters.reject { |e| e['invite_by_uuid'].present? || e['optional_invite_by_uuid'].present? || e['invite_via_field_uuid'].present? } %> <% if submitters.size == 1 %> diff --git a/app/views/submissions/_phone_form.html.erb b/app/views/submissions/_phone_form.html.erb index 68bf6d458..02900a532 100644 --- a/app/views/submissions/_phone_form.html.erb +++ b/app/views/submissions/_phone_form.html.erb @@ -1,5 +1,5 @@ <%= form_for '', url: template_submissions_path(template), html: { class: 'space-y-4', autocomplete: 'off' }, data: { turbo_frame: :_top } do |f| %> - <% submitters = template.submitters.reject { |e| e['invite_by_uuid'].present? || e['optional_invite_by_uuid'].present? } %> + <% submitters = template.submitters.reject { |e| e['invite_by_uuid'].present? || e['optional_invite_by_uuid'].present? || e['invite_via_field_uuid'].present? } %>
diff --git a/app/views/templates_preferences/_recipients.html.erb b/app/views/templates_preferences/_recipients.html.erb index a8547669e..4d45e38f9 100644 --- a/app/views/templates_preferences/_recipients.html.erb +++ b/app/views/templates_preferences/_recipients.html.erb @@ -7,8 +7,8 @@
<% template.submitters.each_with_index do |submitter, index| %>
- <%= f.fields_for :submitters, item = Struct.new(:name, :uuid, :is_requester, :email, :invite_by_uuid, :optional_invite_by_uuid, :linked_to_uuid, :order, :option).new(*submitter.values_at('name', 'uuid', 'is_requester', 'email', 'invite_by_uuid', 'optional_invite_by_uuid', 'linked_to_uuid', 'order')), index: do |ff| %> - <% item.option = item.is_requester.present? ? 'is_requester' : (item.email.present? ? 'email' : (item.linked_to_uuid.present? ? "linked_to_#{item.linked_to_uuid}" : (item.invite_by_uuid.present? ? "invite_by_#{item.invite_by_uuid}" : (item.optional_invite_by_uuid.present? ? "optional_invite_by_#{item.optional_invite_by_uuid}" : '')))) %> + <%= f.fields_for :submitters, item = Struct.new(:name, :uuid, :is_requester, :email, :invite_by_uuid, :invite_via_field_uuid, :optional_invite_by_uuid, :linked_to_uuid, :order, :option).new(*submitter.values_at('name', 'uuid', 'is_requester', 'email', 'invite_by_uuid', 'invite_via_field_uuid', 'optional_invite_by_uuid', 'linked_to_uuid', 'order')), index: do |ff| %> + <% item.option = item.is_requester.present? ? 'is_requester' : (item.email.present? ? 'email' : (item.linked_to_uuid.present? ? "linked_to_#{item.linked_to_uuid}" : (item.invite_by_uuid.present? ? "invite_by_#{item.invite_by_uuid}" : (item.optional_invite_by_uuid.present? ? "optional_invite_by_#{item.optional_invite_by_uuid}" : (item.invite_via_field_uuid.present? ? 'invite_via_field' : ''))))) %> <%= ff.hidden_field :uuid %>
@@ -28,10 +28,16 @@ <% if template.submitters.size == 2 %> <%= tag.input name: ff.field_name(:email), value: ff.object.email, type: :email, class: 'base-input', multiple: true, autocomplete: 'off', placeholder: t('default_email'), disabled: ff.object.is_requester || ff.object.invite_by_uuid.present? || ff.object.optional_invite_by_uuid.present?, id: field_uuid = SecureRandom.uuid %> <% else %> + <% invite_fields = template.fields.select { |field| field['name'].present? && field['submitter_uuid'] != submitter['uuid'] } %> - <%= ff.select :option, [[t('not_specified'), 'not_set'], (local_assigns[:with_submission_requester] == false ? nil : [t('submission_requester'), 'is_requester']), [t('specified_email'), 'email'], *(template.submitters - [submitter]).flat_map { |e| [[t('invite_by_name', name: e['name']), "invite_by_#{e['uuid']}"], [t('invite_by_name', name: e['name']) + " (#{t(:optional).capitalize})", "optional_invite_by_#{e['uuid']}"]] }, *(template.submitters - [submitter]).map { |e| [t('same_as_name', name: e['name']), "linked_to_#{e['uuid']}"] }].compact, {}, class: 'base-select mb-3' %> + + <%= ff.select :option, [[t('not_specified'), 'not_set'], (local_assigns[:with_submission_requester] == false ? nil : [t('submission_requester'), 'is_requester']), [t('specified_email'), 'email'], *(template.submitters - [submitter]).flat_map { |e| [[t('invite_by_name', name: e['name']), "invite_by_#{e['uuid']}"], [t('invite_by_name', name: e['name']) + " (#{t(:optional).capitalize})", "optional_invite_by_#{e['uuid']}"]] }, *(template.submitters - [submitter]).map { |e| [t('same_as_name', name: e['name']), "linked_to_#{e['uuid']}"] }, (invite_fields.present? ? [t('invite_via_form_field'), 'invite_via_field'] : nil)].compact, {}, class: 'base-select mb-3' %> + <%= tag.input name: ff.field_name(:email), type: :email, value: ff.object.email, multiple: true, class: "base-input #{'hidden' if item.option != 'email'}", autocomplete: 'off', placeholder: t('default_email'), id: email_field_uuid %> + + <%= select_tag ff.field_name(:invite_via_field_uuid), options_for_select(invite_fields.map { |field| [field['name'], field['uuid']] }, item.invite_via_field_uuid), prompt: t(:select_field), class: 'base-select',required: true %> + <% end %>
<% if template.submitters.size == 2 %> diff --git a/config/locales/i18n.yml b/config/locales/i18n.yml index 4b3f7777b..1a5312c67 100644 --- a/config/locales/i18n.yml +++ b/config/locales/i18n.yml @@ -500,6 +500,7 @@ en: &en submission_requester: Submission requester specified_email: Specified email invite_by_name: 'Invite by %{name}' + invite_via_form_field: Invite via Form Field same_as_name: 'Same as %{name}' default_email: Default Email processing: Processing @@ -1490,6 +1491,7 @@ es: &es submission_requester: Solicitante del envío specified_email: Correo electrónico especificado invite_by_name: 'Invitar por %{name}' + invite_via_form_field: Invitar a través de campo del formulario same_as_name: 'Igual que %{name}' default_email: Correo electrónico predeterminado processing: Procesando @@ -2477,6 +2479,7 @@ it: &it submission_requester: "Richiedente dell'invio" specified_email: Email specificata invite_by_name: 'Invito da %{name}' + invite_via_form_field: Invito tramite campo del modulo same_as_name: 'Uguale a %{name}' default_email: Email predefinita processing: Elaborazione in corso @@ -3465,6 +3468,7 @@ fr: &fr submission_requester: Demandeur de soumission specified_email: E‑mail spécifié invite_by_name: Inviter par %{name} + invite_via_form_field: Inviter via champ du formulaire same_as_name: Identique à %{name} default_email: E‑mail par défaut processing: Traitement en cours @@ -4449,6 +4453,7 @@ pt: &pt submission_requester: Solicitante de submissão specified_email: E-mail especificado invite_by_name: 'Convidado por %{name}' + invite_via_form_field: Convidar via campo do formulário same_as_name: 'Igual a %{name}' default_email: E-mail padrão processing: Processando @@ -5436,6 +5441,7 @@ de: &de submission_requester: Anfragende Person specified_email: Angegebene E-Mail invite_by_name: 'Einladung von %{name}' + invite_via_form_field: Einladung über Formularfeld same_as_name: 'Gleich wie %{name}' default_email: Standard-E-Mail processing: Verarbeitung @@ -6812,6 +6818,7 @@ nl: &nl submission_requester: Aanvrager van inzending specified_email: Opgegeven e-mail invite_by_name: Uitnodigen door %{name} + invite_via_form_field: Uitnodigen via formulierveld same_as_name: Zelfde als %{name} default_email: Standaard e-mail processing: Verwerken diff --git a/lib/submissions/create_from_submitters.rb b/lib/submissions/create_from_submitters.rb index b1e318cf5..b367403c0 100644 --- a/lib/submissions/create_from_submitters.rb +++ b/lib/submissions/create_from_submitters.rb @@ -56,7 +56,9 @@ def call(template:, user:, submissions_attrs:, source:, submitters_order:, param template_submitter = template_submitters.find { |e| e['uuid'] == uuid } end - template_submitter = template_submitter.except('optional_invite_by_uuid', 'invite_by_uuid') + template_submitter = template_submitter.except('optional_invite_by_uuid', 'invite_by_uuid', + 'invite_via_field_uuid') + template_submitter['order'] = submitter_attrs['order'] if submitter_attrs['order'].present? submission.template_submitters << template_submitter @@ -113,7 +115,10 @@ def maybe_add_invite_submitters(submission, template, submitter_attrs) item = item.merge('invite_by_uuid' => invite_by_uuid) if invite_by_uuid end - next if item['invite_by_uuid'].blank? && item['optional_invite_by_uuid'].blank? + next if item['invite_by_uuid'].blank? && + item['optional_invite_by_uuid'].blank? && + item['invite_via_field_uuid'].blank? + next if submission.template_submitters.any? { |e| e['uuid'] == item['uuid'] } item = item.merge('order' => submitter_attr['order']) if submitter_attr && submitter_attr['order'].present? diff --git a/lib/submitters/submit_values.rb b/lib/submitters/submit_values.rb index 4b7f4dcb8..a29929576 100644 --- a/lib/submitters/submit_values.rb +++ b/lib/submitters/submit_values.rb @@ -6,6 +6,7 @@ module SubmitValues RequiredFieldError = Class.new(StandardError) VARIABLE_REGEXP = /\{\{?(\w+)\}\}?/ + PHONE_REGEXP = /[+\d()\s-]+/ NONEDITABLE_FIELD_TYPES = %w[stamp heading strikethrough].freeze STRFTIME_MAP = { @@ -52,7 +53,11 @@ def update_submitter!(submitter, params, request, validate_required: true) ActiveStorage::Attachment.where(uuid: touch_attachment_uuid, record: submitter).touch_all(:created_at) end - SubmissionEvents.create_with_tracking_data(submitter, 'complete_form', request) if params[:completed] == 'true' + if params[:completed] == 'true' + maybe_invite_via_field(submitter, request) + + SubmissionEvents.create_with_tracking_data(submitter, 'complete_form', request) + end submitter.save! end @@ -405,6 +410,47 @@ def replace_default_variables(value, attrs, submission, with_time: false) end end + def maybe_invite_via_field(submitter, request) + submission = submitter.submission + + is_invited = false + + submission.template_submitters.each do |s| + field_uuid = s['invite_via_field_uuid'] + + next if field_uuid.blank? + + field = submission.template_fields.find { |e| e['uuid'] == field_uuid } + + next unless field + next unless field['submitter_uuid'] == submitter.uuid + + next if submission.submitters.exists?(uuid: s['uuid']) + + value = submitter.values[field_uuid] + + next if value.blank? + + if value.include?('@') + email = Submissions.normalize_email(value) + elsif value.match?(PHONE_REGEXP) + phone = value.gsub(/[^+\d]/, '') + end + + next if email.blank? && phone.blank? + + submission.submitters.create!(uuid: s['uuid'], email:, phone:, account_id: submitter.account_id) + + SubmissionEvents.create_with_tracking_data(submitter, 'invite_party', request, { uuid: submitter.uuid }) + + is_invited = true + end + + submission.update!(submitters_order: :preserved) if is_invited + + submitter + end + def validate_value!(_value, field, _params, submitter, _request) if field['readonly'] == true Rollbar.warning("Readonly field #{submitter.id}: #{field['uuid']}") if defined?(Rollbar) diff --git a/lib/templates.rb b/lib/templates.rb index 2abd93ddd..ad63302bf 100644 --- a/lib/templates.rb +++ b/lib/templates.rb @@ -72,6 +72,7 @@ def fulltext_search(current_user, templates, keyword) def filter_undefined_submitters(template_submitters) template_submitters.to_a.select do |item| item['invite_by_uuid'].blank? && item['optional_invite_by_uuid'].blank? && + item['invite_via_field_uuid'].blank? && item['linked_to_uuid'].blank? && item['is_requester'].blank? && item['email'].blank? end end diff --git a/lib/templates/clone.rb b/lib/templates/clone.rb index e4b5fc60a..1d213152a 100644 --- a/lib/templates/clone.rb +++ b/lib/templates/clone.rb @@ -4,7 +4,7 @@ module Templates module Clone module_function - # rubocop:disable Metrics, Style/CombinableLoops + # rubocop:disable Metrics def call(original_template, author:, external_id: nil, name: nil, folder_name: nil) template = original_template.account.templates.new @@ -49,20 +49,6 @@ def update_submitters_and_fields_and_schema(cloned_submitters, cloned_fields, cl submitter['uuid'] = new_submitter_uuid end - cloned_submitters.each do |submitter| - if submitter['optional_invite_by_uuid'].present? - submitter['optional_invite_by_uuid'] = submitter_uuids_replacements[submitter['optional_invite_by_uuid']] - end - - if submitter['invite_by_uuid'].present? - submitter['invite_by_uuid'] = submitter_uuids_replacements[submitter['invite_by_uuid']] - end - - if submitter['linked_to_uuid'].present? - submitter['linked_to_uuid'] = submitter_uuids_replacements[submitter['linked_to_uuid']] - end - end - cloned_preferences['submitters'].to_a.each do |submitter| submitter['uuid'] = submitter_uuids_replacements[submitter['uuid']] end @@ -97,8 +83,26 @@ def update_submitters_and_fields_and_schema(cloned_submitters, cloned_fields, cl end end + cloned_submitters.each do |submitter| + if submitter['optional_invite_by_uuid'].present? + submitter['optional_invite_by_uuid'] = submitter_uuids_replacements[submitter['optional_invite_by_uuid']] + end + + if submitter['invite_by_uuid'].present? + submitter['invite_by_uuid'] = submitter_uuids_replacements[submitter['invite_by_uuid']] + end + + if submitter['linked_to_uuid'].present? + submitter['linked_to_uuid'] = submitter_uuids_replacements[submitter['linked_to_uuid']] + end + + if submitter['invite_via_field_uuid'].present? + submitter['invite_via_field_uuid'] = field_uuids_replacements[submitter['invite_via_field_uuid']] + end + end + [cloned_submitters, cloned_fields, cloned_schema, cloned_preferences] end - # rubocop:enable Metrics, Style/CombinableLoops + # rubocop:enable Metrics end end From fb5e13ee4ca1bc6e9d63c39aa2e6571ea523c04f Mon Sep 17 00:00:00 2001 From: Pete Matsyburka Date: Fri, 20 Feb 2026 18:01:12 +0200 Subject: [PATCH 31/54] adjust recipients form --- .rubocop.yml | 2 +- app/javascript/elements/toggle_attribute.js | 10 +++- app/javascript/elements/toggle_classes.js | 12 +++- .../_recipients.html.erb | 56 ++++++++++++++----- 4 files changed, 62 insertions(+), 18 deletions(-) diff --git a/.rubocop.yml b/.rubocop.yml index 019c504b5..9be296e58 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -84,7 +84,7 @@ RSpec/AnyInstance: Enabled: false Metrics/BlockNesting: - Max: 5 + Max: 6 Rails/I18nLocaleTexts: Enabled: false diff --git a/app/javascript/elements/toggle_attribute.js b/app/javascript/elements/toggle_attribute.js index e9ee30754..5ff6b7c3b 100644 --- a/app/javascript/elements/toggle_attribute.js +++ b/app/javascript/elements/toggle_attribute.js @@ -1,12 +1,18 @@ export default class extends HTMLElement { connectedCallback () { this.input.addEventListener('change', (event) => { + if (!this.target) return + + const value = event.target.type === 'checkbox' ? event.target.checked : event.target.value + const dataValue = this.dataset.value === 'false' ? false : this.dataset.value || true + if (this.dataset.attribute) { - this.target[this.dataset.attribute] = event.target.checked + this.target[this.dataset.attribute] = value === dataValue } if (this.dataset.className) { - this.target.classList.toggle(this.dataset.className, event.target.value !== this.dataset.value) + this.target.classList.toggle(this.dataset.className, value !== dataValue) + if (this.dataset.className === 'hidden' && this.target.tagName === 'INPUT') { this.target.disabled = event.target.value !== this.dataset.value } diff --git a/app/javascript/elements/toggle_classes.js b/app/javascript/elements/toggle_classes.js index ab96f293d..332ac84ff 100644 --- a/app/javascript/elements/toggle_classes.js +++ b/app/javascript/elements/toggle_classes.js @@ -1,10 +1,18 @@ export default class extends HTMLElement { connectedCallback () { - const button = this.querySelector('a, button') + const button = this.querySelector('a, button, label') + + const target = this.dataset.targetId ? document.getElementById(this.dataset.targetId) : button button.addEventListener('click', () => { this.dataset.classes.split(' ').forEach((cls) => { - button.classList.toggle(cls) + if (this.dataset.action === 'remove') { + target.classList.remove(cls) + } else if (this.dataset.action === 'add') { + target.classList.add(cls) + } else { + target.classList.toggle(cls) + } }) }) } diff --git a/app/views/templates_preferences/_recipients.html.erb b/app/views/templates_preferences/_recipients.html.erb index 4d45e38f9..4e5eb61b9 100644 --- a/app/views/templates_preferences/_recipients.html.erb +++ b/app/views/templates_preferences/_recipients.html.erb @@ -26,7 +26,20 @@ <% end %>
<% if template.submitters.size == 2 %> - <%= tag.input name: ff.field_name(:email), value: ff.object.email, type: :email, class: 'base-input', multiple: true, autocomplete: 'off', placeholder: t('default_email'), disabled: ff.object.is_requester || ff.object.invite_by_uuid.present? || ff.object.optional_invite_by_uuid.present?, id: field_uuid = SecureRandom.uuid %> + <%= tag.input name: ff.field_name(:email), value: ff.object.email, type: :email, multiple: true, autocomplete: 'off', placeholder: t('default_email'), id: email_field_uuid = SecureRandom.uuid, disabled: is_email_disabled = ff.object.is_requester || ff.object.invite_by_uuid.present? || ff.object.optional_invite_by_uuid.present? || ff.object.invite_via_field_uuid.present?, class: "base-input w-full #{'hidden' if ff.object.invite_by_uuid.present? || ff.object.optional_invite_by_uuid.present? || ff.object.invite_via_field_uuid.present?}" %> + <% if index == 1 %> + <% invite_fields = template.fields.select { |field| field['name'].present? && field['submitter_uuid'] != submitter['uuid'] } %> + + + <%= ff.select :option, [*(template.submitters - [submitter]).flat_map { |e| [[t('invite_by_name', name: e['name']), "invite_by_#{e['uuid']}"], [t('invite_by_name', name: e['name']) + " (#{t(:optional).capitalize})", "optional_invite_by_#{e['uuid']}"]] }, (invite_fields.present? ? [t('invite_via_form_field'), 'invite_via_field'] : nil)].compact, {}, class: 'base-select' %> + + <% if invite_fields.present? %> + + <%= select_tag ff.field_name(:invite_via_field_uuid), options_for_select(invite_fields.map { |field| [field['name'], field['uuid']] }, ff.object.invite_via_field_uuid), prompt: t(:select_field), class: 'base-select mt-3' %> + + <% end %> + + <% end %> <% else %> <% invite_fields = template.fields.select { |field| field['name'].present? && field['submitter_uuid'] != submitter['uuid'] } %> @@ -35,32 +48,49 @@ <%= tag.input name: ff.field_name(:email), type: :email, value: ff.object.email, multiple: true, class: "base-input #{'hidden' if item.option != 'email'}", autocomplete: 'off', placeholder: t('default_email'), id: email_field_uuid %> - - <%= select_tag ff.field_name(:invite_via_field_uuid), options_for_select(invite_fields.map { |field| [field['name'], field['uuid']] }, item.invite_via_field_uuid), prompt: t(:select_field), class: 'base-select',required: true %> - + <% if invite_fields.present? %> + + <%= select_tag ff.field_name(:invite_via_field_uuid), options_for_select(invite_fields.map { |field| [field['name'], field['uuid']] }, item.invite_via_field_uuid), prompt: t(:select_field), class: 'base-select', required: true %> + + <% end %> <% end %>
<% if template.submitters.size == 2 %> - <% if local_assigns[:with_submission_requester] != false %> + <% if index == 0 && local_assigns[:with_submission_requester] != false %> - <% end %> - <% if index == 1 %> + <% elsif index == 1 %> + <% if local_assigns[:with_submission_requester] != false %> + + + + + + <% end %> <% end %> From b6635fcc4f078e7de69a6676430c39efe4f7b760 Mon Sep 17 00:00:00 2001 From: Pete Matsyburka Date: Fri, 20 Feb 2026 22:25:12 +0200 Subject: [PATCH 32/54] change user only when root --- config/dotenv.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/dotenv.rb b/config/dotenv.rb index b17d9aac3..cb0f8e7c2 100644 --- a/config/dotenv.rb +++ b/config/dotenv.rb @@ -60,7 +60,7 @@ ENV['DATABASE_URL'] = ENV['DATABASE_URL'].to_s.empty? ? database_url : ENV.fetch('DATABASE_URL', nil) end - unless Process.euid == 2000 + if Process.uid.zero? begin test_file = "#{ENV.fetch('WORKDIR', '.')}/test" From f1d146eca3faad1193aafe3c6acf052241b757a3 Mon Sep 17 00:00:00 2001 From: Pete Matsyburka Date: Fri, 20 Feb 2026 22:29:39 +0200 Subject: [PATCH 33/54] fix typos --- app/javascript/submission_form/i18n.js | 2 +- app/javascript/template_builder/i18n.js | 2 +- config/locales/i18n.yml | 20 ++++++++++---------- 3 files changed, 12 insertions(+), 12 deletions(-) diff --git a/app/javascript/submission_form/i18n.js b/app/javascript/submission_form/i18n.js index 705679920..687215ff3 100644 --- a/app/javascript/submission_form/i18n.js +++ b/app/javascript/submission_form/i18n.js @@ -1,7 +1,7 @@ const en = { kba: 'KBA', please_upload_an_image_file: 'Please upload an image file', - must_be_characters_length: 'Must be {number} characters length', + must_be_characters_length: 'Must be {number} characters long', complete_all_required_fields_to_proceed_with_identity_verification: 'Complete all required fields to proceed with identity verification.', verify_id: 'Verify ID', identity_verification: 'Identity verification', diff --git a/app/javascript/template_builder/i18n.js b/app/javascript/template_builder/i18n.js index aea33d4be..51b45f78d 100644 --- a/app/javascript/template_builder/i18n.js +++ b/app/javascript/template_builder/i18n.js @@ -193,7 +193,7 @@ const en = { learn_more: 'Learn more', and: 'and', or: 'or', - start_a_quick_tour_to_learn_how_to_create_an_send_your_first_document: 'Start a quick tour to learn how to create an send your first document', + start_a_quick_tour_to_learn_how_to_create_an_send_your_first_document: 'Start a quick tour to learn how to create and send your first document', start_tour: 'Start Tour', or_add_from: 'Or add from', sync: 'Sync', diff --git a/config/locales/i18n.yml b/config/locales/i18n.yml index 1a5312c67..b50ae2705 100644 --- a/config/locales/i18n.yml +++ b/config/locales/i18n.yml @@ -218,7 +218,7 @@ en: &en copy: Copy copied: Copied rotate: Rotate - remove_existing_api_token_and_generated_a_new_one_are_you_sure_: Remove existing API token and generated a new one. Are you sure? + remove_existing_api_token_and_generated_a_new_one_are_you_sure_: Remove existing API token and generate a new one. Are you sure? request_signature_multiple_submitters_with_default_values: Request signature, multiple submitters with default values request_signature_single_submitter: Request signature, single submitter template_details: Template details @@ -330,8 +330,8 @@ en: &en initials: Initials update_initials: Update Initials unable_to_save_initials: Unable to save initials. - initials_has_been_saved: Initials has been saved. - initials_has_been_removed: Initials has been removed. + initials_has_been_saved: Initials have been saved. + initials_has_been_removed: Initials have been removed. change_password: Change Password two_factor_authentication: Two-Factor Authentication 2fa_is_not_configured: 2FA is not configured @@ -388,7 +388,7 @@ en: &en from: From account_sid: Account SID send_sms_via_webhook: Send SMS via webhook - webhook_integration_allows_to_send_sms_using_any_provider: Webhook integration allows to send SMS using any provider + webhook_integration_allows_to_send_sms_using_any_provider: Webhook integration allows you to send SMS using any provider test: Test single_sign_on_with_saml_2_0: Single Sign On with SAML 2.0 force_sso_disable_login_with_email_and_password: Force SSO (disable login with email and password) @@ -420,7 +420,7 @@ en: &en send_signature_request_emails_without_limits_with_docuseal_pro: Send signature request emails without limits with DocuSeal Pro count_emails_used: '%{count} emails used' has_been_connected: has been connected - sms_not_configured: SMS not Configure + sms_not_configured: SMS not Configured configure_sms_settings_in_order_to_send_text_messages_: 'Configure SMS settings in order to send text messages:' go_to_sms_settings: Go to SMS settings back_to_active: Back to Active @@ -597,7 +597,7 @@ en: &en upload_file: Upload file upgrade_your_plan_to_invite_more_users_contact_email: 'Upgrade your plan to invite more users (contact %{email}).' contact_your_admin_email_to_invite_more_users: 'Contact your admin %{email} to invite more users.' - contact_your_administrator_to_add_new_users: Contact your administrator to add new user + contact_your_administrator_to_add_new_users: Contact your administrator to add new users one_hour: 1 hour two_hours: 2 hours four_hours: 4 hours @@ -626,7 +626,7 @@ en: &en personalize_email_content: Personalize email content automated_reminders: Automated reminders bulk_send_from_spreadsheet: Bulk send from spreadsheet - identify_verification_via_sms: Identify verification via SMS + identify_verification_via_sms: Identity verification via SMS start_with_pro: Start with Pro user_month: user / month developer_sandbox: Developer Sandbox. @@ -738,7 +738,7 @@ en: &en find_suitable_zapier_templates_to_automate_your_workflow: Find suitable Zapier templates to automate your workflow. get_started: Get started click_here_to_learn_more_about_user_roles_and_permissions_html: '
Click here to learn more about user roles and permissions.' - count_10_signature_request_emails_sent_this_month_upgrade_to_pro_to_send_unlimited_signature_request_email: '%{count} / 10 signature request emails sent this month. Upgrade to Pro to send unlimited signature request email.' + count_10_signature_request_emails_sent_this_month_upgrade_to_pro_to_send_unlimited_signature_request_email: '%{count} / 10 signature request emails sent this month. Upgrade to Pro to send unlimited signature request emails.' test_mode_emails_limit_will_be_reset_within_24_hours: Test mode emails limit will be reset within 24 hours. on_a_scale_of_1_to_10_how_satisfied_are_you_with_the_docuseal_product_: On a scale of 1 to 10, how satisfied are you with the DocuSeal product? tell_us_more_about_your_experience: Tell us more about your experience @@ -756,7 +756,7 @@ en: &en manage_plan: Manage plan this_submission_has_multiple_signers_which_prevents_the_use_of_a_sharing_link_html: This submission has multiple signers, which prevents the use of a sharing link as it's unclear which signer is responsible for specific fields. To resolve this, follow this guide to define the default signer details. welcome_to_docuseal: Welcome to DocuSeal - start_a_quick_tour_to_learn_how_to_create_an_send_your_first_document: Start a quick tour to learn how to create an send your first document + start_a_quick_tour_to_learn_how_to_create_an_send_your_first_document: Start a quick tour to learn how to create and send your first document start_tour: Start Tour name_a_z: Name A-Z recently_used: Recently used @@ -816,7 +816,7 @@ en: &en connect_gmail_or_outlook: Connect Gmail or Outlook connect_your_email_to_bulk_send: Connect your email to bulk send connect_your_email_or_outlook_account_or_add_smtp_settings_to_bulk_send: Connect your Gmail or Outlook account or add SMTP settings to bulk send. - are_you_sure_you_want_to_add_recipients_without_sending_to_send_emails_it_requires_to_connect_gmail_or_outlook: Are you sure you want to add recipients without sending? To send emails it requires to connect Gmail or Outlook. + are_you_sure_you_want_to_add_recipients_without_sending_to_send_emails_it_requires_to_connect_gmail_or_outlook: Are you sure you want to add recipients without sending? To send emails you need to connect Gmail or Outlook. template_name_has_been_completed_by_submitters_html: '"{template.name}" has been completed by {submission.submitters}' please_check_the_copy_of_your_template_name_in_the_email_attachments_html: 'Please check the copy of your "{template.name}" in the email attachments.' you_have_been_invited_to_sign_the_template_name_html: 'You have been invited to sign the "{template.name}".' From 231fff5508f1bd8460c87256aae9dc9f6bf7bc33 Mon Sep 17 00:00:00 2001 From: Pete Matsyburka Date: Sat, 21 Feb 2026 09:53:03 +0200 Subject: [PATCH 34/54] canvas blocked message --- app/javascript/submission_form/i18n.js | 14 ++++++++++++ .../submission_form/initials_step.vue | 11 +++++++++- .../submission_form/signature_step.vue | 22 ++++++++++++++++--- .../submission_form/validate_signature.js | 22 ++++++++++++++++++- 4 files changed, 64 insertions(+), 5 deletions(-) diff --git a/app/javascript/submission_form/i18n.js b/app/javascript/submission_form/i18n.js index 687215ff3..b9da958de 100644 --- a/app/javascript/submission_form/i18n.js +++ b/app/javascript/submission_form/i18n.js @@ -97,6 +97,7 @@ const en = { upload: 'Upload', files: 'Files', signature_is_too_small_or_simple_please_redraw: 'Signature is too small or simple. Please redraw.', + browser_privacy_settings_block_canvas: 'Your browser privacy settings restrict use of the drawing canvas. Please use a different browser or device, or disable privacy settings that block canvas in order to sign.', wait_countdown_seconds: 'Wait {countdown} seconds' } @@ -199,6 +200,7 @@ const es = { upload: 'Subir', files: 'Archivos', signature_is_too_small_or_simple_please_redraw: 'La firma es demasiado pequeña o simple. Por favor, vuelve a dibujarla.', + browser_privacy_settings_block_canvas: 'La configuración de privacidad de su navegador restringe el uso del lienzo de dibujo. Utilice un navegador o dispositivo diferente, o desactive la configuración de privacidad que bloquea el lienzo para firmar.', wait_countdown_seconds: 'Espera {countdown} segundos' } @@ -301,6 +303,7 @@ const it = { upload: 'Carica', files: 'File', signature_is_too_small_or_simple_please_redraw: 'La firma è troppo piccola o semplice. Ridisegnala, per favore.', + browser_privacy_settings_block_canvas: 'Le impostazioni sulla privacy del browser limitano l\'uso dell\'area di disegno. Utilizza un browser o dispositivo diverso oppure disattiva le impostazioni sulla privacy che bloccano il canvas per firmare.', wait_countdown_seconds: 'Attendi {countdown} secondi' } @@ -403,6 +406,7 @@ const de = { upload: 'Hochladen', files: 'Dateien', signature_is_too_small_or_simple_please_redraw: 'Die Unterschrift ist zu klein oder zu einfach. Bitte neu zeichnen.', + browser_privacy_settings_block_canvas: 'Die Datenschutzeinstellungen Ihres Browsers schränken die Nutzung der Zeichenfläche ein. Bitte verwenden Sie einen anderen Browser oder ein anderes Gerät oder deaktivieren Sie die Datenschutzeinstellungen, die Canvas blockieren, um zu unterschreiben.', wait_countdown_seconds: 'Bitte {countdown} Sekunden warten' } @@ -505,6 +509,7 @@ const fr = { upload: 'Téléverser', files: 'Fichiers', signature_is_too_small_or_simple_please_redraw: 'La signature est trop petite ou trop simple. Veuillez la redessiner.', + browser_privacy_settings_block_canvas: 'Les paramètres de confidentialité de votre navigateur empêchent l\'utilisation du canevas de dessin. Veuillez utiliser un autre navigateur ou appareil, ou désactiver les paramètres de confidentialité qui bloquent le canevas pour signer.', wait_countdown_seconds: 'Veuillez patienter {countdown} secondes' } @@ -607,6 +612,7 @@ const pl = { upload: 'Przesyłanie', files: 'Pliki', signature_is_too_small_or_simple_please_redraw: 'Podpis jest zbyt mały lub zbyt prosty. Proszę narysować go ponownie.', + browser_privacy_settings_block_canvas: 'Ustawienia prywatności przeglądarki blokują użycie obszaru rysowania. Użyj innej przeglądarki lub urządzenia albo wyłącz ustawienia prywatności blokujące canvas, aby podpisać.', wait_countdown_seconds: 'Poczekaj {countdown} sekund' } @@ -709,6 +715,7 @@ const uk = { upload: 'Завантажити', files: 'Файли', signature_is_too_small_or_simple_please_redraw: 'Підпис занадто маленький або надто простий. Будь ласка, перемалюйте.', + browser_privacy_settings_block_canvas: 'Налаштування конфіденційності вашого браузера блокують використання полотна для малювання. Будь ласка, скористайтеся іншим браузером або пристроєм, або вимкніть налаштування конфіденційності, що блокують canvas, щоб підписати.', wait_countdown_seconds: 'Зачекайте {countdown} секунд' } @@ -811,6 +818,7 @@ const cs = { upload: 'Nahrát', files: 'Soubory', signature_is_too_small_or_simple_please_redraw: 'Podpis je příliš malý nebo jednoduchý. Nakreslete jej prosím znovu.', + browser_privacy_settings_block_canvas: 'Nastavení soukromí vašeho prohlížeče omezuje použití kreslicího plátna. Použijte prosím jiný prohlížeč nebo zařízení, nebo vypněte nastavení soukromí blokující canvas pro podepsání.', wait_countdown_seconds: 'Počkejte {countdown} sekund' } @@ -913,6 +921,7 @@ const pt = { upload: 'Carregar', files: 'Arquivos', signature_is_too_small_or_simple_please_redraw: 'A assinatura é muito pequena ou simples. Por favor, redesenhe.', + browser_privacy_settings_block_canvas: 'As configurações de privacidade do seu navegador restringem o uso da área de desenho. Use um navegador ou dispositivo diferente, ou desative as configurações de privacidade que bloqueiam o canvas para assinar.', wait_countdown_seconds: 'Aguarde {countdown} segundos' } @@ -1015,6 +1024,7 @@ const he = { upload: 'העלאה', files: 'קבצים', signature_is_too_small_or_simple_please_redraw: 'החתימה קטנה או פשוטה מדי. אנא חתום מחדש.', + browser_privacy_settings_block_canvas: 'הגדרות הפרטיות של הדפדפן שלך מגבילות את השימוש באזור הציור. אנא השתמש בדפדפן או מכשיר אחר, או בטל את הגדרות הפרטיות החוסמות canvas כדי לחתום.', wait_countdown_seconds: 'המתן {countdown} שניות' } @@ -1117,6 +1127,7 @@ const nl = { upload: 'Uploaden', files: 'Bestanden', signature_is_too_small_or_simple_please_redraw: 'De handtekening is te klein of te eenvoudig. Teken opnieuw.', + browser_privacy_settings_block_canvas: 'De privacyinstellingen van uw browser beperken het gebruik van het tekenveld. Gebruik een andere browser of ander apparaat, of schakel de privacyinstellingen uit die canvas blokkeren om te ondertekenen.', wait_countdown_seconds: 'Wacht {countdown} seconden' } @@ -1219,6 +1230,7 @@ const ar = { upload: 'تحميل', files: 'الملفات', signature_is_too_small_or_simple_please_redraw: 'التوقيع صغير جدًا أو بسيط جدًا. يرجى إعادة رسمه.', + browser_privacy_settings_block_canvas: 'إعدادات الخصوصية في متصفحك تمنع استخدام لوحة الرسم. يرجى استخدام متصفح أو جهاز مختلف، أو تعطيل إعدادات الخصوصية التي تحظر canvas للتوقيع.', wait_countdown_seconds: 'انتظر {countdown} ثانية' } @@ -1321,6 +1333,7 @@ const ko = { upload: '업로드', files: '파일', signature_is_too_small_or_simple_please_redraw: '서명이 너무 작거나 단순합니다. 다시 그려주세요.', + browser_privacy_settings_block_canvas: '브라우저 개인정보 보호 설정으로 인해 그리기 캔버스를 사용할 수 없습니다. 다른 브라우저나 기기를 사용하거나, 서명을 위해 캔버스를 차단하는 개인정보 보호 설정을 비활성화해 주세요.', wait_countdown_seconds: '{countdown}초 기다리세요' } @@ -1423,6 +1436,7 @@ const ja = { upload: 'アップロード', files: 'ファイル', signature_is_too_small_or_simple_please_redraw: '署名が小さすぎるか単純すぎます。もう一度描いてください。', + browser_privacy_settings_block_canvas: 'ブラウザのプライバシー設定により、描画キャンバスの使用が制限されています。別のブラウザまたはデバイスを使用するか、署名するためにキャンバスをブロックするプライバシー設定を無効にしてください。', wait_countdown_seconds: '{countdown} 秒お待ちください' } diff --git a/app/javascript/submission_form/initials_step.vue b/app/javascript/submission_form/initials_step.vue index 8e1cc1b50..814b9c122 100644 --- a/app/javascript/submission_form/initials_step.vue +++ b/app/javascript/submission_form/initials_step.vue @@ -150,6 +150,7 @@