diff --git a/.github/workflows/docker-gar.yml b/.github/workflows/docker-gar.yml new file mode 100644 index 000000000..37e415d31 --- /dev/null +++ b/.github/workflows/docker-gar.yml @@ -0,0 +1,48 @@ +name: Build and Push to Google Artifact Registry + +on: + push: + tags: + - "*.*.*" + branches: + - kencove + +permissions: + contents: read + id-token: write + +jobs: + build: + runs-on: ubuntu-latest + timeout-minutes: 5 + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Authenticate to Google Cloud + id: auth + uses: google-github-actions/auth@v2 + with: + workload_identity_provider: ${{ secrets.GCP_WORKLOAD_IDENTITY_PROVIDER }} + service_account: ${{ secrets.GCP_SERVICE_ACCOUNT }} + + - name: Set up Cloud SDK + uses: google-github-actions/setup-gcloud@v2 + + - name: Determine image tag + id: tag + run: | + if [[ "${{ github.ref_type }}" == "tag" ]]; then + echo "img_tag=${{ github.ref_name }}" >> "$GITHUB_OUTPUT" + else + echo "img_tag=$(echo ${{ github.sha }} | cut -c1-8)" >> "$GITHUB_OUTPUT" + fi + + - name: Trigger Cloud Build + run: | + gcloud builds submit \ + --config=cloudbuild.yaml \ + --project=kencove-prod \ + --substitutions=_IMG_TAG=${{ steps.tag.outputs.img_tag }} \ + --async diff --git a/.rubocop.yml b/.rubocop.yml index bda7960e8..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 @@ -106,3 +106,10 @@ Rails/StrongParametersExpect: Rails/RedirectBackOrTo: Enabled: false + +Rails/UnknownEnv: + Environments: + - development + - test + - production + - local 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 diff --git a/Gemfile b/Gemfile index 61c848072..89a568bb2 100644 --- a/Gemfile +++ b/Gemfile @@ -25,6 +25,8 @@ gem 'jwt', require: false gem 'lograge' gem 'numo-narray-alt', require: false gem 'oj' +gem 'omniauth-google-oauth2' +gem 'omniauth-rails_csrf_protection' gem 'onnxruntime', require: false gem 'pagy' gem 'pg', require: false @@ -33,8 +35,8 @@ gem 'pretender' gem 'puma', require: false gem 'rack' gem 'rails' -gem 'rails_autolink' gem 'rails-i18n' +gem 'redis' gem 'rotp' gem 'rouge', require: false gem 'rqrcode', require: false diff --git a/Gemfile.lock b/Gemfile.lock index 962c2364a..3e06d3948 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,6 +1,6 @@ GIT remote: https://github.com/trilogy-libraries/trilogy.git - revision: 3963d490459df7a2b5bedb42424c3285f25eab22 + revision: 3fb9d2a06bfc5ac8c2ac6af918f0b7dbd2d9f630 glob: contrib/ruby/*.gemspec specs: trilogy (2.10.0) @@ -83,16 +83,16 @@ GEM securerandom (>= 0.3) tzinfo (~> 2.0, >= 2.0.5) uri (>= 0.13.1) - addressable (2.8.8) + addressable (2.8.9) public_suffix (>= 2.0.2, < 8.0) - annotaterb (4.20.0) + annotaterb (4.22.0) activerecord (>= 6.0.0) activesupport (>= 6.0.0) arabic-letter-connector (0.1.1) ast (2.4.3) aws-eventstream (1.4.0) - aws-partitions (1.1209.0) - aws-sdk-core (3.241.4) + aws-partitions (1.1220.0) + aws-sdk-core (3.242.0) aws-eventstream (~> 1, >= 1.3.0) aws-partitions (~> 1, >= 1.992.0) aws-sigv4 (~> 1.9) @@ -100,10 +100,10 @@ GEM bigdecimal jmespath (~> 1, >= 1.6.1) logger - aws-sdk-kms (1.121.0) + aws-sdk-kms (1.122.0) aws-sdk-core (~> 3, >= 3.241.4) aws-sigv4 (~> 1.5) - aws-sdk-s3 (1.212.0) + aws-sdk-s3 (1.213.0) aws-sdk-core (~> 3, >= 3.241.4) aws-sdk-kms (~> 1) aws-sigv4 (~> 1.5) @@ -126,9 +126,9 @@ GEM smart_properties bigdecimal (4.0.1) bindex (0.8.1) - bootsnap (1.21.1) + bootsnap (1.23.0) msgpack (~> 1.2) - brakeman (7.1.2) + brakeman (8.0.4) racc builder (3.3.0) bullet (8.1.0) @@ -158,7 +158,7 @@ GEM bigdecimal rexml crass (1.0.6) - css_parser (1.21.1) + css_parser (2.0.0) addressable csv (3.3.5) csv-safe (3.3.1) @@ -171,16 +171,16 @@ GEM irb (~> 1.10) reline (>= 0.3.8) declarative (0.0.20) - devise (4.9.4) + devise (5.0.2) bcrypt (~> 3.0) orm_adapter (~> 0.1) - railties (>= 4.1.0) + railties (>= 7.0) responders warden (~> 1.2.3) - devise-two-factor (6.3.1) - activesupport (>= 7.0, < 8.2) - devise (>= 4.0, < 5.0) - railties (>= 7.0, < 8.2) + devise-two-factor (6.4.0) + activesupport (>= 7.2, < 8.2) + devise (>= 4.0, < 6.0) + railties (>= 7.2, < 8.2) rotp (~> 6.0) diff-lcs (1.6.2) digest-crc (0.7.0) @@ -189,7 +189,7 @@ GEM dotenv (3.2.0) drb (2.2.3) email_typo (0.2.3) - erb (6.0.1) + erb (6.0.2) erb_lint (0.9.0) activesupport better_html (>= 2.0.1) @@ -219,7 +219,6 @@ GEM concurrent-ruby (~> 1.1) webrick (~> 1.7) websocket-driver (~> 0.7) - ffi (1.17.3) ffi (1.17.3-aarch64-linux-gnu) ffi (1.17.3-aarch64-linux-musl) ffi (1.17.3-arm64-darwin) @@ -240,7 +239,7 @@ GEM retriable (~> 3.1) google-apis-iamcredentials_v1 (0.26.0) google-apis-core (>= 0.15.0, < 2.a) - google-apis-storage_v1 (0.59.0) + google-apis-storage_v1 (0.61.0) google-apis-core (>= 0.15.0, < 2.a) google-cloud-core (1.8.0) google-cloud-env (>= 1.0, < 3.a) @@ -259,7 +258,7 @@ GEM googleauth (~> 1.9) mini_mime (~> 1.0) google-logging-utils (0.2.0) - googleauth (1.16.1) + googleauth (1.16.2) faraday (>= 1.0, < 3.a) google-cloud-env (~> 2.2) google-logging-utils (~> 0.1) @@ -268,7 +267,9 @@ GEM os (>= 0.9, < 2.0) signet (>= 0.16, < 2.a) hashdiff (1.2.1) - hexapdf (1.5.0) + hashie (5.1.0) + logger + hexapdf (1.6.0) cmdparse (~> 3.0, >= 3.0.3) geom2d (~> 0.4, >= 0.4.1) openssl (>= 2.2.1) @@ -280,12 +281,16 @@ GEM mini_magick (>= 4.9.5, < 6) ruby-vips (>= 2.0.17, < 3) io-console (0.8.2) - irb (1.16.0) + irb (1.17.0) pp (>= 0.6.0) + prism (>= 1.3.0) rdoc (>= 4.0.0) reline (>= 0.4.2) jmespath (1.6.2) json (2.18.1) + json-schema (6.1.0) + addressable (~> 2.8) + bigdecimal (>= 3.1, < 5) jwt (3.1.2) base64 language_server-protocol (3.17.0.5) @@ -318,17 +323,22 @@ GEM net-smtp marcel (1.1.0) matrix (0.4.3) + mcp (0.7.1) + json-schema (>= 4.1) method_source (1.1.0) mini_magick (5.3.1) logger mini_mime (1.1.5) - minitest (6.0.1) + minitest (6.0.2) + drb (~> 2.0) prism (~> 1.5) msgpack (1.8.0) multi_json (1.19.1) + multi_xml (0.8.1) + bigdecimal (>= 3.1, < 5) net-http (0.9.1) uri (>= 0.11.1) - net-imap (0.6.2) + net-imap (0.6.3) date net-protocol net-pop (0.1.2) @@ -338,41 +348,63 @@ GEM net-smtp (0.5.1) net-protocol nio4r (2.7.5) - nokogiri (1.19.0-aarch64-linux-gnu) + nokogiri (1.19.1-aarch64-linux-gnu) racc (~> 1.4) - nokogiri (1.19.0-aarch64-linux-musl) + nokogiri (1.19.1-aarch64-linux-musl) racc (~> 1.4) - nokogiri (1.19.0-arm64-darwin) + nokogiri (1.19.1-arm64-darwin) racc (~> 1.4) - nokogiri (1.19.0-x86_64-linux-gnu) + nokogiri (1.19.1-x86_64-linux-gnu) racc (~> 1.4) - nokogiri (1.19.0-x86_64-linux-musl) + nokogiri (1.19.1-x86_64-linux-musl) racc (~> 1.4) - numo-narray-alt (0.9.13) - oj (3.16.13) + numo-narray-alt (0.10.3) + oauth2 (2.0.18) + faraday (>= 0.17.3, < 4.0) + jwt (>= 1.0, < 4.0) + logger (~> 1.2) + multi_xml (~> 0.5) + rack (>= 1.2, < 4) + snaky_hash (~> 2.0, >= 2.0.3) + version_gem (~> 1.1, >= 1.1.9) + oj (3.16.15) bigdecimal (>= 3.0) ostruct (>= 0.2) - onnxruntime (0.10.1) - ffi - onnxruntime (0.10.1-aarch64-linux) + omniauth (2.1.4) + hashie (>= 3.4.6) + logger + rack (>= 2.2.3) + rack-protection + omniauth-google-oauth2 (1.2.2) + jwt (>= 2.9.2) + oauth2 (~> 2.0) + omniauth (~> 2.0) + omniauth-oauth2 (~> 1.8) + omniauth-oauth2 (1.9.0) + oauth2 (>= 2.0.2, < 3) + omniauth (~> 2.0) + omniauth-rails_csrf_protection (2.0.1) + actionpack (>= 4.2) + omniauth (~> 2.0) + onnxruntime (0.11.0-aarch64-linux) ffi - onnxruntime (0.10.1-arm64-darwin) + onnxruntime (0.11.0-arm64-darwin) ffi - onnxruntime (0.10.1-x86_64-linux) + onnxruntime (0.11.0-x86_64-linux) ffi - openssl (4.0.0) + openssl (4.0.1) orm_adapter (0.5.0) os (1.1.4) ostruct (0.6.3) package_json (0.2.0) - pagy (43.2.8) + pagy (43.3.1) json + uri yaml parallel (1.27.0) - parser (3.3.10.1) + parser (3.3.10.2) ast (~> 2.4.1) racc - pg (1.6.3) pg (1.6.3-aarch64-linux) pg (1.6.3-aarch64-linux-musl) pg (1.6.3-arm64-darwin) @@ -380,7 +412,7 @@ GEM pg (1.6.3-x86_64-linux-musl) pp (0.6.3) prettyprint - premailer (1.27.0) + premailer (1.28.0) addressable css_parser (>= 1.19.0) htmlentities (>= 4.0.0) @@ -391,7 +423,7 @@ GEM pretender (0.6.0) actionpack (>= 7.1) prettyprint (0.2.0) - prism (1.8.0) + prism (1.9.0) pry (0.16.0) coderay (~> 1.1) method_source (~> 1.0) @@ -405,7 +437,11 @@ GEM puma (7.2.0) nio4r (~> 2.0) racc (1.8.1) - rack (3.2.4) + rack (3.2.5) + rack-protection (4.2.1) + base64 (>= 0.1.0) + logger (>= 1.6.0) + rack (>= 3.0.0, < 4) rack-proxy (0.7.7) rack rack-session (2.1.1) @@ -433,16 +469,12 @@ GEM activesupport (>= 5.0.0) minitest nokogiri (>= 1.6) - rails-html-sanitizer (1.6.2) - loofah (~> 2.21) + rails-html-sanitizer (1.7.0) + loofah (~> 2.25) nokogiri (>= 1.15.7, != 1.16.7, != 1.16.6, != 1.16.5, != 1.16.4, != 1.16.3, != 1.16.2, != 1.16.1, != 1.16.0.rc1, != 1.16.0) rails-i18n (8.1.0) i18n (>= 0.7, < 2) railties (>= 8.0.0, < 9) - rails_autolink (1.1.8) - actionview (> 3.1) - activesupport (> 3.1) - railties (> 3.1) railties (8.1.2) actionpack (= 8.1.2) activesupport (= 8.1.2) @@ -454,10 +486,12 @@ GEM zeitwerk (~> 2.6) rainbow (3.1.1) rake (13.3.1) - rdoc (7.1.0) + rdoc (7.2.0) erb psych (>= 4.0.0) tsort + redis (5.4.1) + redis-client (>= 0.22.0) redis-client (0.26.4) connection_pool regexp_parser (2.11.3) @@ -472,7 +506,7 @@ GEM responders (3.2.0) actionpack (>= 7.0) railties (>= 7.0) - retriable (3.1.2) + retriable (3.2.1) rexml (3.4.4) rotp (6.3.0) rouge (4.7.0) @@ -485,10 +519,10 @@ GEM rspec-expectations (3.13.5) diff-lcs (>= 1.2.0, < 2.0) rspec-support (~> 3.13.0) - rspec-mocks (3.13.7) + rspec-mocks (3.13.8) diff-lcs (>= 1.2.0, < 2.0) rspec-support (~> 3.13.0) - rspec-rails (8.0.2) + rspec-rails (8.0.3) actionpack (>= 7.2) activesupport (>= 7.2) railties (>= 7.2) @@ -496,16 +530,17 @@ GEM rspec-expectations (~> 3.13) rspec-mocks (~> 3.13) rspec-support (~> 3.13) - rspec-support (3.13.6) - rubocop (1.82.1) + rspec-support (3.13.7) + rubocop (1.85.0) json (~> 2.3) language_server-protocol (~> 3.17.0.2) lint_roller (~> 1.1.0) + mcp (~> 0.6) parallel (~> 1.10) parser (>= 3.3.0.2) rainbow (>= 2.2.2, < 4.0) regexp_parser (>= 2.9.3, < 3.0) - rubocop-ast (>= 1.48.0, < 2.0) + rubocop-ast (>= 1.49.0, < 2.0) ruby-progressbar (~> 1.7) unicode-display_width (>= 2.4.0, < 4.0) rubocop-ast (1.49.0) @@ -533,14 +568,14 @@ GEM rubyzip (>= 3.2.2) rubyzip (3.2.2) securerandom (0.4.1) - semantic_range (3.1.0) + semantic_range (3.1.1) shakapacker (9.5.0) activesupport (>= 5.2) package_json rack-proxy (>= 0.6.1) railties (>= 5.2) semantic_range (>= 2.3.0) - sidekiq (8.1.0) + sidekiq (8.1.1) connection_pool (>= 3.0.0) json (>= 2.16.0) logger (>= 1.7.0) @@ -558,6 +593,9 @@ GEM simplecov-html (0.13.2) simplecov_json_formatter (0.1.4) smart_properties (1.17.0) + snaky_hash (2.0.3) + hashie (>= 0.1.0, < 6) + version_gem (>= 1.1.8, < 3) sqlite3 (2.9.0-aarch64-linux-gnu) sqlite3 (2.9.0-aarch64-linux-musl) sqlite3 (2.9.0-arm64-darwin) @@ -571,7 +609,7 @@ GEM timeout (0.6.0) trailblazer-option (0.1.2) tsort (0.2.0) - turbo-rails (2.0.21) + turbo-rails (2.0.23) actionpack (>= 7.1.0) railties (>= 7.1.0) twitter_cldr (6.14.0) @@ -590,13 +628,13 @@ GEM uniform_notifier (1.18.0) uri (1.1.1) useragent (0.16.11) + version_gem (1.1.9) warden (1.2.9) rack (>= 2.0.9) - web-console (4.2.1) - actionview (>= 6.0.0) - activemodel (>= 6.0.0) + web-console (4.3.0) + actionview (>= 8.0.0) bindex (>= 0.4.0) - railties (>= 6.0.0) + railties (>= 8.0.0) webmock (3.26.1) addressable (>= 2.8.0) crack (>= 0.3.2) @@ -609,7 +647,7 @@ GEM xpath (3.2.0) nokogiri (~> 1.8) yaml (0.4.0) - zeitwerk (2.7.4) + zeitwerk (2.7.5) PLATFORMS aarch64-linux @@ -652,6 +690,8 @@ DEPENDENCIES lograge numo-narray-alt oj + omniauth-google-oauth2 + omniauth-rails_csrf_protection onnxruntime pagy pg @@ -662,7 +702,7 @@ DEPENDENCIES rack rails rails-i18n - rails_autolink + redis rotp rouge rqrcode diff --git a/app/controllers/api/active_storage_blobs_proxy_controller.rb b/app/controllers/api/active_storage_blobs_proxy_controller.rb index a542c637b..56ad1c3da 100644 --- a/app/controllers/api/active_storage_blobs_proxy_controller.rb +++ b/app/controllers/api/active_storage_blobs_proxy_controller.rb @@ -14,7 +14,7 @@ def show blob_uuid, purp, exp = ApplicationRecord.signed_id_verifier.verified(params[:signed_uuid]) if blob_uuid.blank? || purp != 'blob' - Rollbar.error('Blob not found') if defined?(Rollbar) + Rails.logger.error('Blob not found') return head :not_found end @@ -57,7 +57,7 @@ def authorization_check!(attachment, record, exp) return if !require_ttl && !require_auth end - Rollbar.error('Blob unauthorized') if defined?(Rollbar) + Rails.logger.error('Blob unauthorized') raise CanCan::AccessDenied end diff --git a/app/controllers/api/active_storage_blobs_proxy_legacy_controller.rb b/app/controllers/api/active_storage_blobs_proxy_legacy_controller.rb index 77ad2c6ad..c023f2656 100644 --- a/app/controllers/api/active_storage_blobs_proxy_legacy_controller.rb +++ b/app/controllers/api/active_storage_blobs_proxy_legacy_controller.rb @@ -12,7 +12,7 @@ class ActiveStorageBlobsProxyLegacyController < ApiBaseController # rubocop:disable Metrics def show - Rollbar.info('Blob legacy') if defined?(Rollbar) + Rails.logger.info('Blob legacy') blob = ActiveStorage::Blob.find_signed(params[:signed_blob_id] || params[:signed_id]) @@ -25,7 +25,7 @@ def show end unless is_permitted - Rollbar.error("Blob account not found: #{blob.id}") if defined?(Rollbar) + Rails.logger.error("Blob account not found: #{blob.id}") return render json: { error: 'Not authenticated' }, status: :unauthorized end diff --git a/app/controllers/api/api_base_controller.rb b/app/controllers/api/api_base_controller.rb index ff01fc8fc..8ebe7f217 100644 --- a/app/controllers/api/api_base_controller.rb +++ b/app/controllers/api/api_base_controller.rb @@ -20,7 +20,7 @@ class ApiBaseController < ActionController::API end rescue_from RateLimit::LimitApproached do |e| - Rollbar.error(e) if defined?(Rollbar) + Rails.logger.error(e) render json: { error: 'Too many requests' }, status: :too_many_requests end @@ -31,7 +31,7 @@ class ApiBaseController < ActionController::API end rescue_from JSON::ParserError do |e| - Rollbar.warning(e) if defined?(Rollbar) + Rails.logger.warn(e) render json: { error: "JSON parse error: #{e.message}" }, status: :unprocessable_content end diff --git a/app/controllers/api/attachments_controller.rb b/app/controllers/api/attachments_controller.rb index f00552cd5..aa7eccef6 100644 --- a/app/controllers/api/attachments_controller.rb +++ b/app/controllers/api/attachments_controller.rb @@ -14,13 +14,13 @@ def create image = Vips::Image.new_from_file(params[:file].path) if ImageUtils.blank?(image) - Rollbar.error("Empty signature: #{submitter.id}") if defined?(Rollbar) + Rails.logger.error("Empty signature: #{submitter.id}") return render json: { error: "#{params[:type]} is empty" }, status: :unprocessable_content end if ImageUtils.error?(image) - Rollbar.error("Error signature: #{submitter.id}") if defined?(Rollbar) + Rails.logger.error("Error signature: #{submitter.id}") return render json: { error: "#{params[:type]} error, try to sign on another device" }, status: :unprocessable_content @@ -35,7 +35,7 @@ def create render json: attachment.as_json(only: %i[uuid created_at], methods: %i[url filename content_type]) rescue Submitters::MaliciousFileExtension => e - Rollbar.error(e) if defined?(Rollbar) + Rails.logger.error(e) render json: { error: e.message }, status: :unprocessable_content end diff --git a/app/controllers/api/submissions_controller.rb b/app/controllers/api/submissions_controller.rb index 22af24d92..986bbb87d 100644 --- a/app/controllers/api/submissions_controller.rb +++ b/app/controllers/api/submissions_controller.rb @@ -55,7 +55,7 @@ def create return render json: { error: 'Template not found' }, status: :unprocessable_content if @template.nil? if @template.fields.blank? - Rollbar.warning("Template does not contain fields: #{@template.id}") if defined?(Rollbar) + Rails.logger.warn("Template does not contain fields: #{@template.id}") return render json: { error: 'Template does not contain fields' }, status: :unprocessable_content end @@ -82,7 +82,7 @@ def create render json: build_create_json(submissions) rescue Submitters::NormalizeValues::BaseError, Submissions::CreateFromSubmitters::BaseError, DownloadUtils::UnableToDownload => e - Rollbar.warning(e) if defined?(Rollbar) + Rails.logger.warn(e) render json: { error: e.message }, status: :unprocessable_content end @@ -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..1071549e6 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? @@ -74,10 +78,11 @@ def update render json: Submitters::SerializeForApi.call(@submitter, with_template: false, with_urls: true, with_events: false, params:) rescue Submitters::NormalizeValues::BaseError, DownloadUtils::UnableToDownload => e - Rollbar.warning(e) if defined?(Rollbar) + Rails.logger.warn(e) 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/application_controller.rb b/app/controllers/application_controller.rb index 50823ca7b..51569068b 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -28,24 +28,20 @@ class ApplicationController < ActionController::Base end rescue_from RateLimit::LimitApproached do |e| - Rollbar.error(e) if defined?(Rollbar) + Rails.logger.error(e) redirect_to request.referer, alert: 'Too many requests', status: :too_many_requests end if Rails.env.production? || Rails.env.test? rescue_from CanCan::AccessDenied do |e| - Rollbar.warning(e) if defined?(Rollbar) + Rails.logger.warn(e) redirect_to root_path, alert: e.message end end def default_url_options - if request.domain == 'docuseal.com' - return { host: 'docuseal.com', protocol: ENV['FORCE_SSL'].present? ? 'https' : 'http' } - end - Docuseal.default_url_options end @@ -125,11 +121,7 @@ def form_link_host Docuseal.default_url_options[:host] end - def maybe_redirect_com - return if request.domain != 'docuseal.co' - - redirect_to request.url.gsub('.co/', '.com/'), allow_other_host: true, status: :moved_permanently - end + def maybe_redirect_com; end def set_csp request.content_security_policy = current_content_security_policy.tap do |policy| diff --git a/app/controllers/console_redirect_controller.rb b/app/controllers/console_redirect_controller.rb index 4b9102637..b14ef7f7a 100644 --- a/app/controllers/console_redirect_controller.rb +++ b/app/controllers/console_redirect_controller.rb @@ -5,22 +5,6 @@ class ConsoleRedirectController < ApplicationController skip_authorization_check def index - if request.path == '/upgrade' - params[:redir] = Docuseal.multitenant? ? "#{Docuseal::CONSOLE_URL}/plans" : "#{Docuseal::CONSOLE_URL}/on_premises" - end - - params[:redir] = "#{Docuseal::CONSOLE_URL}/manage" if request.path == '/manage' - - return redirect_to(new_user_session_path({ redir: params[:redir] }.compact)) if true_user.blank? - - auth = JsonWebToken.encode(uuid: true_user.uuid, - scope: :console, - exp: 1.minute.from_now.to_i) - - redir_uri = Addressable::URI.parse(params[:redir]) - path = redir_uri.path if params[:redir].to_s.starts_with?(Docuseal::CONSOLE_URL) - - redirect_to "#{Docuseal::CONSOLE_URL}#{path}?#{{ **redir_uri&.query_values, 'auth' => auth }.to_query}", - allow_other_host: true + redirect_to root_path end end diff --git a/app/controllers/embed_scripts_controller.rb b/app/controllers/embed_scripts_controller.rb index a40ed79c9..70c2794e4 100644 --- a/app/controllers/embed_scripts_controller.rb +++ b/app/controllers/embed_scripts_controller.rb @@ -1,18 +1,12 @@ # frozen_string_literal: true class EmbedScriptsController < ActionController::Metal - DUMMY_SCRIPT = <<~JAVASCRIPT.freeze + DUMMY_SCRIPT = <<~JAVASCRIPT const DummyBuilder = class extends HTMLElement { connectedCallback() { this.innerHTML = `
-

