diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 28f7e404..16adc9bd 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -107,6 +107,8 @@ jobs: node-version: 20.9.0 - name: Install Chrome uses: browser-actions/setup-chrome@latest + with: + chrome-version: 125 - name: Cache node_modules uses: actions/cache@v1 with: diff --git a/Dockerfile b/Dockerfile index 6c879d9d..022fc808 100644 --- a/Dockerfile +++ b/Dockerfile @@ -2,7 +2,7 @@ FROM ruby:3.3.3-alpine as fonts WORKDIR /fonts -RUN apk --no-cache add fontforge wget && wget https://github.com/satbyy/go-noto-universal/releases/download/v7.0/GoNotoKurrent-Regular.ttf && wget https://github.com/satbyy/go-noto-universal/releases/download/v7.0/GoNotoKurrent-Bold.ttf && wget https://github.com/impallari/DancingScript/raw/master/fonts/v2031/DancingScript-Regular.otf && wget https://cdn.jsdelivr.net/gh/notofonts/notofonts.github.io/fonts/NotoSansSymbols2/hinted/ttf/NotoSansSymbols2-Regular.ttf && wget https://github.com/Maxattax97/gnu-freefont/raw/master/ttf/FreeSans.ttf && wget https://github.com/impallari/DancingScript/raw/master/OFL.txt +RUN apk --no-cache add fontforge wget && wget https://github.com/satbyy/go-noto-universal/releases/download/v7.0/GoNotoKurrent-Regular.ttf && wget https://github.com/satbyy/go-noto-universal/releases/download/v7.0/GoNotoKurrent-Bold.ttf && wget https://github.com/impallari/DancingScript/raw/master/fonts/DancingScript-Regular.otf && wget https://cdn.jsdelivr.net/gh/notofonts/notofonts.github.io/fonts/NotoSansSymbols2/hinted/ttf/NotoSansSymbols2-Regular.ttf && wget https://github.com/Maxattax97/gnu-freefont/raw/master/ttf/FreeSans.ttf && wget https://github.com/impallari/DancingScript/raw/master/OFL.txt RUN fontforge -lang=py -c 'font1 = fontforge.open("FreeSans.ttf"); font2 = fontforge.open("NotoSansSymbols2-Regular.ttf"); font1.mergeFonts(font2); font1.generate("FreeSans.ttf")' diff --git a/Gemfile b/Gemfile index 5600e361..7c68b0a4 100644 --- a/Gemfile +++ b/Gemfile @@ -6,6 +6,7 @@ ruby '3.3.3' gem 'arabic-letter-connector', require: 'arabic-letter-connector/logic' gem 'aws-sdk-s3', require: false +gem 'aws-sdk-secretsmanager', require: false gem 'azure-storage-blob', require: false gem 'bootsnap', require: false gem 'cancancan' @@ -22,6 +23,7 @@ gem 'image_processing' gem 'jwt' gem 'lograge' gem 'mysql2', require: false +gem 'net-smtp', '0.4.0' gem 'oj' gem 'pagy' gem 'pg', require: false diff --git a/Gemfile.lock b/Gemfile.lock index 10dc6621..24337766 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,35 +1,35 @@ GEM remote: https://rubygems.org/ specs: - actioncable (7.1.3.2) - actionpack (= 7.1.3.2) - activesupport (= 7.1.3.2) + actioncable (7.1.3.4) + actionpack (= 7.1.3.4) + activesupport (= 7.1.3.4) nio4r (~> 2.0) websocket-driver (>= 0.6.1) zeitwerk (~> 2.6) - actionmailbox (7.1.3.2) - actionpack (= 7.1.3.2) - activejob (= 7.1.3.2) - activerecord (= 7.1.3.2) - activestorage (= 7.1.3.2) - activesupport (= 7.1.3.2) + actionmailbox (7.1.3.4) + actionpack (= 7.1.3.4) + activejob (= 7.1.3.4) + activerecord (= 7.1.3.4) + activestorage (= 7.1.3.4) + activesupport (= 7.1.3.4) mail (>= 2.7.1) net-imap net-pop net-smtp - actionmailer (7.1.3.2) - actionpack (= 7.1.3.2) - actionview (= 7.1.3.2) - activejob (= 7.1.3.2) - activesupport (= 7.1.3.2) + actionmailer (7.1.3.4) + actionpack (= 7.1.3.4) + actionview (= 7.1.3.4) + activejob (= 7.1.3.4) + activesupport (= 7.1.3.4) mail (~> 2.5, >= 2.5.4) net-imap net-pop net-smtp rails-dom-testing (~> 2.2) - actionpack (7.1.3.2) - actionview (= 7.1.3.2) - activesupport (= 7.1.3.2) + actionpack (7.1.3.4) + actionview (= 7.1.3.4) + activesupport (= 7.1.3.4) nokogiri (>= 1.8.5) racc rack (>= 2.2.4) @@ -37,35 +37,35 @@ GEM rack-test (>= 0.6.3) rails-dom-testing (~> 2.2) rails-html-sanitizer (~> 1.6) - actiontext (7.1.3.2) - actionpack (= 7.1.3.2) - activerecord (= 7.1.3.2) - activestorage (= 7.1.3.2) - activesupport (= 7.1.3.2) + actiontext (7.1.3.4) + actionpack (= 7.1.3.4) + activerecord (= 7.1.3.4) + activestorage (= 7.1.3.4) + activesupport (= 7.1.3.4) globalid (>= 0.6.0) nokogiri (>= 1.8.5) - actionview (7.1.3.2) - activesupport (= 7.1.3.2) + actionview (7.1.3.4) + activesupport (= 7.1.3.4) builder (~> 3.1) erubi (~> 1.11) rails-dom-testing (~> 2.2) rails-html-sanitizer (~> 1.6) - activejob (7.1.3.2) - activesupport (= 7.1.3.2) + activejob (7.1.3.4) + activesupport (= 7.1.3.4) globalid (>= 0.3.6) - activemodel (7.1.3.2) - activesupport (= 7.1.3.2) - activerecord (7.1.3.2) - activemodel (= 7.1.3.2) - activesupport (= 7.1.3.2) + activemodel (7.1.3.4) + activesupport (= 7.1.3.4) + activerecord (7.1.3.4) + activemodel (= 7.1.3.4) + activesupport (= 7.1.3.4) timeout (>= 0.4.0) - activestorage (7.1.3.2) - actionpack (= 7.1.3.2) - activejob (= 7.1.3.2) - activerecord (= 7.1.3.2) - activesupport (= 7.1.3.2) + activestorage (7.1.3.4) + actionpack (= 7.1.3.4) + activejob (= 7.1.3.4) + activerecord (= 7.1.3.4) + activesupport (= 7.1.3.4) marcel (~> 1.0) - activesupport (7.1.3.2) + activesupport (7.1.3.4) base64 bigdecimal concurrent-ruby (~> 1.0, >= 1.0.2) @@ -96,6 +96,9 @@ GEM aws-sdk-core (~> 3, >= 3.191.0) aws-sdk-kms (~> 1) aws-sigv4 (~> 1.8) + aws-sdk-secretsmanager (1.91.0) + aws-sdk-core (~> 3, >= 3.191.0) + aws-sigv4 (~> 1.1) aws-sigv4 (1.8.0) aws-eventstream (~> 1, >= 1.0.2) azure-storage-blob (2.0.3) @@ -119,7 +122,7 @@ GEM bindex (0.8.1) bootsnap (1.18.3) msgpack (~> 1.2) - builder (3.2.4) + builder (3.3.0) bullet (7.1.6) activesupport (>= 3.0.0) uniform_notifier (~> 1.11) @@ -139,7 +142,7 @@ GEM cldr-plurals-runtime-rb (1.1.0) cmdparse (3.0.7) coderay (1.1.3) - concurrent-ruby (1.2.3) + concurrent-ruby (1.3.4) connection_pool (2.4.1) crack (1.0.0) bigdecimal @@ -181,7 +184,7 @@ GEM rainbow rubocop smart_properties - erubi (1.12.0) + erubi (1.13.0) factory_bot (6.4.6) activesupport (>= 5.0.0) factory_bot_rails (6.4.3) @@ -308,8 +311,8 @@ GEM method_source (1.1.0) mini_magick (4.12.0) mini_mime (1.1.5) - mini_portile2 (2.8.6) - minitest (5.23.0) + mini_portile2 (2.8.7) + minitest (5.24.1) msgpack (1.7.2) multi_json (1.15.0) multipart-post (2.4.0) @@ -317,22 +320,21 @@ GEM mysql2 (0.5.6) net-http-persistent (4.0.2) connection_pool (~> 2.2) - net-imap (0.4.10) + net-imap (0.4.14) date net-protocol net-pop (0.1.2) - net-protocol net-protocol (0.2.2) timeout net-smtp (0.4.0) net-protocol nio4r (2.7.1) - nokogiri (1.16.5) + nokogiri (1.16.7) mini_portile2 (~> 2.8.2) racc (~> 1.4) - nokogiri (1.16.5-aarch64-linux) + nokogiri (1.16.7-aarch64-linux) racc (~> 1.4) - nokogiri (1.16.5-x86_64-linux) + nokogiri (1.16.7-x86_64-linux) racc (~> 1.4) oj (3.16.3) bigdecimal (>= 3.0) @@ -366,8 +368,8 @@ GEM public_suffix (5.0.5) puma (6.4.2) nio4r (~> 2.0) - racc (1.8.0) - rack (3.0.11) + racc (1.8.1) + rack (3.1.7) rack-proxy (0.7.7) rack rack-session (2.0.0) @@ -377,20 +379,20 @@ GEM rackup (2.1.0) rack (>= 3) webrick (~> 1.8) - rails (7.1.3.2) - actioncable (= 7.1.3.2) - actionmailbox (= 7.1.3.2) - actionmailer (= 7.1.3.2) - actionpack (= 7.1.3.2) - actiontext (= 7.1.3.2) - actionview (= 7.1.3.2) - activejob (= 7.1.3.2) - activemodel (= 7.1.3.2) - activerecord (= 7.1.3.2) - activestorage (= 7.1.3.2) - activesupport (= 7.1.3.2) + rails (7.1.3.4) + actioncable (= 7.1.3.4) + actionmailbox (= 7.1.3.4) + actionmailer (= 7.1.3.4) + actionpack (= 7.1.3.4) + actiontext (= 7.1.3.4) + actionview (= 7.1.3.4) + activejob (= 7.1.3.4) + activemodel (= 7.1.3.4) + activerecord (= 7.1.3.4) + activestorage (= 7.1.3.4) + activesupport (= 7.1.3.4) bundler (>= 1.15.0) - railties (= 7.1.3.2) + railties (= 7.1.3.4) rails-dom-testing (2.2.0) activesupport (>= 5.0.0) minitest @@ -405,9 +407,9 @@ GEM actionview (> 3.1) activesupport (> 3.1) railties (> 3.1) - railties (7.1.3.2) - actionpack (= 7.1.3.2) - activesupport (= 7.1.3.2) + railties (7.1.3.4) + actionpack (= 7.1.3.4) + activesupport (= 7.1.3.4) irb rackup (>= 1.0.0) rake (>= 12.2) @@ -417,7 +419,7 @@ GEM rake (13.2.1) rdoc (6.6.3.1) psych (>= 4.0.0) - redis-client (0.22.0) + redis-client (0.22.2) connection_pool regexp_parser (2.9.2) reline (0.5.7) @@ -432,7 +434,7 @@ GEM actionpack (>= 5.2) railties (>= 5.2) retriable (3.1.2) - rexml (3.3.2) + rexml (3.3.3) strscan rotp (6.3.0) rqrcode (2.2.0) @@ -494,7 +496,7 @@ GEM rack-proxy (>= 0.6.1) railties (>= 5.2) semantic_range (>= 2.3.0) - sidekiq (7.2.2) + sidekiq (7.2.4) concurrent-ruby (< 2) connection_pool (>= 2.3.0) rack (>= 2.2.4) @@ -563,6 +565,7 @@ DEPENDENCIES annotate arabic-letter-connector aws-sdk-s3 + aws-sdk-secretsmanager azure-storage-blob better_html bootsnap @@ -588,6 +591,7 @@ DEPENDENCIES letter_opener_web lograge mysql2 + net-smtp (= 0.4.0) oj pagy pg diff --git a/app/controllers/account_configs_controller.rb b/app/controllers/account_configs_controller.rb index de3e1826..0b67f714 100644 --- a/app/controllers/account_configs_controller.rb +++ b/app/controllers/account_configs_controller.rb @@ -14,7 +14,8 @@ class AccountConfigsController < ApplicationController AccountConfig::DOWNLOAD_LINKS_AUTH_KEY, AccountConfig::FORCE_SSO_AUTH_KEY, AccountConfig::FLATTEN_RESULT_PDF_KEY, - AccountConfig::WITH_SIGNATURE_ID + AccountConfig::WITH_SIGNATURE_ID, + AccountConfig::REQUIRE_SIGNING_REASON_KEY ].freeze InvalidKey = Class.new(StandardError) diff --git a/app/controllers/api/submission_events_controller.rb b/app/controllers/api/submission_events_controller.rb new file mode 100644 index 00000000..1a32023b --- /dev/null +++ b/app/controllers/api/submission_events_controller.rb @@ -0,0 +1,52 @@ +# frozen_string_literal: true + +module Api + class SubmissionEventsController < ApiBaseController + load_and_authorize_resource :submission, parent: false + + def index + submissions = build_completed_query(@submissions) + + params[:after] = Time.zone.at(params[:after].to_i) if params[:after].present? + params[:before] = Time.zone.at(params[:before].to_i) if params[:before].present? + + submissions = paginate(submissions.preload( + :created_by_user, :submission_events, + template: :folder, + submitters: { documents_attachments: :blob, attachments_attachments: :blob }, + audit_trail_attachment: :blob + ), + field: :completed_at) + + render json: { + data: submissions.map do |s| + { + event_type: 'submission.completed', + timestamp: s.completed_at, + data: Submissions::SerializeForApi.call(s, s.submitters) + } + end, + pagination: { + count: submissions.size, + next: submissions.last&.completed_at&.to_i, + prev: submissions.first&.completed_at&.to_i + } + } + end + + private + + def build_completed_query(submissions) + submissions = submissions.where( + Submitter.where(completed_at: nil).where( + Submitter.arel_table[:submission_id].eq(Submission.arel_table[:id]) + ).select(1).arel.exists.not + ) + + submissions.joins(:submitters) + .group(:id) + .select(Submission.arel_table[Arel.star], + Submitter.arel_table[:completed_at].maximum.as('completed_at')) + end + end +end diff --git a/app/controllers/api/templates_controller.rb b/app/controllers/api/templates_controller.rb index cecb193c..7341b916 100644 --- a/app/controllers/api/templates_controller.rb +++ b/app/controllers/api/templates_controller.rb @@ -95,6 +95,7 @@ module Api def template_params permitted_params = [ :name, + :external_id, { submitters: [%i[name uuid]], fields: [[:uuid, :submitter_uuid, :name, :type, diff --git a/app/controllers/api/tools_controller.rb b/app/controllers/api/tools_controller.rb index 9eaa1486..32c59808 100644 --- a/app/controllers/api/tools_controller.rb +++ b/app/controllers/api/tools_controller.rb @@ -14,5 +14,29 @@ module Api data: Base64.encode64(PdfUtils.merge(files.map { |base64| StringIO.new(Base64.decode64(base64)) }).string) } end + + def verify + file = Base64.decode64(params[:file]) + pdf = HexaPDF::Document.new(io: StringIO.new(file)) + + trusted_certs = Accounts.load_trusted_certs(current_account) + + is_checksum_found = ActiveStorage::Attachment.joins(:blob) + .where(name: 'documents', record_type: 'Submitter') + .exists?(blob: { checksum: Digest::MD5.base64digest(file) }) + + render json: { + checksum_status: is_checksum_found ? 'verified' : 'not_found', + signatures: pdf.signatures.map do |sig| + { + verification_result: sig.verify(trusted_certs:).messages, + signer_name: sig.signer_name, + signing_reason: sig.signing_reason, + signing_time: sig.signing_time, + signature_type: sig.signature_type + } + end + } + end end end diff --git a/app/controllers/start_form_controller.rb b/app/controllers/start_form_controller.rb index 3d73410d..31fc5746 100644 --- a/app/controllers/start_form_controller.rb +++ b/app/controllers/start_form_controller.rb @@ -82,6 +82,8 @@ class StartFormController < ApplicationController template_submitters: template.submitters, source: :link) + submitter.account_id = submitter.submission.account_id + submitter end diff --git a/app/controllers/verify_pdf_signature_controller.rb b/app/controllers/verify_pdf_signature_controller.rb index dbd39021..69e6098b 100644 --- a/app/controllers/verify_pdf_signature_controller.rb +++ b/app/controllers/verify_pdf_signature_controller.rb @@ -9,26 +9,7 @@ class VerifyPdfSignatureController < ApplicationController HexaPDF::Document.new(io: file.open) end - cert_data = - if Docuseal.multitenant? - value = EncryptedConfig.find_by(account: current_account, key: EncryptedConfig::ESIGN_CERTS_KEY)&.value || {} - - Docuseal::CERTS.merge(value) - else - EncryptedConfig.find_by(key: EncryptedConfig::ESIGN_CERTS_KEY)&.value || {} - end - - default_pkcs = GenerateCertificate.load_pkcs(cert_data) - - custom_certs = cert_data.fetch('custom', []).map do |e| - OpenSSL::PKCS12.new(Base64.urlsafe_decode64(e['data']), e['password'].to_s) - end - - trusted_certs = [default_pkcs.certificate, - *default_pkcs.ca_certs, - *custom_certs.map(&:certificate), - *custom_certs.flat_map(&:ca_certs).compact, - *Docuseal.trusted_certs] + trusted_certs = Accounts.load_trusted_certs(current_account) render turbo_stream: turbo_stream.replace('result', partial: 'result', locals: { pdfs:, files: params[:files], trusted_certs: }) diff --git a/app/controllers/webhook_secret_controller.rb b/app/controllers/webhook_secret_controller.rb new file mode 100644 index 00000000..12c1edae --- /dev/null +++ b/app/controllers/webhook_secret_controller.rb @@ -0,0 +1,29 @@ +# frozen_string_literal: true + +class WebhookSecretController < ApplicationController + before_action :load_encrypted_config + authorize_resource :encrypted_config, parent: false + + def index; end + + def create + @encrypted_config.assign_attributes(value: { + encrypted_config_params[:key] => encrypted_config_params[:value] + }.compact_blank) + + @encrypted_config.value.present? ? @encrypted_config.save! : @encrypted_config.delete + + redirect_back(fallback_location: settings_webhooks_path, notice: 'Webhook Secret has been saved.') + end + + private + + def load_encrypted_config + @encrypted_config = + current_account.encrypted_configs.find_or_initialize_by(key: EncryptedConfig::WEBHOOK_SECRET_KEY) + end + + def encrypted_config_params + params.require(:encrypted_config).permit(value: %i[key value]).fetch(:value, {}) + end +end diff --git a/app/javascript/application.js b/app/javascript/application.js index 2eaba9c4..9690f241 100644 --- a/app/javascript/application.js +++ b/app/javascript/application.js @@ -25,6 +25,7 @@ import PromptPassword from './elements/prompt_password' import EmailsTextarea from './elements/emails_textarea' import ToggleOnSubmit from './elements/toggle_on_submit' import PasswordInput from './elements/password_input' +import SearchInput from './elements/search_input' import * as TurboInstantClick from './lib/turbo_instant_click' @@ -87,6 +88,7 @@ safeRegisterElement('emails-textarea', EmailsTextarea) safeRegisterElement('toggle-cookies', ToggleCookies) safeRegisterElement('toggle-on-submit', ToggleOnSubmit) safeRegisterElement('password-input', PasswordInput) +safeRegisterElement('search-input', SearchInput) safeRegisterElement('template-builder', class extends HTMLElement { connectedCallback () { @@ -97,6 +99,7 @@ safeRegisterElement('template-builder', class extends HTMLElement { this.app = createApp(TemplateBuilder, { template: reactive(JSON.parse(this.dataset.template)), backgroundColor: '#faf7f5', + locale: this.dataset.locale, withPhone: this.dataset.withPhone === 'true', withLogo: this.dataset.withLogo !== 'false', editable: this.dataset.editable !== 'false', diff --git a/app/javascript/elements/search_input.js b/app/javascript/elements/search_input.js new file mode 100644 index 00000000..9b536ab3 --- /dev/null +++ b/app/javascript/elements/search_input.js @@ -0,0 +1,25 @@ +export default class extends HTMLElement { + connectedCallback () { + this.input.addEventListener('focus', () => { + if (this.title) { + this.title.classList.add('hidden', 'md:block') + this.input.classList.add('w-60') + } + }) + + this.input.addEventListener('blur', (e) => { + if (this.title && !e.target.value) { + this.title.classList.remove('hidden') + this.input.classList.remove('w-60') + } + }) + } + + get input () { + return this.querySelector('input') + } + + get title () { + return document.querySelector(this.dataset.title) + } +} diff --git a/app/javascript/form.js b/app/javascript/form.js index bc8d5e7e..3ee6f373 100644 --- a/app/javascript/form.js +++ b/app/javascript/form.js @@ -23,6 +23,7 @@ safeRegisterElement('submission-form', class extends HTMLElement { dryRun: this.dataset.dryRun === 'true', expand: ['true', 'false'].includes(this.dataset.expand) ? this.dataset.expand === 'true' : null, withSignatureId: this.dataset.withSignatureId === 'true', + requireSigningReason: this.dataset.requireSigningReason === 'true', withConfetti: this.dataset.withConfetti !== 'false', withDisclosure: this.dataset.withDisclosure === 'true', withTypedSignature: this.dataset.withTypedSignature !== 'false', diff --git a/app/javascript/submission_form/area.vue b/app/javascript/submission_form/area.vue index 814eb949..e7596862 100644 --- a/app/javascript/submission_form/area.vue +++ b/app/javascript/submission_form/area.vue @@ -75,7 +75,7 @@ ID: {{ signature.uuid }}
- {{ t('reason') }}: {{ t('digitally_signed_by') }} {{ submitter.name }} + {{ t('reason') }}: {{ values[field.preferences?.reason_field_uuid] || t('digitally_signed_by') }} {{ submitter.name }} @@ -258,6 +258,11 @@ export default { required: false, default: '' }, + values: { + type: Object, + required: false, + default: () => ({}) + }, isActive: { type: Boolean, required: false, diff --git a/app/javascript/submission_form/areas.vue b/app/javascript/submission_form/areas.vue index 6e68efd0..7f72a629 100644 --- a/app/javascript/submission_form/areas.vue +++ b/app/javascript/submission_form/areas.vue @@ -1,6 +1,6 @@