Upgrade to Pro

-

Unlock embedded components by upgrading to Pro

-
- - Learn More - -
+

Embedded components are not available in this installation.

`; } diff --git a/app/controllers/enquiries_controller.rb b/app/controllers/enquiries_controller.rb index 829b578c2..46ef4b1f7 100644 --- a/app/controllers/enquiries_controller.rb +++ b/app/controllers/enquiries_controller.rb @@ -5,18 +5,6 @@ class EnquiriesController < ApplicationController skip_authorization_check def create - if params[:talk_to_sales] == 'on' - Faraday.post(Docuseal::ENQUIRIES_URL, - enquiry_params.merge(type: :talk_to_sales).to_json, - 'Content-Type' => 'application/json') - end - head :ok end - - private - - def enquiry_params - params.require(:user).permit(:email) - end end diff --git a/app/controllers/errors_controller.rb b/app/controllers/errors_controller.rb index 0c9e3632a..ac5a936d4 100644 --- a/app/controllers/errors_controller.rb +++ b/app/controllers/errors_controller.rb @@ -1,34 +1,12 @@ # frozen_string_literal: true class ErrorsController < ActionController::Base - ENTERPRISE_FEATURE_MESSAGE = - 'This feature is available in Pro Edition: https://www.docuseal.com/pricing' - - ENTERPRISE_PATHS = [ - '/submissions/html', - '/api/submissions/html', - '/templates/html', - '/api/templates/html', - '/submissions/pdf', - '/api/submissions/pdf', - '/templates/pdf', - '/api/templates/pdf', - '/templates/doc', - '/api/templates/doc', - '/templates/docx', - '/api/templates/docx' - ].freeze - SAFE_ERROR_MESSAGE_CLASSES = [ ActionDispatch::Http::Parameters::ParseError, JSON::ParserError ].freeze def show - if request.original_fullpath.in?(ENTERPRISE_PATHS) && error_status_code == 404 - return render json: { status: 404, message: ENTERPRISE_FEATURE_MESSAGE }, status: :not_found - end - respond_to do |f| f.json do set_cors_headers diff --git a/app/controllers/esign_settings_controller.rb b/app/controllers/esign_settings_controller.rb index 81c75fae3..19133ef31 100644 --- a/app/controllers/esign_settings_controller.rb +++ b/app/controllers/esign_settings_controller.rb @@ -60,7 +60,7 @@ def create redirect_to settings_esign_path, notice: I18n.t('certificate_has_been_successfully_added') rescue OpenSSL::PKCS12::PKCS12Error => e - Rollbar.error(e) if defined?(Rollbar) + Rails.logger.error(e) @cert_record.errors.add(:password, e.message) diff --git a/app/controllers/newsletters_controller.rb b/app/controllers/newsletters_controller.rb index ddd305c5c..aed26e11e 100644 --- a/app/controllers/newsletters_controller.rb +++ b/app/controllers/newsletters_controller.rb @@ -6,16 +6,6 @@ class NewslettersController < ApplicationController def show; end def update - Faraday.post(Docuseal::NEWSLETTER_URL, newsletter_params.to_json, 'Content-Type' => 'application/json') - rescue StandardError => e - Rails.logger.error(e) - ensure redirect_to root_path end - - private - - def newsletter_params - params.require(:user).permit(:email) - end end diff --git a/app/controllers/omniauth_callbacks_controller.rb b/app/controllers/omniauth_callbacks_controller.rb new file mode 100644 index 000000000..3653e1740 --- /dev/null +++ b/app/controllers/omniauth_callbacks_controller.rb @@ -0,0 +1,22 @@ +# frozen_string_literal: true + +class OmniauthCallbacksController < Devise::OmniauthCallbacksController + skip_before_action :verify_authenticity_token, only: :google_oauth2 + + def google_oauth2 + oauth = request.env['omniauth.auth'] + user = Users.from_omniauth(oauth) + + if user&.active_for_authentication? + sign_in_and_redirect(user, event: :authentication) + elsif user&.archived_at? + redirect_to new_user_session_path, alert: I18n.t('account_archived') + else + redirect_to new_user_session_path, alert: I18n.t('user_not_found') + end + end + + def failure + redirect_to new_user_session_path, alert: I18n.t('authentication_failed') + end +end diff --git a/app/controllers/personalization_settings_controller.rb b/app/controllers/personalization_settings_controller.rb index d9d334901..3510d1aae 100644 --- a/app/controllers/personalization_settings_controller.rb +++ b/app/controllers/personalization_settings_controller.rb @@ -8,7 +8,8 @@ class PersonalizationSettingsController < ApplicationController AccountConfig::SUBMITTER_DOCUMENTS_COPY_EMAIL_KEY, AccountConfig::SUBMITTER_COMPLETED_EMAIL_KEY, AccountConfig::FORM_COMPLETED_MESSAGE_KEY, - *(Docuseal.multitenant? ? [] : [AccountConfig::POLICY_LINKS_KEY]) + *(Docuseal.multitenant? ? [] : [AccountConfig::POLICY_LINKS_KEY]), + AccountConfig::COMPANY_LOGO_URL_KEY ].freeze InvalidKey = Class.new(StandardError) diff --git a/app/controllers/sessions_controller.rb b/app/controllers/sessions_controller.rb index 2281d56f3..6413b6eb6 100644 --- a/app/controllers/sessions_controller.rb +++ b/app/controllers/sessions_controller.rb @@ -9,7 +9,7 @@ def create email = sign_in_params[:email].to_s.downcase if Docuseal.multitenant? && !User.exists?(email:) - Rollbar.warning('Sign in new user') if defined?(Rollbar) + Rails.logger.warn('Sign in new user') return redirect_to new_registration_path(sign_up: true, user: sign_in_params.slice(:email)), notice: I18n.t('create_a_new_account') @@ -25,11 +25,7 @@ def create private def after_sign_in_path_for(...) - if params[:redir].present? - return console_redirect_index_path(redir: params[:redir]) if params[:redir].starts_with?(Docuseal::CONSOLE_URL) - - return params[:redir] - end + return params[:redir] if params[:redir].present? super end diff --git a/app/controllers/sms_settings_controller.rb b/app/controllers/sms_settings_controller.rb index 96168605d..b04e923f7 100644 --- a/app/controllers/sms_settings_controller.rb +++ b/app/controllers/sms_settings_controller.rb @@ -7,10 +7,28 @@ class SmsSettingsController < ApplicationController def index; end + def create + if @encrypted_config.update(sms_configs) + redirect_to settings_sms_index_path, notice: I18n.t('changes_have_been_saved') + else + render :index, status: :unprocessable_content + end + rescue StandardError => e + flash[:alert] = e.message + + render :index, status: :unprocessable_content + end + private def load_encrypted_config @encrypted_config = - EncryptedConfig.find_or_initialize_by(account: current_account, key: 'sms_configs') + EncryptedConfig.find_or_initialize_by(account: current_account, key: EncryptedConfig::SMS_KEY) + end + + def sms_configs + params.require(:encrypted_config).permit(value: {}).tap do |e| + e[:value].compact_blank! + end end end diff --git a/app/controllers/start_form_controller.rb b/app/controllers/start_form_controller.rb index ad5d6455f..1e189b0d8 100644 --- a/app/controllers/start_form_controller.rb +++ b/app/controllers/start_form_controller.rb @@ -27,7 +27,7 @@ def show @template.submitters.first)['uuid']) render :email_verification if params[:email_verification] else - Rollbar.warning("Not shared template: #{@template.id}") if defined?(Rollbar) + Rails.logger.warn("Not shared template: #{@template.id}") return render :private if current_user && current_ability.can?(:read, @template) @@ -111,7 +111,7 @@ def authorize_start! return if @resubmit_submitter return if @template.shared_link? || (current_user && current_ability.can?(:read, @template)) - Rollbar.warning("Not shared template: #{@template.id}") if defined?(Rollbar) + Rails.logger.warn("Not shared template: #{@template.id}") redirect_to start_form_path(@template.slug) end diff --git a/app/controllers/submissions_download_controller.rb b/app/controllers/submissions_download_controller.rb index 4bcd3237e..7b82a43dd 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 + Rails.logger.info("TTL: #{last_submitter.id}") + + 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,8 +46,18 @@ 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) + current_user && current_ability.can?(:read, submitter) end def build_urls(submitter) @@ -57,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) @@ -75,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/submissions_preview_controller.rb b/app/controllers/submissions_preview_controller.rb index 6163c36b9..a7bb86577 100644 --- a/app/controllers/submissions_preview_controller.rb +++ b/app/controllers/submissions_preview_controller.rb @@ -29,7 +29,7 @@ def show end if use_signature?(@submission) && !signature_valid - Rollbar.info("TTL: #{@submission.id}") if defined?(Rollbar) + Rails.logger.info("TTL: #{@submission.id}") return redirect_to submissions_preview_completed_path(@submission.slug) end diff --git a/app/controllers/submit_form_controller.rb b/app/controllers/submit_form_controller.rb index f723aa32f..efbdf3946 100644 --- a/app/controllers/submit_form_controller.rb +++ b/app/controllers/submit_form_controller.rb @@ -9,15 +9,15 @@ 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 def show submission = @submitter.submission + 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? - return render :email_2fa if require_email_2fa?(@submitter) @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 @@ -74,7 +74,7 @@ def update head :ok rescue Submitters::SubmitValues::RequiredFieldError => e - Rollbar.warning("Required field #{@submitter.id}: #{e.message}") if defined?(Rollbar) + Rails.logger.warn("Required field #{@submitter.id}: #{e.message}") render json: { field_uuid: e.message }, status: :unprocessable_content rescue Submitters::SubmitValues::ValidationError => e @@ -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..af9cbeb43 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) @@ -32,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/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..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 @@ -45,7 +47,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/app/controllers/submitters_send_email_controller.rb b/app/controllers/submitters_send_email_controller.rb index f3eb31871..0ecced8e1 100644 --- a/app/controllers/submitters_send_email_controller.rb +++ b/app/controllers/submitters_send_email_controller.rb @@ -7,7 +7,7 @@ def create if Docuseal.multitenant? && SubmissionEvent.exists?(submitter: @submitter, event_type: 'send_email', created_at: 10.hours.ago..Time.current) - Rollbar.warning("Already sent: #{@submitter.id}") if defined?(Rollbar) + Rails.logger.warn("Already sent: #{@submitter.id}") return redirect_back(fallback_location: submission_path(@submitter.submission), alert: I18n.t('email_has_been_sent_already')) diff --git a/app/controllers/submitters_send_sms_controller.rb b/app/controllers/submitters_send_sms_controller.rb new file mode 100644 index 000000000..479251365 --- /dev/null +++ b/app/controllers/submitters_send_sms_controller.rb @@ -0,0 +1,21 @@ +# frozen_string_literal: true + +class SubmittersSendSmsController < ApplicationController + load_and_authorize_resource :submitter, id_param: :submitter_slug, find_by: :slug + + def create + if SubmissionEvent.exists?(submitter: @submitter, + event_type: 'send_sms', + created_at: 10.hours.ago..Time.current) + return redirect_back(fallback_location: submission_path(@submitter.submission), + alert: I18n.t('sms_has_been_sent_already')) + end + + SendSubmitterInvitationSmsJob.perform_async('submitter_id' => @submitter.id) + + @submitter.sent_at ||= Time.current + @submitter.save! + + redirect_back(fallback_location: submission_path(@submitter.submission), notice: I18n.t('sms_has_been_sent')) + end +end 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/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/controllers/templates_uploads_controller.rb b/app/controllers/templates_uploads_controller.rb index e8c00aeaa..cf4e38ea6 100644 --- a/app/controllers/templates_uploads_controller.rb +++ b/app/controllers/templates_uploads_controller.rb @@ -31,7 +31,7 @@ def create rescue Templates::CreateAttachments::PdfEncrypted render turbo_stream: turbo_stream.append(params[:form_id], html: helpers.tag.prompt_password) rescue StandardError => e - Rollbar.error(e) if defined?(Rollbar) + Rails.logger.error(e) raise if Rails.env.local? @@ -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/app/controllers/webhook_secret_controller.rb b/app/controllers/webhook_secret_controller.rb index b9b8a3f24..4c59bb936 100644 --- a/app/controllers/webhook_secret_controller.rb +++ b/app/controllers/webhook_secret_controller.rb @@ -3,12 +3,18 @@ class WebhookSecretController < ApplicationController load_and_authorize_resource :webhook_url, parent: false - def show; end + def show + @webhook_url.ensure_signing_key! + end def update - @webhook_url.update!(secret: { - webhook_secret_params[:key] => webhook_secret_params[:value] - }.compact_blank) + if params[:regenerate_signing_key] + @webhook_url.update!(signing_key: SecureRandom.hex(32)) + else + @webhook_url.update!(secret: { + webhook_secret_params[:key] => webhook_secret_params[:value] + }.compact_blank) + end redirect_back(fallback_location: settings_webhook_path(@webhook_url), notice: I18n.t('webhook_secret_has_been_saved')) diff --git a/app/javascript/application.js b/app/javascript/application.js index 49cd95163..8889609fe 100644 --- a/app/javascript/application.js +++ b/app/javascript/application.js @@ -40,6 +40,7 @@ import DashboardDropzone from './elements/dashboard_dropzone' import RequiredCheckboxGroup from './elements/required_checkbox_group' import PageContainer from './elements/page_container' import EmailEditor from './elements/email_editor' +import MarkdownEditor from './elements/markdown_editor' import MountOnClick from './elements/mount_on_click' import RemoveOnEvent from './elements/remove_on_event' import ScrollTo from './elements/scroll_to' @@ -131,6 +132,7 @@ safeRegisterElement('check-on-click', CheckOnClick) safeRegisterElement('required-checkbox-group', RequiredCheckboxGroup) safeRegisterElement('page-container', PageContainer) safeRegisterElement('email-editor', EmailEditor) +safeRegisterElement('markdown-editor', MarkdownEditor) safeRegisterElement('mount-on-click', MountOnClick) safeRegisterElement('remove-on-event', RemoveOnEvent) safeRegisterElement('scroll-to', ScrollTo) diff --git a/app/javascript/application.scss b/app/javascript/application.scss index 47eeb4f62..fd02cecf2 100644 --- a/app/javascript/application.scss +++ b/app/javascript/application.scss @@ -155,3 +155,7 @@ button[disabled] .enabled, button.btn-disabled .enabled { .font-courier { font-family: "Courier New", Consolas, "Liberation Mono", monospace, ui-monospace, SFMono-Regular, Menlo, Monaco; } + +markdown-editor [contenteditable] p { + margin-bottom: 18px; +} diff --git a/app/javascript/elements/markdown_editor.js b/app/javascript/elements/markdown_editor.js new file mode 100644 index 000000000..174312771 --- /dev/null +++ b/app/javascript/elements/markdown_editor.js @@ -0,0 +1,385 @@ +import { target, targetable } from '@github/catalyst/lib/targetable' +import { actionable } from '@github/catalyst/lib/actionable' + +function loadTiptap () { + return Promise.all([ + import(/* webpackChunkName: "markdown-editor" */ '@tiptap/core'), + import(/* webpackChunkName: "markdown-editor" */ '@tiptap/extension-bold'), + import(/* webpackChunkName: "markdown-editor" */ '@tiptap/extension-italic'), + import(/* webpackChunkName: "markdown-editor" */ '@tiptap/extension-paragraph'), + import(/* webpackChunkName: "markdown-editor" */ '@tiptap/extension-text'), + import(/* webpackChunkName: "markdown-editor" */ '@tiptap/extension-hard-break'), + import(/* webpackChunkName: "markdown-editor" */ '@tiptap/extension-document'), + import(/* webpackChunkName: "markdown-editor" */ '@tiptap/extension-link'), + import(/* webpackChunkName: "markdown-editor" */ '@tiptap/extension-underline'), + import(/* webpackChunkName: "markdown-editor" */ '@tiptap/extensions'), + import(/* webpackChunkName: "markdown-editor" */ '@tiptap/markdown'), + import(/* webpackChunkName: "markdown-editor" */ '@tiptap/pm/state'), + import(/* webpackChunkName: "markdown-editor" */ '@tiptap/pm/view') + ]).then(([core, bold, italic, paragraph, text, hardBreak, document, link, underline, extensions, markdown, pmState, pmView]) => ({ + Editor: core.Editor, + Extension: core.Extension, + Bold: bold.default || bold, + Italic: italic.default || italic, + Paragraph: paragraph.default || paragraph, + Text: text.default || text, + HardBreak: hardBreak.default || hardBreak, + Document: document.default || document, + Link: link.default || link, + Underline: underline.default || underline, + UndoRedo: extensions.UndoRedo, + Markdown: markdown.Markdown, + Plugin: pmState.Plugin, + Decoration: pmView.Decoration, + DecorationSet: pmView.DecorationSet + })) +} + +class LinkTooltip { + constructor (container, editor) { + this.container = container + this.editor = editor + + const template = document.createElement('template') + + template.innerHTML = container.dataset.linkTooltipHtml + + this.tooltip = template.content.firstElementChild + + this.input = this.tooltip.querySelector('input') + this.saveButton = this.tooltip.querySelector('[data-role="link-save"]') + this.removeButton = this.tooltip.querySelector('[data-role="link-remove"]') + + container.style.position = 'relative' + container.appendChild(this.tooltip) + } + + isVisible () { + return !this.tooltip.classList.contains('hidden') + } + + normalizeUrl (url) { + if (!url) return url + if (/^{/i.test(url)) return url + if (/^https?:\/\//i.test(url)) return url + if (/^mailto:/i.test(url)) return url + + return `https://${url}` + } + + show (url, pos, { focus = false } = {}) { + this.input.value = url || '' + this.removeButton.classList.toggle('hidden', !url) + + this.tooltip.classList.remove('hidden') + + const coords = this.editor.view.coordsAtPos(pos) + const containerRect = this.container.getBoundingClientRect() + + this.tooltip.style.left = `${coords.left - containerRect.left}px` + this.tooltip.style.top = `${coords.bottom - containerRect.top + 4}px` + + if (focus) this.input.focus() + + this.saveHandler = () => { + const inputUrl = this.input.value.trim() + + if (inputUrl) { + this.editor.chain().focus().extendMarkRange('link').setLink({ href: this.normalizeUrl(inputUrl) }).run() + } + + this.hide() + } + + this.removeHandler = () => { + this.editor.chain().focus().extendMarkRange('link').unsetLink().run() + + this.hide() + } + + this.keyHandler = (e) => { + if (e.key === 'Enter') { + e.preventDefault() + this.saveHandler() + } else if (e.key === 'Escape') { + e.preventDefault() + this.hide() + } + } + + this.saveButton.addEventListener('click', this.saveHandler, { once: true }) + this.removeButton.addEventListener('click', this.removeHandler, { once: true }) + this.input.addEventListener('keydown', this.keyHandler) + } + + hide () { + if (this.saveHandler) { + this.saveButton.removeEventListener('click', this.saveHandler) + this.saveHandler = null + } + + if (this.removeHandler) { + this.removeButton.removeEventListener('click', this.removeHandler) + this.removeHandler = null + } + + if (this.keyHandler) { + this.input.removeEventListener('keydown', this.keyHandler) + this.keyHandler = null + } + + this.tooltip.classList.add('hidden') + this.currentMark = null + } +} + +export default actionable(targetable(class extends HTMLElement { + static [target.static] = [ + 'textarea', + 'editorElement', + 'boldButton', + 'italicButton', + 'underlineButton', + 'linkButton' + ] + + async connectedCallback () { + if (!this.textarea || !this.editorElement) return + + this.textarea.style.display = 'none' + this.adjustShortcutsForPlatform() + + const { Editor, Extension, Bold, Italic, Paragraph, Text, HardBreak, UndoRedo, Document, Link, Underline, Markdown, Plugin, Decoration, DecorationSet } = await loadTiptap() + + const buildDecorations = (doc) => { + const decorations = [] + const regex = /\{\{?[a-zA-Z0-9_.-]+\}\}?/g + + doc.descendants((node, pos) => { + if (!node.isText) return + + let match + + while ((match = regex.exec(node.text)) !== null) { + decorations.push( + Decoration.inline(pos + match.index, pos + match.index + match[0].length, { + class: 'bg-amber-100 py-0.5 px-1 rounded' + }) + ) + } + }) + + return DecorationSet.create(doc, decorations) + } + + const VariableHighlight = Extension.create({ + name: 'variableHighlight', + addProseMirrorPlugins () { + return [new Plugin({ + state: { + init (_, { doc }) { + return buildDecorations(doc) + }, + apply (tr, oldSet) { + return tr.docChanged ? buildDecorations(tr.doc) : oldSet + } + }, + props: { + decorations (state) { + return this.getState(state) + } + } + })] + } + }) + + this.editor = new Editor({ + element: this.editorElement, + extensions: [ + Markdown, + Document, + Paragraph, + Text, + Bold, + Italic, + HardBreak.extend({ + addKeyboardShortcuts () { + return { + Enter: () => this.editor.commands.setHardBreak() + } + } + }), + UndoRedo, + Link.extend({ + inclusive: true, + addKeyboardShortcuts: () => ({ + 'Mod-k': () => { + this.toggleLink() + + return true + } + }) + }).configure({ + openOnClick: false, + HTMLAttributes: { + class: 'link', + 'data-turbo': 'false', + style: 'color: #2563eb; text-decoration: underline; cursor: text;' + } + }), + Underline, + VariableHighlight + ], + content: (this.textarea.value || '').trim().replace(/ *\n/g, '
'), + contentType: 'markdown', + editorProps: { + attributes: { + style: 'min-height: 220px', + dir: 'auto', + class: 'p-3 outline-none focus:outline-none' + } + }, + onUpdate: ({ editor }) => { + this.textarea.value = editor.getMarkdown() + this.textarea.dispatchEvent(new Event('input', { bubbles: true })) + }, + onSelectionUpdate: ({ editor }) => { + this.updateToolbarState() + this.handleLinkTooltip(editor) + }, + onBlur: () => { + setTimeout(() => { + if (!this.linkTooltip.tooltip.contains(document.activeElement)) { + this.linkTooltip.hide() + } + }, 0) + } + }) + + this.linkTooltip = new LinkTooltip(this, this.editor) + } + + adjustShortcutsForPlatform () { + if ((navigator.userAgentData?.platform || navigator.platform)?.toLowerCase()?.includes('mac')) { + this.querySelectorAll('.tooltip[data-tip]').forEach(tooltip => { + const tip = tooltip.getAttribute('data-tip') + + if (tip && tip.includes('Ctrl')) { + tooltip.setAttribute('data-tip', tip.replace(/Ctrl/g, '⌘')) + } + }) + } + } + + bold (e) { + e.preventDefault() + + this.editor.chain().focus().toggleBold().run() + this.updateToolbarState() + } + + italic (e) { + e.preventDefault() + + this.editor.chain().focus().toggleItalic().run() + this.updateToolbarState() + } + + underline (e) { + e.preventDefault() + + this.editor.chain().focus().toggleUnderline().run() + this.updateToolbarState() + } + + linkSelection (e) { + e.preventDefault() + + this.toggleLink() + this.updateToolbarState() + } + + undo (e) { + e.preventDefault() + + this.editor.chain().focus().undo().run() + this.updateToolbarState() + } + + redo (e) { + e.preventDefault() + + this.editor.chain().focus().redo().run() + this.updateToolbarState() + } + + updateToolbarState () { + this.boldButton.classList.toggle('bg-base-200', this.editor.isActive('bold')) + this.italicButton.classList.toggle('bg-base-200', this.editor.isActive('italic')) + this.underlineButton.classList.toggle('bg-base-200', this.editor.isActive('underline')) + this.linkButton.classList.toggle('bg-base-200', this.editor.isActive('link')) + } + + handleLinkTooltip (editor) { + const { from } = editor.state.selection + const mark = editor.state.doc.resolve(from).marks().find(m => m.type.name === 'link') + + if (!mark) { + if (this.linkTooltip.isVisible()) this.linkTooltip.hide() + + return + } + + if (this.linkTooltip.isVisible() && this.linkTooltip.currentMark === mark) return + + let linkStart = from + const start = editor.state.doc.resolve(from).start() + + for (let i = from - 1; i >= start; i--) { + if (editor.state.doc.resolve(i).marks().some(m => m.eq(mark))) { + linkStart = i + } else { + break + } + } + + this.linkTooltip.hide() + this.linkTooltip.show(mark.attrs.href, linkStart > start ? linkStart - 1 : linkStart) + this.linkTooltip.currentMark = mark + } + + toggleLink () { + if (this.editor.isActive('link')) { + this.linkTooltip.hide() + this.editor.chain().focus().extendMarkRange('link').unsetLink().run() + this.updateToolbarState() + } else { + const { from } = this.editor.state.selection + + this.linkTooltip.hide() + this.linkTooltip.show(this.editor.getAttributes('link').href, from, { focus: true }) + } + } + + insertVariable (e) { + const variable = e.target.closest('[data-variable]')?.dataset.variable + + if (variable) { + const { from, to } = this.editor.state.selection + + if (variable.includes('link') && from !== to) { + this.editor.chain().focus().setLink({ href: `{${variable}}` }).run() + } else { + this.editor.chain().focus().insertContent(`{${variable}}`).run() + } + } + } + + disconnectedCallback () { + this.linkTooltip.hide() + + if (this.editor) { + this.editor.destroy() + } + } +})) diff --git a/app/javascript/elements/toggle_attribute.js b/app/javascript/elements/toggle_attribute.js index e9ee30754..4305e2fee 100644 --- a/app/javascript/elements/toggle_attribute.js +++ b/app/javascript/elements/toggle_attribute.js @@ -1,14 +1,20 @@ 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 + this.target.disabled = value !== dataValue } } 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/javascript/rollbar.js b/app/javascript/rollbar.js deleted file mode 100644 index 21222e830..000000000 --- a/app/javascript/rollbar.js +++ /dev/null @@ -1,40 +0,0 @@ -import Rollbar from 'rollbar/dist/rollbar.umd' - -const token = document.querySelector('meta[name="rollbar-token"]')?.getAttribute('content') - -if (token) { - window.Rollbar ||= new Rollbar({ - accessToken: token, - captureUncaught: true, - captureUnhandledRejections: true, - captureIp: false, - autoInstrument: false, - ignoredMessages: [ - /Failed to fetch/i, - /NetworkError/i, - /Load failed/i, - /Clipboard write is not allowed/i - ], - transform (payload) { - payload.body.telemetry = [] - - if (payload.request.query_string) { - payload.request.query_string = '' - } - - if (payload.request.url) { - payload.request.url = payload.request.url.replace(/(\/[sdep]\/)(\w{5})[^/]+/, '$1$2') - } - - return payload - }, - payload: { - client: { - javascript: { - source_map_enabled: true - } - }, - environment: 'production' - } - }) -} diff --git a/app/javascript/submission_form/completed.vue b/app/javascript/submission_form/completed.vue index 708843bfe..b33ef36d9 100644 --- a/app/javascript/submission_form/completed.vue +++ b/app/javascript/submission_form/completed.vue @@ -63,38 +63,6 @@ {{ t('download') }} - - - - Star on Github - - - - - - {{ t('create_a_free_account') }} - - - -
- {{ t('powered_by') }} - DocuSeal - {{ t('open_source_documents_software') }}
@@ -161,6 +129,11 @@ export default { required: false, default: false }, + fetchOptions: { + type: Object, + required: false, + default: () => ({}) + }, completedButton: { type: Object, required: false, @@ -214,7 +187,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/i18n.js b/app/javascript/submission_form/i18n.js index 705679920..b9da958de 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', @@ -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 @@ "> @@ -88,7 +87,6 @@