Compare commits

..
2 Commits
Author SHA1 Message Date
Pete Matsyburka e64f187f85 fix font url 2024-08-12 13:55:36 +03:00
Pete Matsyburka 416685334a adjust retries 2024-08-12 13:54:13 +03:00
594 changed files with 4734 additions and 78381 deletions

No files matched your search

-58
View File
@@ -1,58 +0,0 @@
---
:position: before
:position_in_additional_file_patterns: before
:position_in_class: before
:position_in_factory: before
:position_in_fixture: before
:position_in_routes: before
:position_in_serializer: before
:position_in_test: before
:classified_sort: true
:exclude_controllers: true
:exclude_factories: true
:exclude_fixtures: false
:exclude_helpers: true
:exclude_scaffolds: true
:exclude_serializers: false
:exclude_sti_subclasses: false
:exclude_tests: false
:force: false
:format_markdown: false
:format_rdoc: false
:format_yard: false
:frozen: false
:ignore_model_sub_dir: false
:ignore_unknown_models: false
:include_version: false
:show_check_constraints: false
:show_complete_foreign_keys: false
:show_foreign_keys: true
:show_indexes: true
:simple_indexes: false
:sort: false
:timestamp: false
:trace: false
:with_comment: true
:with_column_comments: true
:with_table_comments: true
:active_admin: false
:command:
:debug: false
:hide_default_column_types: ''
:hide_limit_column_types: ''
:ignore_columns:
:ignore_routes:
:models: true
:routes: false
:skip_on_db_migrate: false
:target_action: :do_annotations
:wrapper:
:wrapper_close:
:wrapper_open:
:classes_default_to_s: []
:additional_file_patterns: []
:model_dir:
- app/models
:require: []
:root_dir:
- ''
View File
File renamed without changes.
+14 -51
View File
@@ -6,15 +6,14 @@ jobs:
rubocop: rubocop:
name: Rubocop name: Rubocop
runs-on: ubuntu-latest runs-on: ubuntu-latest
timeout-minutes: 10
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v2
- name: Install Ruby - name: Install Ruby
uses: ruby/setup-ruby@v1 uses: ruby/setup-ruby@v1
with: with:
ruby-version: 3.4.2 ruby-version: 3.3.3
- name: Cache gems - name: Cache gems
uses: actions/cache@v4 uses: actions/cache@v1
with: with:
path: vendor/bundle path: vendor/bundle
key: ${{ runner.os }}-gem-${{ hashFiles('**/Gemfile.lock') }} key: ${{ runner.os }}-gem-${{ hashFiles('**/Gemfile.lock') }}
@@ -31,15 +30,14 @@ jobs:
erblint: erblint:
name: Erblint name: Erblint
runs-on: ubuntu-latest runs-on: ubuntu-latest
timeout-minutes: 10
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v2
- name: Install Ruby - name: Install Ruby
uses: ruby/setup-ruby@v1 uses: ruby/setup-ruby@v1
with: with:
ruby-version: 3.4.2 ruby-version: 3.3.3
- name: Cache gems - name: Cache gems
uses: actions/cache@v4 uses: actions/cache@v1
with: with:
path: vendor/bundle path: vendor/bundle
key: ${{ runner.os }}-gem-${{ hashFiles('**/Gemfile.lock') }} key: ${{ runner.os }}-gem-${{ hashFiles('**/Gemfile.lock') }}
@@ -51,14 +49,13 @@ jobs:
bundle config path vendor/bundle bundle config path vendor/bundle
bundle install --jobs 4 --retry 4 bundle install --jobs 4 --retry 4
- name: Run Erblint - name: Run Erblint
run: bundle exec erb_lint ./app run: bundle exec erblint ./app
eslint: eslint:
name: ESLint name: ESLint
runs-on: ubuntu-latest runs-on: ubuntu-latest
timeout-minutes: 10
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v2
- name: Install Node.js - name: Install Node.js
uses: actions/setup-node@v1 uses: actions/setup-node@v1
with: with:
@@ -66,7 +63,7 @@ jobs:
- name: Cache directory path - name: Cache directory path
id: yarn-cache-dir-path id: yarn-cache-dir-path
run: echo "::set-output name=dir::$(yarn cache dir)" run: echo "::set-output name=dir::$(yarn cache dir)"
- uses: actions/cache@v4 - uses: actions/cache@v1
id: yarn-cache id: yarn-cache
with: with:
path: ${{ steps.yarn-cache-dir-path.outputs.dir }} path: ${{ steps.yarn-cache-dir-path.outputs.dir }}
@@ -80,38 +77,9 @@ jobs:
run: | run: |
./node_modules/eslint/bin/eslint.js "app/javascript/**/*.js" ./node_modules/eslint/bin/eslint.js "app/javascript/**/*.js"
brakeman:
name: Brakeman
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@v4
- name: Install Ruby
uses: ruby/setup-ruby@v1
with:
ruby-version: 3.4.2
- name: Cache gems
uses: actions/cache@v4
with:
path: vendor/bundle
key: ${{ runner.os }}-gem-${{ hashFiles('**/Gemfile.lock') }}
restore-keys: |
${{ runner.os }}-gem-
- name: Install gems
run: |
gem install bundler
bundle config path vendor/bundle
bundle install --jobs 4 --retry 4
yarn install
sudo apt-get update
sudo apt-get install libvips
- name: Run Brakeman
run: bundle exec brakeman -q --exit-on-warn
rspec: rspec:
name: RSpec name: RSpec
runs-on: ubuntu-latest runs-on: ubuntu-latest
timeout-minutes: 10
services: services:
postgres: postgres:
@@ -128,26 +96,24 @@ jobs:
--health-retries 5 --health-retries 5
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v2
- name: Install Ruby - name: Install Ruby
uses: ruby/setup-ruby@v1 uses: ruby/setup-ruby@v1
with: with:
ruby-version: 3.4.2 ruby-version: 3.3.3
- name: Set up Node - name: Set up Node
uses: actions/setup-node@v1 uses: actions/setup-node@v1
with: with:
node-version: 20.9.0 node-version: 20.9.0
- name: Install Chrome - name: Install Chrome
uses: browser-actions/setup-chrome@latest uses: browser-actions/setup-chrome@latest
with:
chrome-version: 125
- name: Cache node_modules - name: Cache node_modules
uses: actions/cache@v4 uses: actions/cache@v1
with: with:
path: node_modules path: node_modules
key: ${{ runner.os }}-node-${{ hashFiles('**/yarn.lock') }} key: ${{ runner.os }}-node-${{ hashFiles('**/yarn.lock') }}
- name: Cache gems - name: Cache gems
uses: actions/cache@v4 uses: actions/cache@v1
with: with:
path: vendor/bundle path: vendor/bundle
key: ${{ runner.os }}-gem-${{ hashFiles('**/Gemfile.lock') }} key: ${{ runner.os }}-gem-${{ hashFiles('**/Gemfile.lock') }}
@@ -162,10 +128,7 @@ jobs:
bundle install --jobs 4 --retry 4 bundle install --jobs 4 --retry 4
yarn install yarn install
sudo apt-get update sudo apt-get update
sudo apt-get install -y libvips sudo apt-get install libvips
wget -O pdfium-linux.tgz "https://github.com/docusealco/pdfium-binaries/releases/latest/download/pdfium-linux-$(uname -m | sed 's/x86_64/x64/;s/aarch64/arm64/').tgz"
sudo tar -xzf pdfium-linux.tgz --strip-components=1 -C /usr/lib lib/libpdfium.so
rm -f pdfium-linux.tgz
- name: Run - name: Run
env: env:
RAILS_ENV: test RAILS_ENV: test
+5 -6
View File
@@ -7,8 +7,7 @@ on:
jobs: jobs:
build: build:
runs-on: ubuntu-24.04-arm runs-on: ubuntu-latest
timeout-minutes: 20
steps: steps:
- name: Checkout code - name: Checkout code
@@ -26,22 +25,22 @@ jobs:
tags: | tags: |
type=semver,pattern={{version}} type=semver,pattern={{version}}
- name: Set up QEMU - name: Set up QEMU
uses: docker/setup-qemu-action@v3 uses: docker/setup-qemu-action@v2
- name: Set up Docker Buildx - name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3 uses: docker/setup-buildx-action@v2
- name: Create .version file - name: Create .version file
run: echo ${{ github.ref_name }} > .version run: echo ${{ github.ref_name }} > .version
- name: Login to Docker Hub - name: Login to Docker Hub
uses: docker/login-action@v3 uses: docker/login-action@v2
with: with:
username: ${{ secrets.DOCKERHUB_USERNAME }} username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }} password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Build and push Docker image - name: Build and push Docker image
uses: docker/build-push-action@v6 uses: docker/build-push-action@v4
with: with:
context: . context: .
push: true push: true
-1
View File
@@ -1 +0,0 @@
--require rails_helper
+5 -11
View File
@@ -34,7 +34,6 @@ Metrics/MethodLength:
Max: 30 Max: 30
Exclude: Exclude:
- 'db/migrate/**' - 'db/migrate/**'
- 'spec/**/*'
Metrics/CyclomaticComplexity: Metrics/CyclomaticComplexity:
Max: 15 Max: 15
@@ -47,29 +46,24 @@ Layout/LineLength:
Metrics/AbcSize: Metrics/AbcSize:
Max: 45 Max: 45
Exclude:
- spec/**/*
Metrics/ModuleLength: Metrics/ModuleLength:
Max: 1000 Max: 500
Metrics/ClassLength: Metrics/ClassLength:
Max: 1000 Max: 500
RSpec/NestedGroups: RSpec/NestedGroups:
Max: 6 Max: 6
RSpec/MultipleExpectations: RSpec/MultipleExpectations:
Max: 25 Max: 20
RSpec/ExampleLength: RSpec/ExampleLength:
Max: 500 Max: 40
RSpec/MultipleMemoizedHelpers: RSpec/MultipleMemoizedHelpers:
Max: 15 Max: 6
Metrics/BlockNesting:
Max: 5
Rails/I18nLocaleTexts: Rails/I18nLocaleTexts:
Enabled: false Enabled: false
+9 -21
View File
@@ -1,21 +1,12 @@
FROM ruby:3.4.2-alpine AS download FROM ruby:3.3.3-alpine as fonts
WORKDIR /fonts WORKDIR /fonts
RUN apk --no-cache add fontforge wget && \ 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
wget https://github.com/satbyy/go-noto-universal/releases/download/v7.0/GoNotoKurrent-Regular.ttf && \
wget https://github.com/satbyy/go-noto-universal/releases/download/v7.0/GoNotoKurrent-Bold.ttf && \
wget https://github.com/impallari/DancingScript/raw/master/fonts/DancingScript-Regular.otf && \
wget https://cdn.jsdelivr.net/gh/notofonts/notofonts.github.io/fonts/NotoSansSymbols2/hinted/ttf/NotoSansSymbols2-Regular.ttf && \
wget https://github.com/Maxattax97/gnu-freefont/raw/master/ttf/FreeSans.ttf && \
wget https://github.com/impallari/DancingScript/raw/master/OFL.txt && \
wget -O pdfium-linux.tgz "https://github.com/docusealco/pdfium-binaries/releases/latest/download/pdfium-linux-$(uname -m | sed 's/x86_64/x64/;s/aarch64/arm64/').tgz" && \
mkdir -p /pdfium-linux && \
tar -xzf pdfium-linux.tgz -C /pdfium-linux
RUN fontforge -lang=py -c 'font1 = fontforge.open("FreeSans.ttf"); font2 = fontforge.open("NotoSansSymbols2-Regular.ttf"); font1.mergeFonts(font2); font1.generate("FreeSans.ttf")' RUN fontforge -lang=py -c 'font1 = fontforge.open("FreeSans.ttf"); font2 = fontforge.open("NotoSansSymbols2-Regular.ttf"); font1.mergeFonts(font2); font1.generate("FreeSans.ttf")'
FROM ruby:3.4.2-alpine AS webpack FROM ruby:3.3.3-alpine as webpack
ENV RAILS_ENV=production ENV RAILS_ENV=production
ENV NODE_ENV=production ENV NODE_ENV=production
@@ -41,7 +32,7 @@ COPY ./app/views ./app/views
RUN echo "gem 'shakapacker'" > Gemfile && ./bin/shakapacker RUN echo "gem 'shakapacker'" > Gemfile && ./bin/shakapacker
FROM ruby:3.4.2-alpine AS app FROM ruby:3.3.3-alpine as app
ENV RAILS_ENV=production ENV RAILS_ENV=production
ENV BUNDLE_WITHOUT="development:test" ENV BUNDLE_WITHOUT="development:test"
@@ -50,7 +41,7 @@ ENV OPENSSL_CONF=/app/openssl_legacy.cnf
WORKDIR /app WORKDIR /app
RUN echo '@edge https://dl-cdn.alpinelinux.org/alpine/edge/community' >> /etc/apk/repositories && apk add --no-cache sqlite-dev libpq-dev mariadb-dev vips-dev@edge redis libheif@edge vips-heif@edge gcompat ttf-freefont && mkdir /fonts && rm /usr/share/fonts/freefont/FreeSans.otf RUN apk add --no-cache sqlite-dev libpq-dev mariadb-dev vips-dev vips-poppler poppler-utils redis vips-heif gcompat ttf-freefont && mkdir /fonts && rm /usr/share/fonts/freefont/FreeSans.otf
RUN echo $'.include = /etc/ssl/openssl.cnf\n\ RUN echo $'.include = /etc/ssl/openssl.cnf\n\
\n\ \n\
@@ -66,23 +57,20 @@ activate = 1' >> /app/openssl_legacy.cnf
COPY ./Gemfile ./Gemfile.lock ./ COPY ./Gemfile ./Gemfile.lock ./
RUN apk add --no-cache build-base && bundle install && apk del --no-cache build-base && rm -rf ~/.bundle /usr/local/bundle/cache && ruby -e "puts Dir['/usr/local/bundle/**/{spec,rdoc,resources/shared,resources/collation,resources/locales}']" | xargs rm -rf RUN apk add --no-cache build-base && bundle install && apk del build-base && rm -rf ~/.bundle /usr/local/bundle/cache && ruby -e "puts Dir['/usr/local/bundle/**/{spec,rdoc,resources/shared,resources/collation,resources/locales}']" | xargs rm -rf
COPY ./bin ./bin COPY ./bin ./bin
COPY ./app ./app COPY ./app ./app
COPY ./config ./config COPY ./config ./config
COPY ./db/migrate ./db/migrate COPY ./db ./db
COPY ./log ./log COPY ./log ./log
COPY ./lib ./lib COPY ./lib ./lib
COPY ./public ./public COPY ./public ./public
COPY ./tmp ./tmp COPY ./tmp ./tmp
COPY LICENSE README.md Rakefile config.ru .version ./ COPY LICENSE README.md Rakefile config.ru .version ./
COPY .version ./public/version
COPY --from=download /fonts/GoNotoKurrent-Regular.ttf /fonts/GoNotoKurrent-Bold.ttf /fonts/DancingScript-Regular.otf /fonts/OFL.txt /fonts COPY --from=fonts /fonts/GoNotoKurrent-Regular.ttf /fonts/GoNotoKurrent-Bold.ttf /fonts/DancingScript-Regular.otf /fonts/OFL.txt /fonts
COPY --from=download /fonts/FreeSans.ttf /usr/share/fonts/freefont COPY --from=fonts /fonts/FreeSans.ttf /usr/share/fonts/freefont
COPY --from=download /pdfium-linux/lib/libpdfium.so /usr/lib/libpdfium.so
COPY --from=download /pdfium-linux/licenses/pdfium.txt /usr/lib/libpdfium-LICENSE.txt
COPY --from=webpack /app/public/packs ./public/packs COPY --from=webpack /app/public/packs ./public/packs
RUN ln -s /fonts /app/public/fonts RUN ln -s /fonts /app/public/fonts
+3 -6
View File
@@ -2,7 +2,7 @@
source 'https://rubygems.org' source 'https://rubygems.org'
ruby '3.4.2' ruby '3.3.3'
gem 'arabic-letter-connector', require: 'arabic-letter-connector/logic' gem 'arabic-letter-connector', require: 'arabic-letter-connector/logic'
gem 'aws-sdk-s3', require: false gem 'aws-sdk-s3', require: false
@@ -34,19 +34,19 @@ gem 'rails'
gem 'rails_autolink' gem 'rails_autolink'
gem 'rails-i18n' gem 'rails-i18n'
gem 'rotp' gem 'rotp'
gem 'rouge', require: false
gem 'rqrcode' gem 'rqrcode'
gem 'ruby-vips' gem 'ruby-vips'
gem 'rubyXL' gem 'rubyXL'
gem 'shakapacker' gem 'shakapacker'
gem 'sidekiq' gem 'sidekiq'
gem 'sqlite3', require: false gem 'sqlite3', require: false, force_ruby_platform: true
gem 'strip_attributes' gem 'strip_attributes'
gem 'turbo-rails' gem 'turbo-rails'
gem 'twitter_cldr', require: false gem 'twitter_cldr', require: false
gem 'tzinfo-data' gem 'tzinfo-data'
group :development, :test do group :development, :test do
gem 'annotate'
gem 'better_html' gem 'better_html'
gem 'bullet' gem 'bullet'
gem 'debug' gem 'debug'
@@ -63,9 +63,6 @@ group :development, :test do
end end
group :development do group :development do
gem 'annotaterb'
gem 'brakeman', require: false
gem 'foreman', require: false
gem 'letter_opener_web' gem 'letter_opener_web'
gem 'web-console' gem 'web-console'
end end
+225 -256
View File
@@ -1,100 +1,105 @@
GEM GEM
remote: https://rubygems.org/ remote: https://rubygems.org/
specs: specs:
actioncable (8.0.1) actioncable (7.1.3.4)
actionpack (= 8.0.1) actionpack (= 7.1.3.4)
activesupport (= 8.0.1) activesupport (= 7.1.3.4)
nio4r (~> 2.0) nio4r (~> 2.0)
websocket-driver (>= 0.6.1) websocket-driver (>= 0.6.1)
zeitwerk (~> 2.6) zeitwerk (~> 2.6)
actionmailbox (8.0.1) actionmailbox (7.1.3.4)
actionpack (= 8.0.1) actionpack (= 7.1.3.4)
activejob (= 8.0.1) activejob (= 7.1.3.4)
activerecord (= 8.0.1) activerecord (= 7.1.3.4)
activestorage (= 8.0.1) activestorage (= 7.1.3.4)
activesupport (= 8.0.1) activesupport (= 7.1.3.4)
mail (>= 2.8.0) mail (>= 2.7.1)
actionmailer (8.0.1) net-imap
actionpack (= 8.0.1) net-pop
actionview (= 8.0.1) net-smtp
activejob (= 8.0.1) actionmailer (7.1.3.4)
activesupport (= 8.0.1) actionpack (= 7.1.3.4)
mail (>= 2.8.0) 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) rails-dom-testing (~> 2.2)
actionpack (8.0.1) actionpack (7.1.3.4)
actionview (= 8.0.1) actionview (= 7.1.3.4)
activesupport (= 8.0.1) activesupport (= 7.1.3.4)
nokogiri (>= 1.8.5) nokogiri (>= 1.8.5)
racc
rack (>= 2.2.4) rack (>= 2.2.4)
rack-session (>= 1.0.1) rack-session (>= 1.0.1)
rack-test (>= 0.6.3) rack-test (>= 0.6.3)
rails-dom-testing (~> 2.2) rails-dom-testing (~> 2.2)
rails-html-sanitizer (~> 1.6) rails-html-sanitizer (~> 1.6)
useragent (~> 0.16) actiontext (7.1.3.4)
actiontext (8.0.1) actionpack (= 7.1.3.4)
actionpack (= 8.0.1) activerecord (= 7.1.3.4)
activerecord (= 8.0.1) activestorage (= 7.1.3.4)
activestorage (= 8.0.1) activesupport (= 7.1.3.4)
activesupport (= 8.0.1)
globalid (>= 0.6.0) globalid (>= 0.6.0)
nokogiri (>= 1.8.5) nokogiri (>= 1.8.5)
actionview (8.0.1) actionview (7.1.3.4)
activesupport (= 8.0.1) activesupport (= 7.1.3.4)
builder (~> 3.1) builder (~> 3.1)
erubi (~> 1.11) erubi (~> 1.11)
rails-dom-testing (~> 2.2) rails-dom-testing (~> 2.2)
rails-html-sanitizer (~> 1.6) rails-html-sanitizer (~> 1.6)
activejob (8.0.1) activejob (7.1.3.4)
activesupport (= 8.0.1) activesupport (= 7.1.3.4)
globalid (>= 0.3.6) globalid (>= 0.3.6)
activemodel (8.0.1) activemodel (7.1.3.4)
activesupport (= 8.0.1) activesupport (= 7.1.3.4)
activerecord (8.0.1) activerecord (7.1.3.4)
activemodel (= 8.0.1) activemodel (= 7.1.3.4)
activesupport (= 8.0.1) activesupport (= 7.1.3.4)
timeout (>= 0.4.0) timeout (>= 0.4.0)
activestorage (8.0.1) activestorage (7.1.3.4)
actionpack (= 8.0.1) actionpack (= 7.1.3.4)
activejob (= 8.0.1) activejob (= 7.1.3.4)
activerecord (= 8.0.1) activerecord (= 7.1.3.4)
activesupport (= 8.0.1) activesupport (= 7.1.3.4)
marcel (~> 1.0) marcel (~> 1.0)
activesupport (8.0.1) activesupport (7.1.3.4)
base64 base64
benchmark (>= 0.3)
bigdecimal bigdecimal
concurrent-ruby (~> 1.0, >= 1.3.1) concurrent-ruby (~> 1.0, >= 1.0.2)
connection_pool (>= 2.2.5) connection_pool (>= 2.2.5)
drb drb
i18n (>= 1.6, < 2) i18n (>= 1.6, < 2)
logger (>= 1.4.2)
minitest (>= 5.1) minitest (>= 5.1)
securerandom (>= 0.3) mutex_m
tzinfo (~> 2.0, >= 2.0.5) tzinfo (~> 2.0)
uri (>= 0.13.1) addressable (2.8.6)
addressable (2.8.7) public_suffix (>= 2.0.2, < 6.0)
public_suffix (>= 2.0.2, < 7.0) annotate (3.2.0)
annotaterb (4.14.0) activerecord (>= 3.2, < 8.0)
rake (>= 10.4, < 14.0)
arabic-letter-connector (0.1.1) arabic-letter-connector (0.1.1)
ast (2.4.2) ast (2.4.2)
aws-eventstream (1.3.0) aws-eventstream (1.3.0)
aws-partitions (1.1027.0) aws-partitions (1.913.0)
aws-sdk-core (3.214.0) aws-sdk-core (3.191.6)
aws-eventstream (~> 1, >= 1.3.0) aws-eventstream (~> 1, >= 1.3.0)
aws-partitions (~> 1, >= 1.992.0) aws-partitions (~> 1, >= 1.651.0)
aws-sigv4 (~> 1.9) aws-sigv4 (~> 1.8)
jmespath (~> 1, >= 1.6.1) jmespath (~> 1, >= 1.6.1)
aws-sdk-kms (1.96.0) aws-sdk-kms (1.79.0)
aws-sdk-core (~> 3, >= 3.210.0) aws-sdk-core (~> 3, >= 3.191.0)
aws-sigv4 (~> 1.5) aws-sigv4 (~> 1.1)
aws-sdk-s3 (1.176.1) aws-sdk-s3 (1.146.1)
aws-sdk-core (~> 3, >= 3.210.0) aws-sdk-core (~> 3, >= 3.191.0)
aws-sdk-kms (~> 1) aws-sdk-kms (~> 1)
aws-sigv4 (~> 1.5) aws-sigv4 (~> 1.8)
aws-sdk-secretsmanager (1.110.0) aws-sdk-secretsmanager (1.91.0)
aws-sdk-core (~> 3, >= 3.210.0) aws-sdk-core (~> 3, >= 3.191.0)
aws-sigv4 (~> 1.5) aws-sigv4 (~> 1.1)
aws-sigv4 (1.10.1) aws-sigv4 (1.8.0)
aws-eventstream (~> 1, >= 1.0.2) aws-eventstream (~> 1, >= 1.0.2)
azure-storage-blob (2.0.3) azure-storage-blob (2.0.3)
azure-storage-common (~> 2.0) azure-storage-common (~> 2.0)
@@ -106,7 +111,6 @@ GEM
nokogiri (~> 1, >= 1.10.8) nokogiri (~> 1, >= 1.10.8)
base64 (0.2.0) base64 (0.2.0)
bcrypt (3.1.20) bcrypt (3.1.20)
benchmark (0.4.0)
better_html (2.1.1) better_html (2.1.1)
actionview (>= 6.0) actionview (>= 6.0)
activesupport (>= 6.0) activesupport (>= 6.0)
@@ -116,16 +120,14 @@ GEM
smart_properties smart_properties
bigdecimal (3.1.8) bigdecimal (3.1.8)
bindex (0.8.1) bindex (0.8.1)
bootsnap (1.18.4) bootsnap (1.18.3)
msgpack (~> 1.2) msgpack (~> 1.2)
brakeman (7.0.0)
racc
builder (3.3.0) builder (3.3.0)
bullet (8.0.0) bullet (7.1.6)
activesupport (>= 3.0.0) activesupport (>= 3.0.0)
uniform_notifier (~> 1.11) uniform_notifier (~> 1.11)
camertron-eprun (1.1.1) camertron-eprun (1.1.1)
cancancan (3.6.1) cancancan (3.5.0)
capybara (3.40.0) capybara (3.40.0)
addressable addressable
matrix matrix
@@ -135,8 +137,7 @@ GEM
rack-test (>= 0.6.3) rack-test (>= 0.6.3)
regexp_parser (>= 1.5, < 3.0) regexp_parser (>= 1.5, < 3.0)
xpath (~> 3.2) xpath (~> 3.2)
childprocess (5.1.0) childprocess (5.0.0)
logger (~> 1.5)
chunky_png (1.4.0) chunky_png (1.4.0)
cldr-plurals-runtime-rb (1.1.0) cldr-plurals-runtime-rb (1.1.0)
cmdparse (3.0.7) cmdparse (3.0.7)
@@ -147,14 +148,14 @@ GEM
bigdecimal bigdecimal
rexml rexml
crass (1.0.6) crass (1.0.6)
css_parser (1.21.0) css_parser (1.17.1)
addressable addressable
csv (3.3.2) csv (3.3.0)
cuprite (0.15.1) cuprite (0.15)
capybara (~> 3.0) capybara (~> 3.0)
ferrum (~> 0.15.0) ferrum (~> 0.14.0)
date (3.4.1) date (3.3.4)
debug (1.10.0) debug (1.9.2)
irb (~> 1.10) irb (~> 1.10)
reline (>= 0.3.8) reline (>= 0.3.8)
declarative (0.0.20) declarative (0.0.20)
@@ -164,34 +165,34 @@ GEM
railties (>= 4.1.0) railties (>= 4.1.0)
responders responders
warden (~> 1.2.3) warden (~> 1.2.3)
devise-two-factor (6.1.0) devise-two-factor (5.0.0)
activesupport (>= 7.0, < 8.1) activesupport (~> 7.0)
devise (~> 4.0) devise (~> 4.0)
railties (>= 7.0, < 8.1) railties (~> 7.0)
rotp (~> 6.0) rotp (~> 6.0)
diff-lcs (1.5.1) diff-lcs (1.5.1)
digest-crc (0.6.5) digest-crc (0.6.5)
rake (>= 12.0.0, < 14.0.0) rake (>= 12.0.0, < 14.0.0)
docile (1.4.1) docile (1.4.0)
dotenv (3.1.7) dotenv (3.1.0)
drb (2.2.1) drb (2.2.1)
email_typo (0.2.3) email_typo (0.2.3)
erb_lint (0.7.0) erb_lint (0.5.0)
activesupport activesupport
better_html (>= 2.0.1) better_html (>= 2.0.1)
parser (>= 2.7.1.4) parser (>= 2.7.1.4)
rainbow rainbow
rubocop (>= 1) rubocop
smart_properties smart_properties
erubi (1.13.1) erubi (1.13.0)
factory_bot (6.5.0) factory_bot (6.4.6)
activesupport (>= 5.0.0) activesupport (>= 5.0.0)
factory_bot_rails (6.4.4) factory_bot_rails (6.4.3)
factory_bot (~> 6.5) factory_bot (~> 6.4)
railties (>= 5.0.0) railties (>= 5.0.0)
faker (3.5.1) faker (3.3.1)
i18n (>= 1.8.11, < 2) i18n (>= 1.8.11, < 2)
faraday (1.10.4) faraday (1.10.3)
faraday-em_http (~> 1.0) faraday-em_http (~> 1.0)
faraday-em_synchrony (~> 1.0) faraday-em_synchrony (~> 1.0)
faraday-excon (~> 1.1) faraday-excon (~> 1.1)
@@ -209,104 +210,95 @@ GEM
faraday-follow_redirects (0.3.0) faraday-follow_redirects (0.3.0)
faraday (>= 1, < 3) faraday (>= 1, < 3)
faraday-httpclient (1.0.1) faraday-httpclient (1.0.1)
faraday-multipart (1.1.0) faraday-multipart (1.0.4)
multipart-post (~> 2.0) multipart-post (~> 2)
faraday-net_http (1.0.2) faraday-net_http (1.0.1)
faraday-net_http_persistent (1.2.0) faraday-net_http_persistent (1.2.0)
faraday-patron (1.0.0) faraday-patron (1.0.0)
faraday-rack (1.0.0) faraday-rack (1.0.0)
faraday-retry (1.0.3) faraday-retry (1.0.3)
faraday_middleware (1.2.1) faraday_middleware (1.2.0)
faraday (~> 1.0) faraday (~> 1.0)
ferrum (0.15) ferrum (0.14)
addressable (~> 2.5) addressable (~> 2.5)
concurrent-ruby (~> 1.1) concurrent-ruby (~> 1.1)
webrick (~> 1.7) webrick (~> 1.7)
websocket-driver (~> 0.7) websocket-driver (>= 0.6, < 0.8)
ffi (1.17.1) ffi (1.16.3)
ffi (1.17.1-aarch64-linux-gnu)
ffi (1.17.1-aarch64-linux-musl)
ffi (1.17.1-arm64-darwin)
ffi (1.17.1-x86_64-linux-gnu)
ffi (1.17.1-x86_64-linux-musl)
foreman (0.88.1)
geom2d (0.4.1) geom2d (0.4.1)
globalid (1.2.1) globalid (1.2.1)
activesupport (>= 6.1) activesupport (>= 6.1)
google-apis-core (0.15.1) google-apis-core (0.14.1)
addressable (~> 2.5, >= 2.5.1) addressable (~> 2.5, >= 2.5.1)
googleauth (~> 1.9) googleauth (~> 1.9)
httpclient (>= 2.8.3, < 3.a) httpclient (>= 2.8.1, < 3.a)
mini_mime (~> 1.0) mini_mime (~> 1.0)
mutex_m
representable (~> 3.0) representable (~> 3.0)
retriable (>= 2.0, < 4.a) retriable (>= 2.0, < 4.a)
google-apis-iamcredentials_v1 (0.22.0) rexml
google-apis-core (>= 0.15.0, < 2.a) google-apis-iamcredentials_v1 (0.20.0)
google-apis-storage_v1 (0.49.0) google-apis-core (>= 0.14.0, < 2.a)
google-apis-core (>= 0.15.0, < 2.a) google-apis-storage_v1 (0.37.0)
google-cloud-core (1.7.1) google-apis-core (>= 0.14.0, < 2.a)
google-cloud-core (1.7.0)
google-cloud-env (>= 1.0, < 3.a) google-cloud-env (>= 1.0, < 3.a)
google-cloud-errors (~> 1.0) google-cloud-errors (~> 1.0)
google-cloud-env (2.2.1) google-cloud-env (2.1.1)
faraday (>= 1.0, < 3.a) faraday (>= 1.0, < 3.a)
google-cloud-errors (1.4.0) google-cloud-errors (1.4.0)
google-cloud-storage (1.54.0) google-cloud-storage (1.49.0)
addressable (~> 2.8) addressable (~> 2.8)
digest-crc (~> 0.4) digest-crc (~> 0.4)
google-apis-core (~> 0.13) google-apis-core (~> 0.13)
google-apis-iamcredentials_v1 (~> 0.18) google-apis-iamcredentials_v1 (~> 0.18)
google-apis-storage_v1 (~> 0.38) google-apis-storage_v1 (~> 0.33)
google-cloud-core (~> 1.6) google-cloud-core (~> 1.6)
googleauth (~> 1.9) googleauth (~> 1.9)
mini_mime (~> 1.0) mini_mime (~> 1.0)
google-logging-utils (0.1.0) googleauth (1.11.0)
googleauth (1.12.2)
faraday (>= 1.0, < 3.a) faraday (>= 1.0, < 3.a)
google-cloud-env (~> 2.2) google-cloud-env (~> 2.1)
google-logging-utils (~> 0.1)
jwt (>= 1.4, < 3.0) jwt (>= 1.4, < 3.0)
multi_json (~> 1.11) multi_json (~> 1.11)
os (>= 0.9, < 2.0) os (>= 0.9, < 2.0)
signet (>= 0.16, < 2.a) signet (>= 0.16, < 2.a)
hashdiff (1.1.2) hashdiff (1.1.0)
hexapdf (1.0.3) hexapdf (0.42.0)
cmdparse (~> 3.0, >= 3.0.3) cmdparse (~> 3.0, >= 3.0.3)
geom2d (~> 0.4, >= 0.4.1) geom2d (~> 0.4, >= 0.4.1)
openssl (>= 2.2.1) openssl (>= 2.2.1)
htmlentities (4.3.4) htmlentities (4.3.4)
httpclient (2.8.3) httpclient (2.8.3)
i18n (1.14.6) i18n (1.14.5)
concurrent-ruby (~> 1.0) concurrent-ruby (~> 1.0)
image_processing (1.13.0) image_processing (1.12.2)
mini_magick (>= 4.9.5, < 5) mini_magick (>= 4.9.5, < 5)
ruby-vips (>= 2.0.17, < 3) ruby-vips (>= 2.0.17, < 3)
io-console (0.8.0) io-console (0.7.2)
irb (1.14.3) irb (1.13.1)
rdoc (>= 4.0.0) rdoc (>= 4.0.0)
reline (>= 0.4.2) reline (>= 0.4.2)
jmespath (1.6.2) jmespath (1.6.2)
json (2.9.1) json (2.7.2)
jwt (2.9.3) jwt (2.8.1)
base64 base64
language_server-protocol (3.17.0.3) language_server-protocol (3.17.0.3)
launchy (3.0.1) launchy (3.0.0)
addressable (~> 2.8) addressable (~> 2.8)
childprocess (~> 5.0) childprocess (~> 5.0)
letter_opener (1.10.0) letter_opener (1.10.0)
launchy (>= 2.2, < 4) launchy (>= 2.2, < 4)
letter_opener_web (3.0.0) letter_opener_web (2.0.0)
actionmailer (>= 6.1) actionmailer (>= 5.2)
letter_opener (~> 1.9) letter_opener (~> 1.7)
railties (>= 6.1) railties (>= 5.2)
rexml rexml
logger (1.6.4)
lograge (0.14.0) lograge (0.14.0)
actionpack (>= 4) actionpack (>= 4)
activesupport (>= 4) activesupport (>= 4)
railties (>= 4) railties (>= 4)
request_store (~> 1.0) request_store (~> 1.0)
loofah (2.23.1) loofah (2.22.0)
crass (~> 1.0.2) crass (~> 1.0.2)
nokogiri (>= 1.12.0) nokogiri (>= 1.12.0)
mail (2.8.1) mail (2.8.1)
@@ -317,57 +309,48 @@ GEM
marcel (1.0.4) marcel (1.0.4)
matrix (0.4.2) matrix (0.4.2)
method_source (1.1.0) method_source (1.1.0)
mini_magick (4.13.2) mini_magick (4.12.0)
mini_mime (1.1.5) mini_mime (1.1.5)
mini_portile2 (2.8.8) mini_portile2 (2.8.7)
minitest (5.25.4) minitest (5.24.1)
msgpack (1.7.5) msgpack (1.7.2)
multi_json (1.15.0) multi_json (1.15.0)
multipart-post (2.4.1) multipart-post (2.4.0)
mutex_m (0.3.0) mutex_m (0.2.0)
mysql2 (0.5.6) mysql2 (0.5.6)
net-http-persistent (4.0.5) net-http-persistent (4.0.2)
connection_pool (~> 2.2) connection_pool (~> 2.2)
net-imap (0.5.6) net-imap (0.4.14)
date date
net-protocol net-protocol
net-pop (0.1.2) net-pop (0.1.2)
net-protocol
net-protocol (0.2.2) net-protocol (0.2.2)
timeout timeout
net-smtp (0.5.0) net-smtp (0.5.0)
net-protocol net-protocol
nio4r (2.7.4) nio4r (2.7.1)
nokogiri (1.18.8) nokogiri (1.16.7)
mini_portile2 (~> 2.8.2) mini_portile2 (~> 2.8.2)
racc (~> 1.4) racc (~> 1.4)
nokogiri (1.18.8-aarch64-linux-gnu) nokogiri (1.16.7-aarch64-linux)
racc (~> 1.4) racc (~> 1.4)
nokogiri (1.18.8-aarch64-linux-musl) nokogiri (1.16.7-x86_64-linux)
racc (~> 1.4) racc (~> 1.4)
nokogiri (1.18.8-arm64-darwin) oj (3.16.3)
racc (~> 1.4)
nokogiri (1.18.8-x86_64-linux-gnu)
racc (~> 1.4)
nokogiri (1.18.8-x86_64-linux-musl)
racc (~> 1.4)
oj (3.16.8)
bigdecimal (>= 3.0) bigdecimal (>= 3.0)
ostruct (>= 0.2) openssl (3.2.0)
openssl (3.3.0)
orm_adapter (0.5.0) orm_adapter (0.5.0)
os (1.1.4) os (1.1.4)
ostruct (0.6.1)
package_json (0.1.0) package_json (0.1.0)
pagy (9.3.3) pagy (8.1.2)
parallel (1.26.3) parallel (1.25.1)
parser (3.3.6.0) parser (3.3.4.0)
ast (~> 2.4.1) ast (~> 2.4.1)
racc racc
pg (1.5.9) pg (1.5.6)
premailer (1.27.0) premailer (1.23.0)
addressable addressable
css_parser (>= 1.19.0) css_parser (>= 1.12.0)
htmlentities (>= 4.0.0) htmlentities (>= 4.0.0)
premailer-rails (1.12.0) premailer-rails (1.12.0)
actionmailer (>= 3) actionmailer (>= 3)
@@ -375,150 +358,149 @@ GEM
premailer (~> 1.7, >= 1.7.9) premailer (~> 1.7, >= 1.7.9)
pretender (0.5.0) pretender (0.5.0)
actionpack (>= 6.1) actionpack (>= 6.1)
pry (0.15.0) pry (0.14.2)
coderay (~> 1.1) coderay (~> 1.1)
method_source (~> 1.0) method_source (~> 1.0)
pry-rails (0.3.11) pry-rails (0.3.9)
pry (>= 0.13.0) pry (>= 0.10.4)
psych (5.2.2) psych (5.1.2)
date
stringio stringio
public_suffix (6.0.1) public_suffix (5.0.5)
puma (6.5.0) puma (6.4.2)
nio4r (~> 2.0) nio4r (~> 2.0)
racc (1.8.1) racc (1.8.1)
rack (3.1.16) rack (3.1.7)
rack-proxy (0.7.7) rack-proxy (0.7.7)
rack rack
rack-session (2.0.0) rack-session (2.0.0)
rack (>= 3.0.0) rack (>= 3.0.0)
rack-test (2.1.0) rack-test (2.1.0)
rack (>= 1.3) rack (>= 1.3)
rackup (2.2.1) rackup (2.1.0)
rack (>= 3) rack (>= 3)
rails (8.0.1) webrick (~> 1.8)
actioncable (= 8.0.1) rails (7.1.3.4)
actionmailbox (= 8.0.1) actioncable (= 7.1.3.4)
actionmailer (= 8.0.1) actionmailbox (= 7.1.3.4)
actionpack (= 8.0.1) actionmailer (= 7.1.3.4)
actiontext (= 8.0.1) actionpack (= 7.1.3.4)
actionview (= 8.0.1) actiontext (= 7.1.3.4)
activejob (= 8.0.1) actionview (= 7.1.3.4)
activemodel (= 8.0.1) activejob (= 7.1.3.4)
activerecord (= 8.0.1) activemodel (= 7.1.3.4)
activestorage (= 8.0.1) activerecord (= 7.1.3.4)
activesupport (= 8.0.1) activestorage (= 7.1.3.4)
activesupport (= 7.1.3.4)
bundler (>= 1.15.0) bundler (>= 1.15.0)
railties (= 8.0.1) railties (= 7.1.3.4)
rails-dom-testing (2.2.0) rails-dom-testing (2.2.0)
activesupport (>= 5.0.0) activesupport (>= 5.0.0)
minitest minitest
nokogiri (>= 1.6) nokogiri (>= 1.6)
rails-html-sanitizer (1.6.2) rails-html-sanitizer (1.6.0)
loofah (~> 2.21) loofah (~> 2.21)
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) nokogiri (~> 1.14)
rails-i18n (8.0.1) rails-i18n (7.0.9)
i18n (>= 0.7, < 2) i18n (>= 0.7, < 2)
railties (>= 8.0.0, < 9) railties (>= 6.0.0, < 8)
rails_autolink (1.1.8) rails_autolink (1.1.8)
actionview (> 3.1) actionview (> 3.1)
activesupport (> 3.1) activesupport (> 3.1)
railties (> 3.1) railties (> 3.1)
railties (8.0.1) railties (7.1.3.4)
actionpack (= 8.0.1) actionpack (= 7.1.3.4)
activesupport (= 8.0.1) activesupport (= 7.1.3.4)
irb (~> 1.13) irb
rackup (>= 1.0.0) rackup (>= 1.0.0)
rake (>= 12.2) rake (>= 12.2)
thor (~> 1.0, >= 1.2.2) thor (~> 1.0, >= 1.2.2)
zeitwerk (~> 2.6) zeitwerk (~> 2.6)
rainbow (3.1.1) rainbow (3.1.1)
rake (13.2.1) rake (13.2.1)
rdoc (6.10.0) rdoc (6.6.3.1)
psych (>= 4.0.0) psych (>= 4.0.0)
redis-client (0.23.0) redis-client (0.22.2)
connection_pool connection_pool
regexp_parser (2.9.3) regexp_parser (2.9.2)
reline (0.6.0) reline (0.5.7)
io-console (~> 0.5) io-console (~> 0.5)
representable (3.2.0) representable (3.2.0)
declarative (< 0.1.0) declarative (< 0.1.0)
trailblazer-option (>= 0.1.1, < 0.2.0) trailblazer-option (>= 0.1.1, < 0.2.0)
uber (< 0.2.0) uber (< 0.2.0)
request_store (1.7.0) request_store (1.6.0)
rack (>= 1.4) rack (>= 1.4)
responders (3.1.1) responders (3.1.1)
actionpack (>= 5.2) actionpack (>= 5.2)
railties (>= 5.2) railties (>= 5.2)
retriable (3.1.2) retriable (3.1.2)
rexml (3.4.0) rexml (3.3.3)
strscan
rotp (6.3.0) rotp (6.3.0)
rouge (4.5.2)
rqrcode (2.2.0) rqrcode (2.2.0)
chunky_png (~> 1.0) chunky_png (~> 1.0)
rqrcode_core (~> 1.0) rqrcode_core (~> 1.0)
rqrcode_core (1.2.0) rqrcode_core (1.2.0)
rspec-core (3.13.2) rspec-core (3.13.0)
rspec-support (~> 3.13.0) rspec-support (~> 3.13.0)
rspec-expectations (3.13.3) rspec-expectations (3.13.0)
diff-lcs (>= 1.2.0, < 2.0) diff-lcs (>= 1.2.0, < 2.0)
rspec-support (~> 3.13.0) rspec-support (~> 3.13.0)
rspec-mocks (3.13.2) rspec-mocks (3.13.0)
diff-lcs (>= 1.2.0, < 2.0) diff-lcs (>= 1.2.0, < 2.0)
rspec-support (~> 3.13.0) rspec-support (~> 3.13.0)
rspec-rails (7.1.0) rspec-rails (6.1.2)
actionpack (>= 7.0) actionpack (>= 6.1)
activesupport (>= 7.0) activesupport (>= 6.1)
railties (>= 7.0) railties (>= 6.1)
rspec-core (~> 3.13) rspec-core (~> 3.13)
rspec-expectations (~> 3.13) rspec-expectations (~> 3.13)
rspec-mocks (~> 3.13) rspec-mocks (~> 3.13)
rspec-support (~> 3.13) rspec-support (~> 3.13)
rspec-support (3.13.2) rspec-support (3.13.1)
rubocop (1.69.2) rubocop (1.65.0)
json (~> 2.3) json (~> 2.3)
language_server-protocol (>= 3.17.0) language_server-protocol (>= 3.17.0)
parallel (~> 1.10) parallel (~> 1.10)
parser (>= 3.3.0.2) parser (>= 3.3.0.2)
rainbow (>= 2.2.2, < 4.0) rainbow (>= 2.2.2, < 4.0)
regexp_parser (>= 2.9.3, < 3.0) regexp_parser (>= 2.4, < 3.0)
rubocop-ast (>= 1.36.2, < 2.0) rexml (>= 3.2.5, < 4.0)
rubocop-ast (>= 1.31.1, < 2.0)
ruby-progressbar (~> 1.7) ruby-progressbar (~> 1.7)
unicode-display_width (>= 2.4.0, < 4.0) unicode-display_width (>= 2.4.0, < 3.0)
rubocop-ast (1.37.0) rubocop-ast (1.31.3)
parser (>= 3.3.1.0) parser (>= 3.3.1.0)
rubocop-performance (1.23.0) rubocop-performance (1.21.0)
rubocop (>= 1.48.1, < 2.0) rubocop (>= 1.48.1, < 2.0)
rubocop-ast (>= 1.31.1, < 2.0) rubocop-ast (>= 1.31.1, < 2.0)
rubocop-rails (2.27.0) rubocop-rails (2.24.1)
activesupport (>= 4.2.0) activesupport (>= 4.2.0)
rack (>= 1.1) rack (>= 1.1)
rubocop (>= 1.52.0, < 2.0) rubocop (>= 1.33.0, < 2.0)
rubocop-ast (>= 1.31.1, < 2.0) rubocop-ast (>= 1.31.1, < 2.0)
rubocop-rspec (3.3.0) rubocop-rspec (3.0.3)
rubocop (~> 1.61) rubocop (~> 1.61)
ruby-progressbar (1.13.0) ruby-progressbar (1.13.0)
ruby-vips (2.2.2) ruby-vips (2.2.1)
ffi (~> 1.12) ffi (~> 1.12)
logger
ruby2_keywords (0.0.5) ruby2_keywords (0.0.5)
rubyXL (3.4.33) rubyXL (3.4.27)
nokogiri (>= 1.10.8) nokogiri (>= 1.10.8)
rubyzip (>= 1.3.0) rubyzip (>= 1.3.0)
rubyzip (2.3.2) rubyzip (2.3.2)
securerandom (0.4.1) semantic_range (3.0.0)
semantic_range (3.1.0) shakapacker (8.0.0)
shakapacker (8.0.2)
activesupport (>= 5.2) activesupport (>= 5.2)
package_json package_json
rack-proxy (>= 0.6.1) rack-proxy (>= 0.6.1)
railties (>= 5.2) railties (>= 5.2)
semantic_range (>= 2.3.0) semantic_range (>= 2.3.0)
sidekiq (7.3.7) sidekiq (7.2.4)
concurrent-ruby (< 2)
connection_pool (>= 2.3.0) connection_pool (>= 2.3.0)
logger
rack (>= 2.2.4) rack (>= 2.2.4)
redis-client (>= 0.22.2) redis-client (>= 0.19.0)
signet (0.19.0) signet (0.19.0)
addressable (~> 2.8) addressable (~> 2.8)
faraday (>= 0.17.5, < 3.a) faraday (>= 0.17.5, < 3.a)
@@ -528,40 +510,33 @@ GEM
docile (~> 1.1) docile (~> 1.1)
simplecov-html (~> 0.11) simplecov-html (~> 0.11)
simplecov_json_formatter (~> 0.1) simplecov_json_formatter (~> 0.1)
simplecov-html (0.13.1) simplecov-html (0.12.3)
simplecov_json_formatter (0.1.4) simplecov_json_formatter (0.1.4)
smart_properties (1.17.0) smart_properties (1.17.0)
sqlite3 (2.5.0) sqlite3 (1.7.3)
mini_portile2 (~> 2.8.0) mini_portile2 (~> 2.8.0)
sqlite3 (2.5.0-aarch64-linux-gnu) stringio (3.1.0)
sqlite3 (2.5.0-aarch64-linux-musl) strip_attributes (1.13.0)
sqlite3 (2.5.0-arm64-darwin) activemodel (>= 3.0, < 8.0)
sqlite3 (2.5.0-x86_64-linux-gnu) strscan (3.1.0)
sqlite3 (2.5.0-x86_64-linux-musl) thor (1.3.1)
stringio (3.1.2) timeout (0.4.1)
strip_attributes (1.14.1)
activemodel (>= 3.0, < 9.0)
thor (1.3.2)
timeout (0.4.3)
trailblazer-option (0.1.2) trailblazer-option (0.1.2)
turbo-rails (2.0.11) turbo-rails (2.0.5)
actionpack (>= 6.0.0) actionpack (>= 6.0.0)
activejob (>= 6.0.0)
railties (>= 6.0.0) railties (>= 6.0.0)
twitter_cldr (6.12.1) twitter_cldr (6.12.0)
camertron-eprun camertron-eprun
cldr-plurals-runtime-rb (~> 1.1) cldr-plurals-runtime-rb (~> 1.1)
tzinfo tzinfo
tzinfo (2.0.6) tzinfo (2.0.6)
concurrent-ruby (~> 1.0) concurrent-ruby (~> 1.0)
tzinfo-data (1.2024.2) tzinfo-data (1.2024.1)
tzinfo (>= 1.0.0) tzinfo (>= 1.0.0)
uber (0.1.0) uber (0.1.0)
unicode-display_width (3.1.2) unicode-display_width (2.5.0)
unicode-emoji (~> 4.0, >= 4.0.4)
unicode-emoji (4.0.4)
uniform_notifier (1.16.0) uniform_notifier (1.16.0)
uri (1.0.3)
useragent (0.16.11)
warden (1.2.9) warden (1.2.9)
rack (>= 2.0.9) rack (>= 2.0.9)
web-console (4.2.1) web-console (4.2.1)
@@ -569,35 +544,31 @@ GEM
activemodel (>= 6.0.0) activemodel (>= 6.0.0)
bindex (>= 0.4.0) bindex (>= 0.4.0)
railties (>= 6.0.0) railties (>= 6.0.0)
webmock (3.24.0) webmock (3.23.0)
addressable (>= 2.8.0) addressable (>= 2.8.0)
crack (>= 0.3.2) crack (>= 0.3.2)
hashdiff (>= 0.4.0, < 2.0.0) hashdiff (>= 0.4.0, < 2.0.0)
webrick (1.9.1) webrick (1.8.1)
websocket-driver (0.7.6) websocket-driver (0.7.6)
websocket-extensions (>= 0.1.0) websocket-extensions (>= 0.1.0)
websocket-extensions (0.1.5) websocket-extensions (0.1.5)
xpath (3.2.0) xpath (3.2.0)
nokogiri (~> 1.8) nokogiri (~> 1.8)
zeitwerk (2.7.1) zeitwerk (2.6.14)
PLATFORMS PLATFORMS
aarch64-linux aarch64-linux
aarch64-linux-musl
arm64-darwin
ruby ruby
x86_64-linux x86_64-linux
x86_64-linux-musl
DEPENDENCIES DEPENDENCIES
annotaterb annotate
arabic-letter-connector arabic-letter-connector
aws-sdk-s3 aws-sdk-s3
aws-sdk-secretsmanager aws-sdk-secretsmanager
azure-storage-blob azure-storage-blob
better_html better_html
bootsnap bootsnap
brakeman
bullet bullet
cancancan cancancan
capybara capybara
@@ -613,7 +584,6 @@ DEPENDENCIES
faker faker
faraday faraday
faraday-follow_redirects faraday-follow_redirects
foreman
google-cloud-storage google-cloud-storage
hexapdf hexapdf
image_processing image_processing
@@ -633,7 +603,6 @@ DEPENDENCIES
rails-i18n rails-i18n
rails_autolink rails_autolink
rotp rotp
rouge
rqrcode rqrcode
rspec-rails rspec-rails
rubocop rubocop
@@ -654,7 +623,7 @@ DEPENDENCIES
webmock webmock
RUBY VERSION RUBY VERSION
ruby 3.4.2p28 ruby 3.3.3p89
BUNDLED WITH BUNDLED WITH
2.5.3 2.5.3
+14 -14
View File
@@ -1,6 +1,6 @@
<h1 align="center" style="border-bottom: none"> <h1 align="center" style="border-bottom: none">
<div> <div>
<a href="https://www.docuseal.com"> <a href="https://www.docuseal.co">
<img alt="DocuSeal" src="https://github.com/docusealco/docuseal/assets/5418788/c12cd051-81cd-4402-bc3a-92f2cfdc1b06" width="80" /> <img alt="DocuSeal" src="https://github.com/docusealco/docuseal/assets/5418788/c12cd051-81cd-4402-bc3a-92f2cfdc1b06" width="80" />
<br> <br>
</a> </a>
@@ -25,12 +25,12 @@
DocuSeal is an open source platform that provides secure and efficient digital document signing and processing. Create PDF forms to have them filled and signed online on any device with an easy-to-use, mobile-optimized web tool. DocuSeal is an open source platform that provides secure and efficient digital document signing and processing. Create PDF forms to have them filled and signed online on any device with an easy-to-use, mobile-optimized web tool.
</p> </p>
<h2 align="center"> <h2 align="center">
<a href="https://demo.docuseal.tech">✨ Live Demo</a> <a href="https://demo.docuseal.co">✨ Live Demo</a>
<span>|</span> <span>|</span>
<a href="https://docuseal.com/sign_up">☁️ Try in Cloud</a> <a href="https://docuseal.co/sign_up">☁️ Try in Cloud</a>
</h2> </h2>
[![Demo](https://github.com/docusealco/docuseal/assets/5418788/d8703ea3-361a-423f-8bfe-eff1bd9dbe14)](https://demo.docuseal.tech) [![Demo](https://github.com/docusealco/docuseal/assets/5418788/d8703ea3-361a-423f-8bfe-eff1bd9dbe14)](https://demo.docuseal.co)
## Features ## Features
- PDF form fields builder (WYSIWYG) - PDF form fields builder (WYSIWYG)
@@ -42,7 +42,7 @@ DocuSeal is an open source platform that provides secure and efficient digital d
- PDF signature verification - PDF signature verification
- Users management - Users management
- Mobile-optimized - Mobile-optimized
- 6 UI languages with signing available in 14 languages - Signing available in 13 languages
- API and Webhooks for integrations - API and Webhooks for integrations
- Easy to deploy in minutes - Easy to deploy in minutes
@@ -54,11 +54,11 @@ DocuSeal is an open source platform that provides secure and efficient digital d
- Conditional fields and formulas - Conditional fields and formulas
- Bulk send with CSV, XLSX spreadsheet import - Bulk send with CSV, XLSX spreadsheet import
- SSO / SAML - SSO / SAML
- Template creation with HTML API ([Guide](https://www.docuseal.com/guides/create-pdf-document-fillable-form-with-html-api)) - Template creation with HTML API ([Guide](https://www.docuseal.co/guides/create-pdf-document-fillable-form-with-html-api))
- Template creation with PDF or DOCX and field tags API ([Guide](https://www.docuseal.com/guides/use-embedded-text-field-tags-in-the-pdf-to-create-a-fillable-form)) - Template creation with PDF or DOCX and field tags API ([Guide](https://www.docuseal.co/guides/use-embedded-text-field-tags-in-the-pdf-to-create-a-fillable-form))
- Embedded signing form ([React](https://github.com/docusealco/docuseal-react), [Vue](https://github.com/docusealco/docuseal-vue), [Angular](https://github.com/docusealco/docuseal-angular) or [JavaScript](https://www.docuseal.com/docs/embedded)) - Embedded signing form ([React](https://github.com/docusealco/docuseal-react), [Vue](https://github.com/docusealco/docuseal-vue), [Angular](https://github.com/docusealco/docuseal-angular) or [JavaScript](https://www.docuseal.co/docs/embedded))
- Embedded document form builder ([React](https://github.com/docusealco/docuseal-react), [Vue](https://github.com/docusealco/docuseal-vue), [Angular](https://github.com/docusealco/docuseal-angular) or [JavaScript](https://www.docuseal.com/docs/embedded)) - Embedded document form builder ([React](https://github.com/docusealco/docuseal-react), [Vue](https://github.com/docusealco/docuseal-vue), [Angular](https://github.com/docusealco/docuseal-angular) or [JavaScript](https://www.docuseal.co/docs/embedded))
- [Learn more](https://www.docuseal.com/pricing) - [Learn more](https://www.docuseal.co/pricing)
## Deploy ## Deploy
@@ -93,7 +93,7 @@ sudo HOST=your-domain-name.com docker compose up
At DocuSeal we have expertise and technologies to make documents creation, filling, signing and processing seamlessly integrated with your product. We specialize in working with various industries, including **Banking, Healthcare, Transport, Real Estate, eCommerce, KYC, CRM, and other software products** that require bulk document signing. By leveraging DocuSeal, we can assist in reducing the overall cost of developing and processing electronic documents while ensuring security and compliance with local electronic document laws. At DocuSeal we have expertise and technologies to make documents creation, filling, signing and processing seamlessly integrated with your product. We specialize in working with various industries, including **Banking, Healthcare, Transport, Real Estate, eCommerce, KYC, CRM, and other software products** that require bulk document signing. By leveraging DocuSeal, we can assist in reducing the overall cost of developing and processing electronic documents while ensuring security and compliance with local electronic document laws.
[Book a Meeting](https://www.docuseal.com/contact) [Book a Meeting](https://www.docuseal.co/contact)
## License ## License
@@ -102,6 +102,6 @@ Unless otherwise noted, all files © 2023 DocuSeal LLC.
## Tools ## Tools
- [Signature Maker](https://www.docuseal.com/online-signature) - [Signature Maker](https://www.docuseal.co/online-signature)
- [Sign Document Online](https://www.docuseal.com/sign-documents-online) - [Sign Document Online](https://www.docuseal.co/sign-documents-online)
- [Fill PDF Online](https://www.docuseal.com/fill-pdf) - [Fill PDF Online](https://www.docuseal.co/fill-pdf)
+1 -1
View File
@@ -1,6 +1,6 @@
# Reporting a Vulnerability # Reporting a Vulnerability
If you come across any security concern or vulnarability, please report the information via email to security@docuseal.com instead of opening a GitHub issue. We will promptly respond and will collaborate with you to validate the issue, and resolve it ASAP. If you come across any security concern or vulnarability, please report the information via email to security@docuseal.co instead of opening a GitHub issue. We will promptly respond and will collaborate with you to validate the issue, and resolve it ASAP.
**We have a bug bounty program to reward security researchers.** **We have a bug bounty program to reward security researchers.**
+4 -18
View File
@@ -1,27 +1,21 @@
# frozen_string_literal: true # frozen_string_literal: true
class AccountConfigsController < ApplicationController class AccountConfigsController < ApplicationController
before_action :load_account_config, only: :create before_action :load_account_config
authorize_resource :account_config, only: :create authorize_resource :account_config
load_and_authorize_resource :account_config, only: :destroy
ALLOWED_KEYS = [ ALLOWED_KEYS = [
AccountConfig::ALLOW_TYPED_SIGNATURE, AccountConfig::ALLOW_TYPED_SIGNATURE,
AccountConfig::FORCE_MFA, AccountConfig::FORCE_MFA,
AccountConfig::ALLOW_TO_RESUBMIT, AccountConfig::ALLOW_TO_RESUBMIT,
AccountConfig::ALLOW_TO_DECLINE_KEY,
AccountConfig::FORM_PREFILL_SIGNATURE_KEY, AccountConfig::FORM_PREFILL_SIGNATURE_KEY,
AccountConfig::ESIGNING_PREFERENCE_KEY, AccountConfig::ESIGNING_PREFERENCE_KEY,
AccountConfig::FORM_WITH_CONFETTI_KEY, AccountConfig::FORM_WITH_CONFETTI_KEY,
AccountConfig::DOWNLOAD_LINKS_AUTH_KEY, AccountConfig::DOWNLOAD_LINKS_AUTH_KEY,
AccountConfig::FORCE_SSO_AUTH_KEY, AccountConfig::FORCE_SSO_AUTH_KEY,
AccountConfig::FLATTEN_RESULT_PDF_KEY, AccountConfig::FLATTEN_RESULT_PDF_KEY,
AccountConfig::ENFORCE_SIGNING_ORDER_KEY,
AccountConfig::WITH_SIGNATURE_ID, AccountConfig::WITH_SIGNATURE_ID,
AccountConfig::COMBINE_PDF_RESULT_KEY, AccountConfig::REQUIRE_SIGNING_REASON_KEY
AccountConfig::REQUIRE_SIGNING_REASON_KEY,
AccountConfig::DOCUMENT_FILENAME_FORMAT_KEY
].freeze ].freeze
InvalidKey = Class.new(StandardError) InvalidKey = Class.new(StandardError)
@@ -32,14 +26,6 @@ class AccountConfigsController < ApplicationController
head :ok head :ok
end end
def destroy
raise InvalidKey unless ALLOWED_KEYS.include?(@account_config.key)
@account_config.destroy!
redirect_back(fallback_location: root_path)
end
private private
def load_account_config def load_account_config
@@ -50,7 +36,7 @@ class AccountConfigsController < ApplicationController
end end
def account_config_params def account_config_params
params.required(:account_config).permit(:key, :value, { value: {} }, { value: [] }).tap do |attrs| params.required(:account_config).permit!.tap do |attrs|
attrs[:value] = attrs[:value] == '1' if attrs[:value].in?(%w[1 0]) attrs[:value] = attrs[:value] == '1' if attrs[:value].in?(%w[1 0])
end end
end end
+19 -24
View File
@@ -4,11 +4,11 @@ class AccountsController < ApplicationController
LOCALE_OPTIONS = { LOCALE_OPTIONS = {
'en-US' => 'English (United States)', 'en-US' => 'English (United States)',
'en-GB' => 'English (United Kingdom)', 'en-GB' => 'English (United Kingdom)',
'fr-FR' => 'Français', 'fr-FR' => 'French (France)',
'es-ES' => 'Español', 'es-ES' => 'Spanish (Spain)',
'pt-PT' => 'Português', 'pt-PT' => 'Portuguese (Portugal)',
'de-DE' => 'Deutsch', 'de-DE' => 'German (Germany)',
'it-IT' => 'Italiano' 'it-IT' => 'Italian (Italy)'
}.freeze }.freeze
before_action :load_account before_action :load_account
@@ -19,25 +19,21 @@ class AccountsController < ApplicationController
def update def update
current_account.update!(account_params) current_account.update!(account_params)
unless Docuseal.multitenant? @encrypted_config = EncryptedConfig.find_or_initialize_by(account: current_account,
@encrypted_config = EncryptedConfig.find_or_initialize_by(account: current_account, key: EncryptedConfig::APP_URL_KEY)
key: EncryptedConfig::APP_URL_KEY) @encrypted_config.assign_attributes(app_url_params)
@encrypted_config.assign_attributes(app_url_params)
unless URI.parse(@encrypted_config.value.to_s).class.in?([URI::HTTP, URI::HTTPS]) unless URI.parse(@encrypted_config.value.to_s).class.in?([URI::HTTP, URI::HTTPS])
@encrypted_config.errors.add(:value, I18n.t('should_be_a_valid_url')) @encrypted_config.errors.add(:value, 'should be a valid URL')
return render :show, status: :unprocessable_entity return render :show, status: :unprocessable_entity
end
@encrypted_config.save!
Docuseal.refresh_default_url_options!
end end
with_locale do @encrypted_config.save!
redirect_to settings_account_path, notice: I18n.t('account_information_has_been_updated')
end Docuseal.refresh_default_url_options!
redirect_to settings_account_path, notice: 'Account information has been updated'
rescue ActiveRecord::RecordInvalid rescue ActiveRecord::RecordInvalid
render :show, status: :unprocessable_entity render :show, status: :unprocessable_entity
end end
@@ -45,14 +41,13 @@ class AccountsController < ApplicationController
def destroy def destroy
authorize!(:manage, current_account) authorize!(:manage, current_account)
true_user.update!(locked_at: Time.current, email: true_user.email.sub('@', '+removed@')) true_user.update!(locked_at: Time.current)
# rubocop:disable Layout/LineLength
render turbo_stream: turbo_stream.replace( render turbo_stream: turbo_stream.replace(
:account_delete_button, :account_delete_button,
html: helpers.tag.p(I18n.t('your_account_removal_request_will_be_processed_within_2_months_please_contact_us_if_you_want_to_keep_your_account')) html: helpers.tag.p('Your account removal request will be processed within 2 weeks. ' \
'Please contact us if you want to keep your account.')
) )
# rubocop:enable Layout/LineLength
end end
private private
@@ -21,11 +21,7 @@ module Api
blob = ActiveStorage::Blob.find_by!(uuid: blob_uuid) blob = ActiveStorage::Blob.find_by!(uuid: blob_uuid)
attachment = blob.attachments.take authorization_check!(blob) if exp.blank?
@record = attachment.record
authorization_check!(attachment) if exp.blank?
if request.headers['Range'].present? if request.headers['Range'].present?
send_blob_byte_range_data blob, request.headers['Range'] send_blob_byte_range_data blob, request.headers['Range']
@@ -41,12 +37,13 @@ module Api
private private
def authorization_check!(attachment) def authorization_check!(blob)
is_authorized = attachment.name.in?(%w[logo preview_images]) || is_authorized =
(current_user && attachment.record.account.id == current_user.account_id) || blob.attachments.all? do |a|
(current_user && !Docuseal.multitenant? && current_user.role == 'superadmin') || a.name.in?(%w[logo preview_images]) ||
!attachment.record.account.account_configs (current_user && a.record.account.id == current_user.account_id) ||
.find_or_initialize_by(key: AccountConfig::DOWNLOAD_LINKS_AUTH_KEY).value !a.record.account.account_configs.find_or_initialize_by(key: AccountConfig::DOWNLOAD_LINKS_AUTH_KEY).value
end
return if is_authorized return if is_authorized
+2 -29
View File
@@ -25,9 +25,9 @@ module Api
render json: { error: 'Too many requests' }, status: :too_many_requests render json: { error: 'Too many requests' }, status: :too_many_requests
end end
unless Rails.env.development? if Rails.env.production?
rescue_from CanCan::AccessDenied do |e| rescue_from CanCan::AccessDenied do |e|
render json: { error: access_denied_error_message(e) }, status: :forbidden render json: { error: e.message }, status: :forbidden
end end
rescue_from JSON::ParserError do |e| rescue_from JSON::ParserError do |e|
@@ -39,33 +39,6 @@ module Api
private private
def access_denied_error_message(error)
return 'Not authorized' if request.headers['X-Auth-Token'].blank?
return 'Not authorized' unless error.subject.is_a?(ActiveRecord::Base)
return 'Not authorized' unless error.subject.respond_to?(:account_id)
linked_account_record_exists =
if current_user.account.testing?
current_user.account.linked_account_accounts.where(account_type: 'testing')
.exists?(account_id: error.subject.account_id)
else
current_user.account.testing_accounts.exists?(id: error.subject.account_id)
end
return 'Not authorized' unless linked_account_record_exists
object_name = error.subject.model_name.human
id = error.subject.id
if current_user.account.testing?
"#{object_name} #{id} not found using testing API key; Use production API key to " \
"access production #{object_name.downcase.pluralize}."
else
"#{object_name} #{id} not found using production API key; Use testing API key to " \
"access testing #{object_name.downcase.pluralize}."
end
end
def paginate(relation, field: :id) def paginate(relation, field: :id)
result = relation.order(field => :desc) result = relation.order(field => :desc)
.limit([params.fetch(:limit, DEFAULT_LIMIT).to_i, MAX_LIMIT].min) .limit([params.fetch(:limit, DEFAULT_LIMIT).to_i, MAX_LIMIT].min)
@@ -10,12 +10,6 @@ module Api
def create def create
submitter = Submitter.find_by!(slug: params[:submitter_slug]) submitter = Submitter.find_by!(slug: params[:submitter_slug])
if params[:type].in?(%w[initials signature]) && ImageUtils.blank?(Vips::Image.new_from_file(params[:file].path))
Rollbar.error("Empty signature: #{submitter.id}") if defined?(Rollbar)
return render json: { error: "#{params[:type]} is empty" }, status: :unprocessable_entity
end
attachment = Submitters.create_attachment!(submitter, params) attachment = Submitters.create_attachment!(submitter, params)
if params[:remember_signature] == 'true' && submitter.email.present? if params[:remember_signature] == 'true' && submitter.email.present?
@@ -11,8 +11,7 @@ module Api
params[:before] = Time.zone.at(params[:before].to_i) if params[:before].present? params[:before] = Time.zone.at(params[:before].to_i) if params[:before].present?
submitters = paginate( submitters = paginate(
submitters.preload(template: :folder, submission: [:submitters, { audit_trail_attachment: :blob, submitters.preload(template: :folder, submission: [:submitters, { audit_trail_attachment: :blob }],
combined_document_attachment: :blob }],
documents_attachments: :blob, attachments_attachments: :blob), documents_attachments: :blob, attachments_attachments: :blob),
field: :completed_at field: :completed_at
) )
@@ -1,45 +0,0 @@
# frozen_string_literal: true
module Api
class SubmissionDocumentsController < ApiBaseController
load_and_authorize_resource :submission
def index
documents =
if @submission.submitters.all?(&:completed_at?)
last_submitter = @submission.submitters.max_by(&:completed_at)
if last_submitter.documents_attachments.blank?
last_submitter.documents_attachments = Submissions::EnsureResultGenerated.call(last_submitter)
end
last_submitter.documents_attachments
else
values_hash = Submissions::GeneratePreviewAttachments.build_values_hash(@submission)
if @submission.preview_documents.present? &&
@submission.preview_documents.all? { |s| s.metadata['values_hash'] == values_hash }
@submission.preview_documents
else
ApplicationRecord.no_touching do
@submission.preview_documents.each(&:destroy)
end
Submissions::GeneratePreviewAttachments.call(@submission, values_hash:)
end
end
ActiveRecord::Associations::Preloader.new(
records: documents,
associations: [:blob]
).call
render json: {
id: @submission.id,
documents: documents.map do |attachment|
{ name: attachment.filename.base, url: ActiveStorage::Blob.proxy_url(attachment.blob) }
end
}
end
end
end
@@ -1,52 +0,0 @@
# 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
+20 -53
View File
@@ -10,19 +10,19 @@ module Api
end end
def index def index
submissions = Submissions.search(current_user, @submissions, params[:q]) submissions = Submissions.search(@submissions, params[:q])
submissions = filter_submissions(submissions, params) submissions = submissions.where(template_id: params[:template_id]) if params[:template_id].present?
if params[:template_folder].present?
submissions = submissions.joins(template: :folder).where(folder: { name: params[:template_folder] })
end
submissions = paginate(submissions.preload(:created_by_user, :submitters, submissions = paginate(submissions.preload(:created_by_user, :submitters,
template: :folder, template: :folder,
combined_document_attachment: :blob,
audit_trail_attachment: :blob)) audit_trail_attachment: :blob))
render json: { render json: {
data: submissions.map do |s| data: submissions.as_json(Submissions::SerializeForApi::SERIALIZE_PARAMS),
Submissions::SerializeForApi.call(s, s.submitters, params,
with_events: false, with_documents: false, with_values: false)
end,
pagination: { pagination: {
count: submissions.size, count: submissions.size,
next: submissions.last&.id, next: submissions.last&.id,
@@ -63,35 +63,32 @@ module Api
submissions = create_submissions(@template, params) submissions = create_submissions(@template, params)
WebhookUrls.enqueue_events(submissions, 'submission.created') submissions.each do |submission|
SendSubmissionCreatedWebhookRequestJob.perform_async({ 'submission_id' => submission.id })
end
Submissions.send_signature_requests(submissions) Submissions.send_signature_requests(submissions)
submissions.each do |submission| submissions.each do |submission|
submission.submitters.each do |submitter| if submission.submitters.all?(&:completed_at?) && submission.submitters.last
next unless submitter.completed_at? ProcessSubmitterCompletionJob.perform_async({ 'submitter_id' => submission.submitters.last.id })
ProcessSubmitterCompletionJob.perform_async('submitter_id' => submitter.id, 'send_invitation_email' => false)
end end
end end
SearchEntries.enqueue_reindex(submissions)
render json: build_create_json(submissions) render json: build_create_json(submissions)
rescue Submitters::NormalizeValues::BaseError, Submissions::CreateFromSubmitters::BaseError, rescue Submitters::NormalizeValues::BaseError => e
DownloadUtils::UnableToDownload => e
Rollbar.warning(e) if defined?(Rollbar) Rollbar.warning(e) if defined?(Rollbar)
render json: { error: e.message }, status: :unprocessable_entity render json: { error: e.message }, status: :unprocessable_entity
end end
def destroy def destroy
if params[:permanently].in?(['true', true]) if params[:permanently] == 'true'
@submission.destroy! @submission.destroy!
else else
@submission.update!(archived_at: Time.current) @submission.update!(archived_at: Time.current)
WebhookUrls.enqueue_events(@submission, 'submission.archived') SendSubmissionArchivedWebhookRequestJob.perform_async('submission_id' => @submission.id)
end end
render json: @submission.as_json(only: %i[id archived_at]) render json: @submission.as_json(only: %i[id archived_at])
@@ -99,23 +96,6 @@ module Api
private private
def filter_submissions(submissions, params)
submissions = submissions.where(template_id: params[:template_id]) if params[:template_id].present?
submissions = submissions.where(slug: params[:slug]) if params[:slug].present?
if params[:template_folder].present?
folder_ids = TemplateFolder.accessible_by(current_ability).where(name: params[:template_folder]).pluck(:id)
submissions = submissions.joins(:template).where(template: { folder_id: folder_ids })
end
if params.key?(:archived)
submissions = params[:archived].in?(['true', true]) ? submissions.archived : submissions.active
end
Submissions::Filter.call(submissions, current_user, params)
end
def build_create_json(submissions) def build_create_json(submissions)
json = submissions.flat_map do |submission| json = submissions.flat_map do |submission|
submission.submitters.map do |s| submission.submitters.map do |s|
@@ -123,19 +103,7 @@ module Api
end end
end end
if request.path.ends_with?('/init') json = { submitters: json } if request.path.ends_with?('/init')
json =
if submissions.size == 1
{
id: submissions.first.id,
submitters: json,
expire_at: submissions.first.expire_at,
created_at: submissions.first.created_at
}
else
{ submitters: json }
end
end
json json
end end
@@ -179,15 +147,14 @@ module Api
def submissions_params def submissions_params
permitted_attrs = [ permitted_attrs = [
:send_email, :send_sms, :bcc_completed, :completed_redirect_url, :reply_to, :go_to_last, :send_email, :send_sms, :bcc_completed, :completed_redirect_url, :reply_to, :go_to_last,
:require_phone_2fa, :expire_at, :name, :expire_at,
{ {
message: %i[subject body], message: %i[subject body],
submitters: [[:send_email, :send_sms, :completed_redirect_url, :uuid, :name, :email, :role, submitters: [[:send_email, :send_sms, :completed_redirect_url, :uuid, :name, :email, :role,
:completed, :phone, :application_key, :external_id, :reply_to, :go_to_last, :completed, :phone, :application_key, :external_id, :reply_to, :go_to_last,
:require_phone_2fa, { metadata: {}, values: {}, readonly_fields: [], message: %i[subject body],
{ metadata: {}, values: {}, roles: [], readonly_fields: [], message: %i[subject body],
fields: [:name, :uuid, :default_value, :value, :title, :description, fields: [:name, :uuid, :default_value, :value, :title, :description,
:readonly, :required, :validation_pattern, :invalid_message, :readonly, :redacted, :validation_pattern, :invalid_message,
{ default_value: [], value: [], preferences: {} }] }]] { default_value: [], value: [], preferences: {} }] }]]
} }
] ]
@@ -198,7 +165,7 @@ module Api
key = params.key?(:submission) ? :submission : :submissions key = params.key?(:submission) ? :submission : :submissions
params.permit( params.permit(
{ key => [permitted_attrs] }, { key => permitted_attrs } key => [permitted_attrs]
).fetch(key, []) ).fetch(key, [])
end end
end end
@@ -6,10 +6,10 @@ module Api
skip_authorization_check skip_authorization_check
def create def create
@submitter = Submitter.find_by!(slug: params[:submitter_slug]) submitter = Submitter.find_by!(slug: params[:submitter_slug])
if params[:t] == SubmissionEvents.build_tracking_param(@submitter, 'click_email') if params[:t] == SubmissionEvents.build_tracking_param(submitter, 'click_email')
SubmissionEvents.create_with_tracking_data(@submitter, 'click_email', request) SubmissionEvents.create_with_tracking_data(submitter, 'click_email', request)
end end
render json: {} render json: {}
@@ -6,14 +6,14 @@ module Api
skip_authorization_check skip_authorization_check
def create def create
@submitter = Submitter.find_by!(slug: params[:submitter_slug]) submitter = Submitter.find_by!(slug: params[:submitter_slug])
@submitter.opened_at = Time.current submitter.opened_at = Time.current
@submitter.save submitter.save
SubmissionEvents.create_with_tracking_data(@submitter, 'view_form', request) SubmissionEvents.create_with_tracking_data(submitter, 'view_form', request)
WebhookUrls.enqueue_events(@submitter, 'form.viewed') SendFormViewedWebhookRequestJob.perform_async({ 'submitter_id' => submitter.id })
render json: {} render json: {}
end end
+25 -47
View File
@@ -5,9 +5,17 @@ module Api
load_and_authorize_resource :submitter load_and_authorize_resource :submitter
def index def index
submitters = Submitters.search(current_user, @submitters, params[:q]) submitters = Submitters.search(@submitters, params[:q])
submitters = filter_submitters(submitters, params) submitters = submitters.where(external_id: params[:application_key]) if params[:application_key].present?
submitters = submitters.where(external_id: params[:external_id]) if params[:external_id].present?
submitters = submitters.where(submission_id: params[:submission_id]) if params[:submission_id].present?
if params[:template_id].present?
submitters = submitters.joins(:submission).where(submission: { template_id: params[:template_id] })
end
submitters = maybe_filder_by_completed_at(submitters, params)
submitters = paginate( submitters = paginate(
submitters.preload(:template, :submission, :submission_events, submitters.preload(:template, :submission, :submission_events,
@@ -37,16 +45,14 @@ module Api
return render json: { error: 'Submitter has already completed the submission.' }, status: :unprocessable_entity return render json: { error: 'Submitter has already completed the submission.' }, status: :unprocessable_entity
end end
submission = @submitter.submission role = @submitter.submission.template_submitters.find { |e| e['uuid'] == @submitter.uuid }['name']
role = submission.template_submitters.find { |e| e['uuid'] == @submitter.uuid }['name']
normalized_params, new_attachments = Submissions::NormalizeParamUtils.normalize_submitter_params!( normalized_params, new_attachments =
submitter_params.merge(role:), Submissions::NormalizeParamUtils.normalize_submitter_params!(submitter_params.merge(role:), @submitter.template,
@submitter.template || Template.new(submitters: submission.template_submitters, account: @submitter.account), for_submitter: @submitter)
for_submitter: @submitter
)
Submissions::CreateFromSubmitters.maybe_set_template_fields(submission, [normalized_params], Submissions::CreateFromSubmitters.maybe_set_template_fields(@submitter.submission,
[normalized_params],
default_submitter_uuid: @submitter.uuid) default_submitter_uuid: @submitter.uuid)
assign_submitter_attrs(@submitter, normalized_params) assign_submitter_attrs(@submitter, normalized_params)
@@ -62,19 +68,15 @@ module Api
end end
if @submitter.completed_at? if @submitter.completed_at?
ProcessSubmitterCompletionJob.perform_async('submitter_id' => @submitter.id) ProcessSubmitterCompletionJob.perform_async({ 'submitter_id' => @submitter.id })
elsif normalized_params[:send_email] || normalized_params[:send_sms] elsif normalized_params[:send_email] || normalized_params[:send_sms]
Submitters.send_signature_requests([@submitter]) Submitters.send_signature_requests([@submitter])
end end
SearchEntries.enqueue_reindex(@submitter) render json: Submitters::SerializeForApi.call(@submitter, with_template: false,
with_urls: true,
render json: Submitters::SerializeForApi.call(@submitter, with_template: false, with_urls: true, with_events: false,
with_events: false, params:) params:)
rescue Submitters::NormalizeValues::BaseError, DownloadUtils::UnableToDownload => e
Rollbar.warning(e) if defined?(Rollbar)
render json: { error: e.message }, status: :unprocessable_entity
end end
def submitter_params def submitter_params
@@ -82,10 +84,10 @@ module Api
submitter_params.permit( submitter_params.permit(
:send_email, :send_sms, :reply_to, :completed_redirect_url, :uuid, :name, :email, :role, :send_email, :send_sms, :reply_to, :completed_redirect_url, :uuid, :name, :email, :role,
:completed, :phone, :application_key, :external_id, :go_to_last, :require_phone_2fa, :completed, :phone, :application_key, :external_id, :go_to_last,
{ metadata: {}, values: {}, readonly_fields: [], message: %i[subject body], { metadata: {}, values: {}, readonly_fields: [], message: %i[subject body],
fields: [[:name, :uuid, :default_value, :value, :required, fields: [[:name, :uuid, :default_value, :value,
:readonly, :validation_pattern, :invalid_message, :readonly, :redacted, :validation_pattern, :invalid_message,
{ default_value: [], value: [], preferences: {} }]] } { default_value: [], value: [], preferences: {} }]] }
) )
end end
@@ -114,7 +116,6 @@ module Api
end&.dig('uuid') end&.dig('uuid')
submitter.email = Submissions.normalize_email(attrs[:email]) if attrs.key?(:email) submitter.email = Submissions.normalize_email(attrs[:email]) if attrs.key?(:email)
submitter.name = attrs[:name] if attrs.key?(:name)
if attrs.key?(:phone) if attrs.key?(:phone)
submitter.phone = attrs[:phone].to_s.gsub(/[^0-9+]/, '') submitter.phone = attrs[:phone].to_s.gsub(/[^0-9+]/, '')
@@ -140,15 +141,9 @@ module Api
if attrs[:completed] if attrs[:completed]
submitter.values = Submitters::SubmitValues.merge_default_values(submitter) submitter.values = Submitters::SubmitValues.merge_default_values(submitter)
submitter.values = Submitters::SubmitValues.merge_formula_values(submitter)
submitter.values = Submitters::SubmitValues.maybe_remove_condition_values(submitter) submitter.values = Submitters::SubmitValues.maybe_remove_condition_values(submitter)
formula_values = Submitters::SubmitValues.build_formula_values(submitter)
if formula_values.present?
submitter.values = submitter.values.merge(formula_values)
submitter.values = Submitters::SubmitValues.maybe_remove_condition_values(submitter)
end
submitter.values = submitter.values.transform_values do |v| submitter.values = submitter.values.transform_values do |v|
v == '{{date}}' ? Time.current.in_time_zone(submitter.account.timezone).to_date.to_s : v v == '{{date}}' ? Time.current.in_time_zone(submitter.account.timezone).to_date.to_s : v
end end
@@ -157,19 +152,6 @@ module Api
submitter submitter
end end
def filter_submitters(submitters, params)
submitters = submitters.where(external_id: params[:application_key]) if params[:application_key].present?
submitters = submitters.where(external_id: params[:external_id]) if params[:external_id].present?
submitters = submitters.where(slug: params[:slug]) if params[:slug].present?
submitters = submitters.where(submission_id: params[:submission_id]) if params[:submission_id].present?
if params[:template_id].present?
submitters = submitters.joins(:submission).where(submissions: { template_id: params[:template_id] })
end
maybe_filder_by_completed_at(submitters, params)
end
def assign_external_id(submitter, attrs) def assign_external_id(submitter, attrs)
submitter.external_id = attrs[:application_key] if attrs.key?(:application_key) submitter.external_id = attrs[:application_key] if attrs.key?(:application_key)
submitter.external_id = attrs[:external_id] if attrs.key?(:external_id) submitter.external_id = attrs[:external_id] if attrs.key?(:external_id)
@@ -193,10 +175,6 @@ module Api
submitter.preferences['send_sms'] = submitter_preferences['send_sms'] if submitter_preferences.key?('send_sms') submitter.preferences['send_sms'] = submitter_preferences['send_sms'] if submitter_preferences.key?('send_sms')
submitter.preferences['reply_to'] = submitter_preferences['reply_to'] if submitter_preferences.key?('reply_to') submitter.preferences['reply_to'] = submitter_preferences['reply_to'] if submitter_preferences.key?('reply_to')
if submitter_preferences.key?('require_phone_2fa')
submitter.preferences['require_phone_2fa'] = submitter_preferences['require_phone_2fa']
end
if submitter_preferences.key?('go_to_last') if submitter_preferences.key?('go_to_last')
submitter.preferences['go_to_last'] = submitter_preferences['go_to_last'] submitter.preferences['go_to_last'] = submitter_preferences['go_to_last']
end end
@@ -5,7 +5,7 @@ module Api
load_and_authorize_resource :template load_and_authorize_resource :template
def create def create
authorize!(:create, @template) authorize!(:manage, @template)
ActiveRecord::Associations::Preloader.new( ActiveRecord::Associations::Preloader.new(
records: [@template], records: [@template],
@@ -21,16 +21,11 @@ module Api
) )
cloned_template.source = :api cloned_template.source = :api
schema_documents = Templates::CloneAttachments.call(template: cloned_template,
original_template: @template,
documents: params[:documents])
cloned_template.save! cloned_template.save!
WebhookUrls.enqueue_events(cloned_template, 'template.created') schema_documents = Templates::CloneAttachments.call(template: cloned_template, original_template: @template)
SearchEntries.enqueue_reindex(cloned_template) SendTemplateCreatedWebhookRequestJob.perform_async('template_id' => cloned_template.id)
render json: Templates::SerializeForApi.call(cloned_template, schema_documents) render json: Templates::SerializeForApi.call(cloned_template, schema_documents)
end end
+8 -18
View File
@@ -18,7 +18,7 @@ module Api
preview_image_attachments = preview_image_attachments =
ActiveStorage::Attachment.joins(:blob) ActiveStorage::Attachment.joins(:blob)
.where(blob: { filename: ['0.png', '0.jpg'] }) .where(blob: { filename: '0.jpg' })
.where(record_id: schema_documents.map(&:id), .where(record_id: schema_documents.map(&:id),
record_type: 'ActiveStorage::Attachment', record_type: 'ActiveStorage::Attachment',
name: :preview_images) name: :preview_images)
@@ -65,15 +65,13 @@ module Api
@template.update!(template_params) @template.update!(template_params)
SearchEntries.enqueue_reindex(@template) SendTemplateUpdatedWebhookRequestJob.perform_async('template_id' => @template.id)
WebhookUrls.enqueue_events(@template, 'template.updated')
render json: @template.as_json(only: %i[id updated_at]) render json: @template.as_json(only: %i[id updated_at])
end end
def destroy def destroy
if params[:permanently].in?(['true', true]) if params[:permanently] == 'true'
@template.destroy! @template.destroy!
else else
@template.update!(archived_at: Time.current) @template.update!(archived_at: Time.current)
@@ -85,17 +83,11 @@ module Api
private private
def filter_templates(templates, params) def filter_templates(templates, params)
templates = Templates.search(current_user, templates, params[:q]) templates = Templates.search(templates, params[:q])
templates = params[:archived].in?(['true', true]) ? templates.archived : templates.active templates = params[:archived] ? templates.archived : templates.active
templates = templates.where(external_id: params[:application_key]) if params[:application_key].present? templates = templates.where(external_id: params[:application_key]) if params[:application_key].present?
templates = templates.where(external_id: params[:external_id]) if params[:external_id].present? templates = templates.where(external_id: params[:external_id]) if params[:external_id].present?
templates = templates.where(slug: params[:slug]) if params[:slug].present? templates = templates.joins(:folder).where(folder: { name: params[:folder] }) if params[:folder].present?
if params[:folder].present?
folder_ids = TemplateFolder.accessible_by(current_ability).where(name: params[:folder]).pluck(:id)
templates = templates.where(folder_id: folder_ids)
end
templates templates
end end
@@ -103,15 +95,13 @@ module Api
def template_params def template_params
permitted_params = [ permitted_params = [
:name, :name,
:external_id,
:shared_link,
{ {
submitters: [%i[name uuid is_requester invite_by_uuid optional_invite_by_uuid linked_to_uuid email]], submitters: [%i[name uuid]],
fields: [[:uuid, :submitter_uuid, :name, :type, fields: [[:uuid, :submitter_uuid, :name, :type,
:required, :readonly, :default_value, :required, :readonly, :default_value,
:title, :description, :title, :description,
{ preferences: {}, { preferences: {},
conditions: [%i[field_uuid value action operation]], conditions: [%i[field_uuid value action]],
options: [%i[value uuid]], options: [%i[value uuid]],
validation: %i[message pattern], validation: %i[message pattern],
areas: [%i[x y w h cell_w attachment_uuid option_uuid page]] }]] areas: [%i[x y w h cell_w attachment_uuid option_uuid page]] }]]
+4 -3
View File
@@ -20,7 +20,10 @@ module Api
pdf = HexaPDF::Document.new(io: StringIO.new(file)) pdf = HexaPDF::Document.new(io: StringIO.new(file))
trusted_certs = Accounts.load_trusted_certs(current_account) trusted_certs = Accounts.load_trusted_certs(current_account)
is_checksum_found = CompletedDocument.exists?(sha256: Base64.urlsafe_encode64(Digest::SHA256.digest(file)))
is_checksum_found = ActiveStorage::Attachment.joins(:blob)
.where(name: 'documents', record_type: 'Submitter')
.exists?(blob: { checksum: Digest::MD5.base64digest(file) })
render json: { render json: {
checksum_status: is_checksum_found ? 'verified' : 'not_found', checksum_status: is_checksum_found ? 'verified' : 'not_found',
@@ -34,8 +37,6 @@ module Api
} }
end end
} }
rescue HexaPDF::MalformedPDFError
render json: { error: 'Malformed PDF' }, status: :unprocessable_entity
end end
end end
end end
+1 -1
View File
@@ -12,6 +12,6 @@ class ApiSettingsController < ApplicationController
current_user.access_token.save! current_user.access_token.save!
redirect_back(fallback_location: settings_api_index_path, notice: I18n.t('api_token_has_been_updated')) redirect_back(fallback_location: settings_api_index_path, notice: 'API token as been updated.')
end end
end end
+2 -38
View File
@@ -8,14 +8,12 @@ class ApplicationController < ActionController::Base
check_authorization unless: :devise_controller? check_authorization unless: :devise_controller?
around_action :with_locale
before_action :sign_in_for_demo, if: -> { Docuseal.demo? } before_action :sign_in_for_demo, if: -> { Docuseal.demo? }
before_action :maybe_redirect_to_setup, unless: :signed_in? before_action :maybe_redirect_to_setup, unless: :signed_in?
before_action :authenticate_user!, unless: :devise_controller? before_action :authenticate_user!, unless: :devise_controller?
helper_method :button_title, helper_method :button_title,
:current_account, :current_account,
:form_link_host,
:svg_icon :svg_icon
impersonates :user, with: ->(uuid) { User.find_by(uuid:) } impersonates :user, with: ->(uuid) { User.find_by(uuid:) }
@@ -30,7 +28,7 @@ class ApplicationController < ActionController::Base
redirect_to request.referer, alert: 'Too many requests', status: :too_many_requests redirect_to request.referer, alert: 'Too many requests', status: :too_many_requests
end end
if Rails.env.production? || Rails.env.test? if Rails.env.production?
rescue_from CanCan::AccessDenied do |e| rescue_from CanCan::AccessDenied do |e|
Rollbar.warning(e) if defined?(Rollbar) Rollbar.warning(e) if defined?(Rollbar)
@@ -39,10 +37,6 @@ class ApplicationController < ActionController::Base
end end
def default_url_options 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 Docuseal.default_url_options
end end
@@ -55,28 +49,9 @@ class ApplicationController < ActionController::Base
request.session[:impersonated_user_id] = user.uuid request.session[:impersonated_user_id] = user.uuid
end end
def pagy_auto(collection, **keyword_args)
if current_ability.can?(:manage, :countless)
pagy_countless(collection, **keyword_args)
else
pagy(collection, **keyword_args)
end
end
private private
def with_locale(&)
return yield unless current_account
locale = params[:lang].presence if Rails.env.development?
locale ||= current_account.locale
I18n.with_locale(locale, &)
end
def with_browser_locale(&) def with_browser_locale(&)
return yield if I18n.locale != :'en-US' && I18n.locale != :en
locale = params[:lang].presence locale = params[:lang].presence
locale ||= request.env['HTTP_ACCEPT_LANGUAGE'].to_s[BROWSER_LOCALE_REGEXP].to_s locale ||= request.env['HTTP_ACCEPT_LANGUAGE'].to_s[BROWSER_LOCALE_REGEXP].to_s
@@ -104,8 +79,7 @@ class ApplicationController < ActionController::Base
redirect_to setup_index_path unless User.exists? redirect_to setup_index_path unless User.exists?
end end
def button_title(title: I18n.t('submit'), disabled_with: I18n.t('submitting'), title_class: '', icon: nil, def button_title(title: 'Submit', disabled_with: 'Submitting', title_class: '', icon: nil, icon_disabled: nil)
icon_disabled: nil)
render_to_string(partial: 'shared/button_title', render_to_string(partial: 'shared/button_title',
locals: { title:, disabled_with:, title_class:, icon:, icon_disabled: }) locals: { title:, disabled_with:, title_class:, icon:, icon_disabled: })
end end
@@ -113,14 +87,4 @@ class ApplicationController < ActionController::Base
def svg_icon(icon_name, class: '') def svg_icon(icon_name, class: '')
render_to_string(partial: "icons/#{icon_name}", locals: { class: }) render_to_string(partial: "icons/#{icon_name}", locals: { class: })
end end
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
end end
@@ -6,7 +6,7 @@ class ConsoleRedirectController < ApplicationController
def index def index
if request.path == '/upgrade' if request.path == '/upgrade'
params[:redir] = Docuseal.multitenant? ? "#{Docuseal::CONSOLE_URL}/plans" : "#{Docuseal::CONSOLE_URL}/on_premises" params[:redir] = Docuseal.multitenant? ? "#{Docuseal::CONSOLE_URL}/plans" : "#{Docuseal::CONSOLE_URL}/on_premise"
end end
params[:redir] = "#{Docuseal::CONSOLE_URL}/manage" if request.path == '/manage' params[:redir] = "#{Docuseal::CONSOLE_URL}/manage" if request.path == '/manage'
+1 -1
View File
@@ -33,7 +33,7 @@ class DashboardController < ApplicationController
account_id: current_user.account_id, account_id: current_user.account_id,
key: AccountConfig::FORCE_MFA) key: AccountConfig::FORCE_MFA)
redirect_to mfa_setup_path, notice: I18n.t('setup_2fa_to_continue') redirect_to mfa_setup_path, notice: 'Setup 2FA to continue'
end end
def maybe_render_landing def maybe_render_landing
@@ -9,11 +9,9 @@ class EmailSmtpSettingsController < ApplicationController
def create def create
if @encrypted_config.update(email_configs) if @encrypted_config.update(email_configs)
unless Docuseal.multitenant? SettingsMailer.smtp_successful_setup(@encrypted_config.value['from_email']).deliver_now!
SettingsMailer.smtp_successful_setup(@encrypted_config.value['from_email'] || current_user.email).deliver_now!
end
redirect_to settings_email_index_path, notice: I18n.t('changes_have_been_saved') redirect_to settings_email_index_path, notice: 'Changes have been saved'
else else
render :index, status: :unprocessable_entity render :index, status: :unprocessable_entity
end end
+1 -1
View File
@@ -9,7 +9,7 @@ class EmbedScriptsController < ActionController::Metal
<h2>Upgrade to Pro</h2> <h2>Upgrade to Pro</h2>
<p>Unlock embedded components by upgrading to Pro</p> <p>Unlock embedded components by upgrading to Pro</p>
<div style="margin-top: 40px;"> <div style="margin-top: 40px;">
<a href="#{Docuseal::CONSOLE_URL}/on_premises" target="_blank" style="padding: 15px 25px; background-color: #222; color: white; text-decoration: none; border-radius: 5px; font-size: 16px; cursor: pointer;"> <a href="#{Docuseal::CONSOLE_URL}/on_premise" target="_blank" style="padding: 15px 25px; background-color: #222; color: white; text-decoration: none; border-radius: 5px; font-size: 16px; cursor: pointer;">
Learn More Learn More
</a> </a>
</div> </div>
+1 -5
View File
@@ -2,15 +2,11 @@
class ErrorsController < ActionController::Base class ErrorsController < ActionController::Base
ENTERPRISE_FEATURE_MESSAGE = ENTERPRISE_FEATURE_MESSAGE =
'This feature is available in Pro Edition: https://www.docuseal.com/pricing' 'This feature is available in Pro Edition: https://www.docuseal.co/pricing'
ENTERPRISE_PATHS = [ ENTERPRISE_PATHS = [
'/submissions/html',
'/api/submissions/html',
'/templates/html', '/templates/html',
'/api/templates/html', '/api/templates/html',
'/submissions/pdf',
'/api/submissions/pdf',
'/templates/pdf', '/templates/pdf',
'/api/templates/pdf', '/api/templates/pdf',
'/templates/doc', '/templates/doc',
+10 -18
View File
@@ -11,8 +11,6 @@ class EsignSettingsController < ApplicationController
end end
end end
prepend_before_action :maybe_redirect_com, only: %i[show]
before_action :load_encrypted_config before_action :load_encrypted_config
authorize_resource :encrypted_config, parent: false, only: %i[new create] authorize_resource :encrypted_config, parent: false, only: %i[new create]
authorize_resource :encrypted_config, only: %i[update destroy show] authorize_resource :encrypted_config, only: %i[update destroy show]
@@ -23,9 +21,9 @@ class EsignSettingsController < ApplicationController
default_pkcs = GenerateCertificate.load_pkcs(cert_data) if cert_data['cert'].present? default_pkcs = GenerateCertificate.load_pkcs(cert_data) if cert_data['cert'].present?
custom_pkcs_list = (cert_data['custom'] || []).map do |e| custom_pkcs_list = (cert_data['custom'] || []).map do |e|
pkcs = e['data'].present? ? OpenSSL::PKCS12.new(Base64.urlsafe_decode64(e['data']), e['password'].to_s) : nil { 'pkcs' => OpenSSL::PKCS12.new(Base64.urlsafe_decode64(e['data']), e['password'].to_s),
'name' => e['name'],
{ 'pkcs' => pkcs, 'name' => e['name'], 'status' => e['status'] } 'status' => e['status'] }
end end
@pkcs_list = [ @pkcs_list = [
@@ -50,7 +48,7 @@ class EsignSettingsController < ApplicationController
if (@encrypted_config.value && @encrypted_config.value['custom']&.any? { |e| e['name'] == @cert_record.name }) || if (@encrypted_config.value && @encrypted_config.value['custom']&.any? { |e| e['name'] == @cert_record.name }) ||
@cert_record.name == DEFAULT_CERT_NAME @cert_record.name == DEFAULT_CERT_NAME
@cert_record.errors.add(:name, I18n.t('already_exists')) @cert_record.errors.add(:name, 'already exists')
return render turbo_stream: turbo_stream.replace(:modal, template: 'esign_settings/new'), return render turbo_stream: turbo_stream.replace(:modal, template: 'esign_settings/new'),
status: :unprocessable_entity status: :unprocessable_entity
@@ -58,7 +56,7 @@ class EsignSettingsController < ApplicationController
save_new_cert!(@encrypted_config, @cert_record) save_new_cert!(@encrypted_config, @cert_record)
redirect_to settings_esign_path, notice: I18n.t('certificate_has_been_successfully_added') redirect_to settings_esign_path, notice: 'Certificate has been successfully added!'
rescue OpenSSL::PKCS12::PKCS12Error => e rescue OpenSSL::PKCS12::PKCS12Error => e
Rollbar.error(e) if defined?(Rollbar) Rollbar.error(e) if defined?(Rollbar)
@@ -68,20 +66,14 @@ class EsignSettingsController < ApplicationController
end end
def update def update
@encrypted_config.value['custom'].to_a.each { |e| e['status'] = 'validate' } @encrypted_config.value['custom'].each { |e| e['status'] = 'validate' }
custom_cert_data = @encrypted_config.value['custom'].to_a.find { |e| e['name'] == params[:name] } custom_cert_data = @encrypted_config.value['custom'].find { |e| e['name'] == params[:name] }
custom_cert_data['status'] = 'default' if custom_cert_data
if custom_cert_data
custom_cert_data['status'] = 'default'
elsif params[:name] == Docuseal::AATL_CERT_NAME
@encrypted_config.value['custom'] ||= []
@encrypted_config.value['custom'] << { 'name' => params[:name], 'status' => 'default' }
end
@encrypted_config.save! @encrypted_config.save!
redirect_to settings_esign_path, notice: I18n.t('default_certificate_has_been_selected') redirect_to settings_esign_path, notice: 'Default certificate has been selected'
end end
def destroy def destroy
@@ -89,7 +81,7 @@ class EsignSettingsController < ApplicationController
@encrypted_config.save! @encrypted_config.save!
redirect_to settings_esign_path, notice: I18n.t('certificate_has_been_removed') redirect_to settings_esign_path, notice: 'Certificate has been removed'
end end
private private
+5 -5
View File
@@ -18,11 +18,11 @@ class MfaSetupController < ApplicationController
current_user.otp_required_for_login = true current_user.otp_required_for_login = true
current_user.save! current_user.save!
redirect_to settings_profile_index_path, notice: I18n.t('2fa_has_been_configured') redirect_to settings_profile_index_path, notice: '2FA has been configured'
else else
@provision_url = current_user.otp_provisioning_uri(current_user.email, issuer: Docuseal.product_name) @provision_url = current_user.otp_provisioning_uri(current_user.email, issuer: Docuseal.product_name)
@error_message = I18n.t('code_is_invalid') @error_message = 'Code is invalid'
render turbo_stream: turbo_stream.replace(:mfa_form, partial: 'mfa_setup/form'), status: :unprocessable_entity render turbo_stream: turbo_stream.replace(:mfa_form, partial: 'mfa_setup/form'), status: :unprocessable_entity
end end
@@ -32,9 +32,9 @@ class MfaSetupController < ApplicationController
if current_user.validate_and_consume_otp!(params[:otp_attempt]) if current_user.validate_and_consume_otp!(params[:otp_attempt])
current_user.update!(otp_required_for_login: false, otp_secret: nil) current_user.update!(otp_required_for_login: false, otp_secret: nil)
redirect_to settings_profile_index_path, notice: I18n.t('2fa_has_been_removed') redirect_to settings_profile_index_path, notice: '2FA has been removed'
else else
@error_message = I18n.t('code_is_invalid') @error_message = 'Code is invalid'
render turbo_stream: turbo_stream.replace(:modal, template: 'mfa_setup/edit'), status: :unprocessable_entity render turbo_stream: turbo_stream.replace(:modal, template: 'mfa_setup/edit'), status: :unprocessable_entity
end end
@@ -43,7 +43,7 @@ class MfaSetupController < ApplicationController
private private
def set_provision_url def set_provision_url
return redirect_to root_path, alert: I18n.t('2fa_has_been_set_up_already') if current_user.otp_required_for_login return redirect_to root_path, alert: '2FA has been set up already' if current_user.otp_required_for_login
current_user.otp_secret ||= User.generate_otp_secret current_user.otp_secret ||= User.generate_otp_secret
@@ -13,9 +13,9 @@ class NotificationsSettingsController < ApplicationController
def create def create
if @account_config.value.present? ? @account_config.save : @account_config.delete if @account_config.value.present? ? @account_config.save : @account_config.delete
redirect_back fallback_location: settings_notifications_path, notice: I18n.t('changes_have_been_saved') redirect_back fallback_location: settings_notifications_path, notice: 'Changes have been saved'
else else
redirect_back fallback_location: settings_notifications_path, alert: I18n.t('unable_to_save') redirect_back fallback_location: settings_notifications_path, alert: 'Unable to save'
end end
end end
@@ -39,7 +39,7 @@ class NotificationsSettingsController < ApplicationController
end end
def email_config_params def email_config_params
params.require(:account_config).permit(:key, :value, { value: {} }, { value: [] }).tap do |attrs| params.require(:account_config).permit!.tap do |attrs|
attrs[:key] = nil unless attrs[:key].in?([AccountConfig::BCC_EMAILS, AccountConfig::SUBMITTER_REMINDERS]) attrs[:key] = nil unless attrs[:key].in?([AccountConfig::BCC_EMAILS, AccountConfig::SUBMITTER_REMINDERS])
end end
end end
@@ -6,8 +6,7 @@ class PersonalizationSettingsController < ApplicationController
AccountConfig::SUBMITTER_INVITATION_EMAIL_KEY, AccountConfig::SUBMITTER_INVITATION_EMAIL_KEY,
AccountConfig::SUBMITTER_DOCUMENTS_COPY_EMAIL_KEY, AccountConfig::SUBMITTER_DOCUMENTS_COPY_EMAIL_KEY,
AccountConfig::SUBMITTER_COMPLETED_EMAIL_KEY, AccountConfig::SUBMITTER_COMPLETED_EMAIL_KEY,
AccountConfig::FORM_COMPLETED_MESSAGE_KEY, AccountConfig::FORM_COMPLETED_MESSAGE_KEY
*(Docuseal.multitenant? ? [] : [AccountConfig::POLICY_LINKS_KEY])
].freeze ].freeze
InvalidKey = Class.new(StandardError) InvalidKey = Class.new(StandardError)
@@ -31,7 +30,7 @@ class PersonalizationSettingsController < ApplicationController
@account_config.save! @account_config.save!
end end
redirect_back(fallback_location: settings_personalization_path, notice: I18n.t('settings_have_been_saved')) redirect_back(fallback_location: settings_personalization_path, notice: 'Settings have been saved.')
end end
private private
@@ -50,7 +49,7 @@ class PersonalizationSettingsController < ApplicationController
end end
def account_config_params def account_config_params
attrs = params.require(:account_config).permit(:key, :value, { value: {} }, { value: [] }) attrs = params.require(:account_config).permit!
return attrs if attrs[:value].is_a?(String) return attrs if attrs[:value].is_a?(String)
@@ -12,10 +12,7 @@ class PreviewDocumentPageController < ActionController::API
return head :not_found unless attachment return head :not_found unless attachment
@template = attachment.record preview_image = attachment.preview_images.joins(:blob).find_by(blob: { filename: "#{params[:id]}#{FORMAT}" })
preview_image = attachment.preview_images.joins(:blob)
.find_by(blob: { filename: ["#{params[:id]}.png", "#{params[:id]}.jpg"] })
return redirect_to preview_image.url, allow_other_host: true if preview_image return redirect_to preview_image.url, allow_other_host: true if preview_image
+2 -2
View File
@@ -9,7 +9,7 @@ class ProfileController < ApplicationController
def update_contact def update_contact
if current_user.update(contact_params) if current_user.update(contact_params)
redirect_to settings_profile_index_path, notice: I18n.t('contact_information_has_been_update') redirect_to settings_profile_index_path, notice: 'Contact information has been updated'
else else
render :index, status: :unprocessable_entity render :index, status: :unprocessable_entity
end end
@@ -18,7 +18,7 @@ class ProfileController < ApplicationController
def update_password def update_password
if current_user.update(password_params) if current_user.update(password_params)
bypass_sign_in(current_user) bypass_sign_in(current_user)
redirect_to settings_profile_index_path, notice: I18n.t('password_has_been_changed') redirect_to settings_profile_index_path, notice: 'Password has been changed'
else else
render :index, status: :unprocessable_entity render :index, status: :unprocessable_entity
end end
-4
View File
@@ -1,4 +0,0 @@
# frozen_string_literal: true
class PwaController < ActionController::Base
end
@@ -1,17 +0,0 @@
# frozen_string_literal: true
class SearchEntriesReindexController < ApplicationController
def create
authorize!(:manage, EncryptedConfig)
ReindexAllSearchEntriesJob.perform_async
AccountConfig.find_or_initialize_by(account_id: Account.minimum(:id), key: :fulltext_search)
.update!(value: true)
Docuseal.instance_variable_set(:@fulltext_search, nil)
redirect_back(fallback_location: settings_account_path,
notice: "Started building search index. Visit #{root_url}jobs/busy to check progress.")
end
end
@@ -7,40 +7,27 @@ class SendSubmissionEmailController < ApplicationController
skip_before_action :verify_authenticity_token skip_before_action :verify_authenticity_token
skip_authorization_check skip_authorization_check
SEND_DURATION = 30.minutes def success; end
def create def create
if params[:template_slug] @submitter =
template = Template.find_by!(slug: params[:template_slug]) if params[:template_slug]
Submitter.joins(submission: :template).find_by!(email: params[:email].to_s.downcase,
@submitter = template: { slug: params[:template_slug] })
Submitter.completed.where(submission: template.submissions).find_by!(email: params[:email].to_s.downcase) elsif params[:submission_slug]
elsif params[:submission_slug] Submitter.joins(:submission).find_by!(email: params[:email].to_s.downcase,
submission = Submission.find_by(slug: params[:submission_slug]) submission: { slug: params[:submission_slug] })
else
if submission Submitter.find_by!(slug: params[:submitter_slug])
@submitter = Submitter.completed.find_by(submission: submission, email: params[:email].to_s.downcase)
end end
return redirect_to submissions_preview_completed_path(params[:submission_slug], status: :error) unless @submitter
else
@submitter = Submitter.completed.find_by!(slug: params[:submitter_slug])
end
RateLimit.call("send-email-#{@submitter.id}", limit: 2, ttl: 5.minutes) RateLimit.call("send-email-#{@submitter.id}", limit: 2, ttl: 5.minutes)
SubmitterMailer.documents_copy_email(@submitter, sig: true).deliver_later! unless already_sent?(@submitter) SubmitterMailer.documents_copy_email(@submitter, sig: true).deliver_later!
respond_to do |f| respond_to do |f|
f.html { render :success } f.html { redirect_to success_send_submission_email_index_path }
f.json { head :ok } f.json { head :ok }
end end
end end
private
def already_sent?(submitter)
EmailEvent.exists?(tag: :submitter_documents_copy, email: submitter.email, emailable: submitter,
event_type: :send, created_at: SEND_DURATION.ago..Time.current)
end
end end
+1 -1
View File
@@ -12,7 +12,7 @@ class SessionsController < Devise::SessionsController
Rollbar.warning('Sign in new user') if defined?(Rollbar) Rollbar.warning('Sign in new user') if defined?(Rollbar)
return redirect_to new_registration_path(sign_up: true, user: sign_in_params.slice(:email)), return redirect_to new_registration_path(sign_up: true, user: sign_in_params.slice(:email)),
notice: I18n.t('create_a_new_account') notice: 'Create a new account'
end end
if User.exists?(email:, otp_required_for_login: true) && sign_in_params[:otp_attempt].blank? if User.exists?(email:, otp_required_for_login: true) && sign_in_params[:otp_attempt].blank?
+4 -5
View File
@@ -21,7 +21,7 @@ class SetupController < ApplicationController
@encrypted_config = EncryptedConfig.new(encrypted_config_params) @encrypted_config = EncryptedConfig.new(encrypted_config_params)
unless URI.parse(encrypted_config_params[:value].to_s).class.in?([URI::HTTP, URI::HTTPS]) unless URI.parse(encrypted_config_params[:value].to_s).class.in?([URI::HTTP, URI::HTTPS])
@encrypted_config.errors.add(:value, I18n.t('should_be_a_valid_url')) @encrypted_config.errors.add(:value, 'should be a valid URL')
return render :index, status: :unprocessable_entity return render :index, status: :unprocessable_entity
end end
@@ -34,7 +34,6 @@ class SetupController < ApplicationController
{ key: EncryptedConfig::ESIGN_CERTS_KEY, value: GenerateCertificate.call.transform_values(&:to_pem) } { key: EncryptedConfig::ESIGN_CERTS_KEY, value: GenerateCertificate.call.transform_values(&:to_pem) }
] ]
@account.encrypted_configs.create!(encrypted_configs) @account.encrypted_configs.create!(encrypted_configs)
@account.account_configs.create!(key: :fulltext_search, value: true) if SearchEntry.table_exists?
Docuseal.refresh_default_url_options! Docuseal.refresh_default_url_options!
@@ -57,7 +56,7 @@ class SetupController < ApplicationController
def account_params def account_params
return {} unless params[:account] return {} unless params[:account]
params.require(:account).permit(:name, :timezone, :locale) params.require(:account).permit(:name, :timezone)
end end
def encrypted_config_params def encrypted_config_params
@@ -67,10 +66,10 @@ class SetupController < ApplicationController
end end
def redirect_to_root_if_signed def redirect_to_root_if_signed
redirect_to root_path, notice: I18n.t('you_are_already_signed_in') redirect_to root_path, notice: 'You are already signed in'
end end
def ensure_first_user_not_created! def ensure_first_user_not_created!
redirect_to new_user_session_path, notice: I18n.t('please_sign_in') if User.exists? redirect_to new_user_session_path, notice: 'Please sign in.' if User.exists?
end end
end end
+34 -177
View File
@@ -6,159 +6,73 @@ class StartFormController < ApplicationController
skip_before_action :authenticate_user! skip_before_action :authenticate_user!
skip_authorization_check skip_authorization_check
around_action :with_browser_locale, only: %i[show update completed] around_action :with_browser_locale, only: %i[show completed]
before_action :maybe_redirect_com, only: %i[show completed]
before_action :load_resubmit_submitter, only: :update
before_action :load_template before_action :load_template
before_action :authorize_start!, only: :update
COOKIES_TTL = 12.hours
COOKIES_DEFAULTS = { httponly: true, secure: Rails.env.production? }.freeze
def show def show
raise ActionController::RoutingError, I18n.t('not_found') if @template.preferences['require_phone_2fa'] @submitter = @template.submissions.new(account_id: @template.account_id)
.submitters.new(uuid: @template.submitters.first['uuid'])
if @template.shared_link?
@submitter = @template.submissions.new(account_id: @template.account_id)
.submitters.new(account_id: @template.account_id,
uuid: (filter_undefined_submitters(@template).first ||
@template.submitters.first)['uuid'])
render :email_verification if params[:email_verification]
else
Rollbar.warning("Not shared template: #{@template.id}") if defined?(Rollbar)
return render :private if current_user && current_ability.can?(:read, @template)
raise ActionController::RoutingError, I18n.t('not_found')
end
end end
def update def update
@submitter = find_or_initialize_submitter(@template, submitter_params) return redirect_to start_form_path(@template.slug) if @template.archived_at?
@submitter = Submitter.where(submission: @template.submissions.where(expire_at: Time.current..)
.or(@template.submissions.where(expire_at: nil)).where(archived_at: nil))
.order(id: :desc)
.then { |rel| params[:resubmit].present? ? rel.where(completed_at: nil) : rel }
.find_or_initialize_by(**submitter_params.compact_blank)
if @submitter.completed_at? if @submitter.completed_at?
redirect_to start_form_completed_path(@template.slug, submitter_params.compact_blank) redirect_to start_form_completed_path(@template.slug, email: submitter_params[:email])
else else
if filter_undefined_submitters(@template).size > 1 && @submitter.new_record? if @template.submitters.to_a.size > 1 && @submitter.new_record?
@error_message = multiple_submitters_error_message @error_message = 'Not found'
return render :show, status: :unprocessable_entity return render :show
end end
if (is_new_record = @submitter.new_record?) assign_submission_attributes(@submitter, @template) if @submitter.new_record?
assign_submission_attributes(@submitter, @template)
Submissions::AssignDefinedSubmitters.call(@submitter.submission) is_new_record = @submitter.new_record?
else
@submitter.assign_attributes(ip: request.remote_ip, ua: request.user_agent)
end
if @template.preferences['shared_link_2fa'] == true if @submitter.save
handle_require_2fa(@submitter, is_new_record:) if is_new_record
elsif @submitter.errors.blank? && @submitter.save SendSubmissionCreatedWebhookRequestJob.perform_async({ 'submission_id' => @submitter.submission.id })
enqueue_new_submitter_jobs(@submitter) if is_new_record end
redirect_to submit_form_path(@submitter.slug) redirect_to submit_form_path(@submitter.slug)
else else
render :show, status: :unprocessable_entity render :show
end end
end end
end end
def completed def completed
return redirect_to start_form_path(@template.slug) if !@template.shared_link? || @template.archived_at?
submitter_params = params.permit(:name, :email, :phone).tap do |attrs|
attrs[:email] = Submissions.normalize_email(attrs[:email])
end
required_fields = @template.preferences.fetch('link_form_fields', ['email'])
required_params = required_fields.index_with { |key| submitter_params[key] }
raise ActionController::RoutingError, I18n.t('not_found') if required_params.any? { |_, v| v.blank? } ||
required_params.except('name').compact_blank.blank?
@submitter = Submitter.where(submission: @template.submissions) @submitter = Submitter.where(submission: @template.submissions)
.where.not(completed_at: nil) .where.not(completed_at: nil)
.find_by!(required_params) .find_by!(email: params[:email])
end end
private private
def enqueue_new_submitter_jobs(submitter)
WebhookUrls.enqueue_events(submitter.submission, 'submission.created')
SearchEntries.enqueue_reindex(submitter)
return unless submitter.submission.expire_at?
ProcessSubmissionExpiredJob.perform_at(submitter.submission.expire_at, 'submission_id' => submitter.submission_id)
end
def load_resubmit_submitter
@resubmit_submitter =
if params[:resubmit].present? && !params[:resubmit].in?([true, 'true'])
Submitter.find_by(slug: params[:resubmit])
end
end
def authorize_start!
return redirect_to start_form_path(@template.slug) if @template.archived_at?
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)
redirect_to start_form_path(@template.slug)
end
def find_or_initialize_submitter(template, submitter_params)
required_fields = template.preferences.fetch('link_form_fields', ['email'])
required_params = required_fields.index_with { |key| submitter_params[key] }
find_params = required_params.except('name')
submitter = Submitter.new if find_params.compact_blank.blank?
submitter ||=
Submitter
.where(submission: template.submissions.where(expire_at: Time.current..)
.or(template.submissions.where(expire_at: nil)).where(archived_at: nil))
.order(id: :desc)
.where(declined_at: nil)
.where(external_id: nil)
.where(template.preferences['shared_link_2fa'] == true ? {} : { ip: [nil, request.remote_ip] })
.then { |rel| params[:resubmit].present? || params[:selfsign].present? ? rel.where(completed_at: nil) : rel }
.find_or_initialize_by(find_params)
submitter.name = required_params['name'] if submitter.new_record?
unless @resubmit_submitter
required_params.each do |key, value|
submitter.errors.add(key.to_sym, :blank) if value.blank?
end
end
submitter
end
def assign_submission_attributes(submitter, template) def assign_submission_attributes(submitter, template)
resubmit_submitter =
if params[:resubmit].present?
Submitter.where(submission: @template.submissions).find_by(slug: params[:resubmit])
end
submitter.assign_attributes( submitter.assign_attributes(
uuid: (filter_undefined_submitters(template).first || @template.submitters.first)['uuid'], uuid: template.submitters.first['uuid'],
ip: request.remote_ip, ip: request.remote_ip,
ua: request.user_agent, ua: request.user_agent,
values: @resubmit_submitter&.preferences&.fetch('default_values', nil) || {}, values: resubmit_submitter&.preferences&.fetch('default_values', nil) || {},
preferences: @resubmit_submitter&.preferences.presence || { 'send_email' => true }, preferences: resubmit_submitter&.preferences.presence || { 'send_email' => true },
metadata: @resubmit_submitter&.metadata.presence || {} metadata: resubmit_submitter&.metadata.presence || {}
) )
submitter.assign_attributes(@resubmit_submitter.slice(:name, :email, :phone)) if @resubmit_submitter
if submitter.values.present? if submitter.values.present?
@resubmit_submitter.attachments.each do |attachment| resubmit_submitter.attachments.each do |attachment|
submitter.attachments << attachment.dup if submitter.values.value?(attachment.uuid) submitter.attachments << attachment.dup if submitter.values.value?(attachment.uuid)
end end
end end
@@ -166,77 +80,20 @@ class StartFormController < ApplicationController
submitter.submission ||= Submission.new(template:, submitter.submission ||= Submission.new(template:,
account_id: template.account_id, account_id: template.account_id,
template_submitters: template.submitters, template_submitters: template.submitters,
expire_at: Templates.build_default_expire_at(template),
submitters: [submitter],
source: :link) source: :link)
submitter.account_id = submitter.submission.account_id
submitter submitter
end end
def filter_undefined_submitters(template)
Templates.filter_undefined_submitters(template.submitters)
end
def submitter_params def submitter_params
return { 'email' => current_user.email, 'name' => current_user.full_name } if params[:selfsign]
return @resubmit_submitter.slice(:name, :phone, :email) if @resubmit_submitter.present?
params.require(:submitter).permit(:email, :phone, :name).tap do |attrs| params.require(:submitter).permit(:email, :phone, :name).tap do |attrs|
attrs[:email] = Submissions.normalize_email(attrs[:email]) attrs[:email] = Submissions.normalize_email(attrs[:email])
end end
end end
def load_template def load_template
@template = slug = params[:slug] || params[:start_form_slug]
if @resubmit_submitter
@resubmit_submitter.template
else
Template.find_by!(slug: params[:slug] || params[:start_form_slug])
end
end
def multiple_submitters_error_message @template = Template.find_by!(slug:)
if current_user&.account_id == @template.account_id
helpers.t('this_submission_has_multiple_signers_which_prevents_the_use_of_a_sharing_link_html')
else
I18n.t('not_found')
end
end
def handle_require_2fa(submitter, is_new_record:)
return render :show, status: :unprocessable_entity if submitter.errors.present?
is_otp_verified = Submitters.verify_link_otp!(params[:one_time_code], submitter)
if cookies.encrypted[:email_2fa_slug] == submitter.slug || is_otp_verified
if submitter.save
enqueue_new_submitter_jobs(submitter) if is_new_record
if is_otp_verified
SubmissionEvents.create_with_tracking_data(submitter, 'email_verified', request)
cookies.encrypted[:email_2fa_slug] =
{ value: submitter.slug, expires: COOKIES_TTL.from_now, **COOKIES_DEFAULTS }
end
redirect_to submit_form_path(submitter.slug)
else
render :show, status: :unprocessable_entity
end
else
Submitters.send_shared_link_email_verification_code(submitter, request:)
render :email_verification
end
rescue Submitters::UnableToSendCode, Submitters::InvalidOtp => e
redirect_to start_form_path(submitter.submission.template.slug,
params: submitter_params.merge(email_verification: true)),
alert: e.message
rescue RateLimit::LimitApproached
redirect_to start_form_path(submitter.submission.template.slug,
params: submitter_params.merge(email_verification: true)),
alert: I18n.t(:too_many_attempts)
end end
end end
@@ -1,31 +0,0 @@
# frozen_string_literal: true
class StartFormEmail2faSendController < ApplicationController
around_action :with_browser_locale
skip_before_action :authenticate_user!
skip_authorization_check
def create
@template = Template.find_by!(slug: params[:slug])
@submitter = @template.submissions.new(account_id: @template.account_id)
.submitters.new(**submitter_params, account_id: @template.account_id)
Submitters.send_shared_link_email_verification_code(@submitter, request:)
redir_params = { notice: I18n.t(:code_has_been_resent) } if params[:resend]
redirect_to start_form_path(@template.slug, params: submitter_params.merge(email_verification: true)),
**redir_params
rescue Submitters::UnableToSendCode => e
redirect_to start_form_path(@template.slug, params: submitter_params.merge(email_verification: true)),
alert: e.message
end
private
def submitter_params
params.require(:submitter).permit(:name, :email, :phone)
end
end
@@ -11,7 +11,7 @@ class StorageSettingsController < ApplicationController
if @encrypted_config.update(storage_configs) if @encrypted_config.update(storage_configs)
LoadActiveStorageConfigs.reload LoadActiveStorageConfigs.reload
redirect_to settings_storage_index_path, notice: I18n.t('changes_have_been_saved') redirect_to settings_storage_index_path, notice: 'Changes have been saved'
else else
render :index, status: :unprocessable_entity render :index, status: :unprocessable_entity
end end
@@ -1,26 +0,0 @@
# frozen_string_literal: true
class SubmissionEventsController < ApplicationController
SUBMISSION_EVENT_ICONS = {
'view_form' => 'eye',
'start_form' => 'player_play',
'complete_form' => 'check',
'send_email' => 'mail_forward',
'click_email' => 'hand_click',
'api_complete_form' => 'check',
'send_reminder_email' => 'mail_forward',
'send_2fa_sms' => '2fa',
'send_sms' => 'send',
'phone_verified' => 'phone_check',
'email_verified' => 'email_check',
'click_sms' => 'hand_click',
'decline_form' => 'x',
'start_verification' => 'player_play',
'complete_verification' => 'check',
'invite_party' => 'user_plus'
}.freeze
load_and_authorize_resource :submission
def index; end
end
@@ -4,20 +4,12 @@ class SubmissionsArchivedController < ApplicationController
load_and_authorize_resource :submission, parent: false load_and_authorize_resource :submission, parent: false
def index def index
@submissions = @submissions.left_joins(:template) @submissions = @submissions.joins(:template)
@submissions = @submissions.where.not(archived_at: nil) @submissions = @submissions.where.not(archived_at: nil)
.or(@submissions.where.not(templates: { archived_at: nil })) .or(@submissions.where.not(templates: { archived_at: nil }))
.preload(:template_accesses, :created_by_user, template: :author) .preload(:created_by_user, template: :author)
@submissions = Submissions.search(@submissions, params[:q], search_template: true)
@submissions = Submissions.search(current_user, @submissions, params[:q], search_template: true) @pagy, @submissions = pagy(@submissions.preload(:submitters).order(id: :desc))
@submissions = Submissions::Filter.call(@submissions, current_user, params)
@submissions = if params[:completed_at_from].present? || params[:completed_at_to].present?
@submissions.order(Submitter.arel_table[:completed_at].maximum.desc)
else
@submissions.order(id: :desc)
end
@pagy, @submissions = pagy_auto(@submissions.preload(submitters: :start_form_submission_events))
end end
end end
+11 -27
View File
@@ -6,22 +6,9 @@ class SubmissionsController < ApplicationController
load_and_authorize_resource :submission, only: %i[show destroy] load_and_authorize_resource :submission, only: %i[show destroy]
prepend_before_action :maybe_redirect_com, only: %i[show]
before_action only: :create do
authorize!(:create, Submission)
end
def show def show
@submission = Submissions.preload_with_pages(@submission) @submission = Submissions.preload_with_pages(@submission)
unless @submission.submitters.all?(&:completed_at?)
ActiveRecord::Associations::Preloader.new(
records: [@submission],
associations: [submitters: :start_form_submission_events]
).call
end
render :show, layout: 'plain' render :show, layout: 'plain'
end end
@@ -30,6 +17,8 @@ class SubmissionsController < ApplicationController
end end
def create def create
authorize!(:create, Submission)
save_template_message(@template, params) if params[:save_message] == '1' save_template_message(@template, params) if params[:save_message] == '1'
if params[:is_custom_message] != '1' if params[:is_custom_message] != '1'
@@ -54,35 +43,30 @@ class SubmissionsController < ApplicationController
params: params.merge('send_completed_email' => true)) params: params.merge('send_completed_email' => true))
end end
WebhookUrls.enqueue_events(submissions, 'submission.created') submissions.each do |submission|
SendSubmissionCreatedWebhookRequestJob.perform_async({ 'submission_id' => submission.id })
end
Submissions.send_signature_requests(submissions) Submissions.send_signature_requests(submissions)
SearchEntries.enqueue_reindex(submissions) redirect_to template_path(@template), notice: 'New recipients have been added'
redirect_to template_path(@template), notice: I18n.t('new_recipients_have_been_added')
rescue Submissions::CreateFromSubmitters::BaseError => e
render turbo_stream: turbo_stream.replace(:submitters_error,
partial: 'submissions/error',
locals: { error: e.message }),
status: :unprocessable_entity
end end
def destroy def destroy
notice = notice =
if params[:permanently].in?(['true', true]) if params[:permanently].present?
@submission.destroy! @submission.destroy!
I18n.t('submission_has_been_removed') 'Submission has been removed'
else else
@submission.update!(archived_at: Time.current) @submission.update!(archived_at: Time.current)
WebhookUrls.enqueue_events(@submission, 'submission.archived') SendSubmissionArchivedWebhookRequestJob.perform_async('submission_id' => @submission.id)
I18n.t('submission_has_been_archived') 'Submission has been archived'
end end
redirect_back(fallback_location: @submission.template_id ? template_path(@submission.template) : root_path, notice:) redirect_back(fallback_location: template_path(@submission.template), notice:)
end end
private private
@@ -4,21 +4,17 @@ class SubmissionsDashboardController < ApplicationController
load_and_authorize_resource :submission, parent: false load_and_authorize_resource :submission, parent: false
def index def index
@submissions = @submissions.left_joins(:template) @submissions = @submissions.joins(:template)
@submissions = @submissions.where(archived_at: nil) @submissions = @submissions.where(archived_at: nil)
.where(templates: { archived_at: nil }) .where(templates: { archived_at: nil })
.preload(:template_accesses, :created_by_user, template: :author) .preload(:created_by_user, template: :author)
@submissions = Submissions.search(current_user, @submissions, params[:q], search_template: true) @submissions = Submissions.search(@submissions, params[:q], search_template: true)
@submissions = Submissions::Filter.call(@submissions, current_user, params)
@submissions = if params[:completed_at_from].present? || params[:completed_at_to].present? @submissions = @submissions.pending if params[:status] == 'pending'
@submissions.order(Submitter.arel_table[:completed_at].maximum.desc) @submissions = @submissions.completed if params[:status] == 'completed'
else
@submissions.order(id: :desc)
end
@pagy, @submissions = pagy_auto(@submissions.preload(submitters: :start_form_submission_events)) @pagy, @submissions = pagy(@submissions.preload(:submitters).order(id: :desc))
end end
end end
@@ -8,33 +8,33 @@ class SubmissionsDownloadController < ApplicationController
FILES_TTL = 5.minutes FILES_TTL = 5.minutes
def index def index
@submitter = Submitter.find_signed(params[:sig], purpose: :download_completed) if params[:sig].present? submitter = Submitter.find_signed(params[:sig], purpose: :download_completed) if params[:sig].present?
signature_valid = signature_valid =
if @submitter&.slug == params[:submitter_slug] if submitter&.slug == params[:submitter_slug]
true true
else else
@submitter = nil submitter = nil
end end
@submitter ||= Submitter.find_by!(slug: params[:submitter_slug]) submitter ||= Submitter.find_by!(slug: params[:submitter_slug])
Submissions::EnsureResultGenerated.call(@submitter) Submissions::EnsureResultGenerated.call(submitter)
last_submitter = @submitter.submission.submitters.where.not(completed_at: nil).order(:completed_at).last last_submitter = submitter.submission.submitters.where.not(completed_at: nil).order(:completed_at).last
return head :not_found unless last_submitter
Submissions::EnsureResultGenerated.call(last_submitter) Submissions::EnsureResultGenerated.call(last_submitter)
return head :not_found unless last_submitter.completed_at?
if last_submitter.completed_at < TTL.ago && !signature_valid && !current_user_submitter?(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) Rollbar.info("TTL: #{last_submitter.id}") if defined?(Rollbar)
return head :not_found return head :not_found
end end
if params[:combined] == 'true' if params[:combined]
url = build_combined_url(@submitter) url = build_combined_url(submitter)
if url if url
render json: [url] render json: [url]
@@ -53,15 +53,8 @@ class SubmissionsDownloadController < ApplicationController
end end
def build_urls(submitter) def build_urls(submitter)
filename_format = AccountConfig.find_or_initialize_by(account_id: submitter.account_id,
key: AccountConfig::DOCUMENT_FILENAME_FORMAT_KEY)&.value
Submitters.select_attachments_for_download(submitter).map do |attachment| Submitters.select_attachments_for_download(submitter).map do |attachment|
ActiveStorage::Blob.proxy_url( ActiveStorage::Blob.proxy_url(attachment.blob, expires_at: FILES_TTL.from_now.to_i)
attachment.blob,
expires_at: FILES_TTL.from_now.to_i,
filename: Submitters.build_document_filename(submitter, attachment.blob, filename_format)
)
end end
end end
@@ -72,13 +65,6 @@ class SubmissionsDownloadController < ApplicationController
attachment = submitter.submission.combined_document_attachment attachment = submitter.submission.combined_document_attachment
attachment ||= Submissions::GenerateCombinedAttachment.call(submitter) attachment ||= Submissions::GenerateCombinedAttachment.call(submitter)
filename_format = AccountConfig.find_or_initialize_by(account_id: submitter.account_id, ActiveStorage::Blob.proxy_url(attachment.blob, expires_at: FILES_TTL.from_now.to_i)
key: AccountConfig::DOCUMENT_FILENAME_FORMAT_KEY)&.value
ActiveStorage::Blob.proxy_url(
attachment.blob,
expires_at: FILES_TTL.from_now.to_i,
filename: Submitters.build_document_filename(submitter, attachment.blob, filename_format)
)
end end
end end
@@ -1,18 +0,0 @@
# frozen_string_literal: true
class SubmissionsFiltersController < ApplicationController
ALLOWED_NAMES = %w[
author
completed_at
status
created_at
].freeze
skip_authorization_check
def show
return head :not_found unless ALLOWED_NAMES.include?(params[:name])
render params[:name]
end
end
@@ -1,12 +1,9 @@
# frozen_string_literal: true # frozen_string_literal: true
class SubmissionsPreviewController < ApplicationController class SubmissionsPreviewController < ApplicationController
around_action :with_browser_locale
skip_before_action :authenticate_user! skip_before_action :authenticate_user!
skip_authorization_check skip_authorization_check
prepend_before_action :maybe_redirect_com, only: %i[show completed]
TTL = 40.minutes TTL = 40.minutes
def show def show
@@ -21,14 +18,11 @@ class SubmissionsPreviewController < ApplicationController
@submission ||= Submission.find_by!(slug: params[:slug]) @submission ||= Submission.find_by!(slug: params[:slug])
raise ActionController::RoutingError, I18n.t('not_found') if @submission.account.archived_at? if !@submission.submitters.all?(&:completed_at?) && current_user.blank?
raise ActionController::RoutingError, 'Not Found'
if !@submission.submitters.all?(&:completed_at?) && !signature_valid &&
(!current_user || !current_ability.can?(:read, @submission))
raise ActionController::RoutingError, I18n.t('not_found')
end end
if use_signature?(@submission) && !signature_valid if !submission_valid_ttl?(@submission) && !signature_valid
Rollbar.info("TTL: #{@submission.id}") if defined?(Rollbar) Rollbar.info("TTL: #{@submission.id}") if defined?(Rollbar)
return redirect_to submissions_preview_completed_path(@submission.slug) return redirect_to submissions_preview_completed_path(@submission.slug)
@@ -41,22 +35,15 @@ class SubmissionsPreviewController < ApplicationController
def completed def completed
@submission = Submission.find_by!(slug: params[:submissions_preview_slug]) @submission = Submission.find_by!(slug: params[:submissions_preview_slug])
@template = @submission.template
render :completed, layout: 'form' render :completed, layout: 'form'
end end
private private
def use_signature?(submission)
return false if current_user && can?(:read, submission)
return true if submission.submitters.any? { |e| e.preferences['require_phone_2fa'] }
return true if submission.template&.preferences&.dig('require_phone_2fa')
!submission_valid_ttl?(submission)
end
def submission_valid_ttl?(submission) def submission_valid_ttl?(submission)
return true if current_user && current_user.account.submissions.exists?(id: submission.id)
last_submitter = submission.submitters.select(&:completed_at?).max_by(&:completed_at) last_submitter = submission.submitters.select(&:completed_at?).max_by(&:completed_at)
last_submitter && last_submitter.completed_at > TTL.ago last_submitter && last_submitter.completed_at > TTL.ago
@@ -1,11 +0,0 @@
# frozen_string_literal: true
class SubmissionsUnarchiveController < ApplicationController
load_and_authorize_resource :submission
def create
@submission.update!(archived_at: nil)
redirect_to submission_path(@submission), notice: I18n.t('submission_has_been_unarchived')
end
end
+20 -59
View File
@@ -7,28 +7,23 @@ class SubmitFormController < ApplicationController
skip_before_action :authenticate_user! skip_before_action :authenticate_user!
skip_authorization_check skip_authorization_check
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]
CONFIG_KEYS = [].freeze CONFIG_KEYS = [].freeze
def show def show
submission = @submitter.submission @submitter = Submitter.find_by!(slug: params[:slug])
return redirect_to submit_form_completed_path(@submitter.slug) if @submitter.completed_at? return redirect_to submit_form_completed_path(@submitter.slug) if @submitter.completed_at?
return render :archived if @submitter.submission.template.archived_at? || @submitter.submission.archived_at?
return render :expired if @submitter.submission.expired?
@form_configs = Submitters::FormConfigs.call(@submitter, CONFIG_KEYS) Submitters.preload_with_pages(@submitter)
return render :awaiting if (@form_configs[:enforce_signing_order] ||
submission.template&.preferences&.dig('submitters_order') == 'preserved') &&
!Submitters.current_submitter_order?(@submitter)
Submissions.preload_with_pages(submission)
Submitters::MaybeUpdateDefaultValues.call(@submitter, current_user) Submitters::MaybeUpdateDefaultValues.call(@submitter, current_user)
@attachments_index = build_attachments_index(submission) @attachments_index = ActiveStorage::Attachment.where(record: @submitter.submission.submitters, name: :attachments)
.preload(:blob).index_by(&:uuid)
@form_configs = Submitters::FormConfigs.call(@submitter, CONFIG_KEYS)
return unless @form_configs[:prefill_signature] return unless @form_configs[:prefill_signature]
@@ -47,64 +42,30 @@ class SubmitFormController < ApplicationController
end end
def update def update
if @submitter.completed_at? submitter = Submitter.find_by!(slug: params[:slug])
return render json: { error: I18n.t('form_has_been_completed_already') }, status: :unprocessable_entity
if submitter.completed_at?
return render json: { error: 'Form has been completed already.' }, status: :unprocessable_entity
end end
if @submitter.submission.template&.archived_at? || @submitter.submission.archived_at? if submitter.template.archived_at? || submitter.submission.archived_at?
return render json: { error: I18n.t('form_has_been_archived') }, status: :unprocessable_entity return render json: { error: 'Form has been archived.' }, status: :unprocessable_entity
end end
if @submitter.submission.expired? if submitter.submission.expired?
return render json: { error: I18n.t('form_has_been_expired') }, status: :unprocessable_entity return render json: { error: 'Form has been expired.' }, status: :unprocessable_entity
end end
if @submitter.declined_at? Submitters::SubmitValues.call(submitter, params, request)
return render json: { error: I18n.t('form_has_been_declined') },
status: :unprocessable_entity
end
Submitters::SubmitValues.call(@submitter, params, request)
head :ok head :ok
rescue Submitters::SubmitValues::RequiredFieldError => e
Rollbar.warning("Required field #{@submitter.id}: #{e.message}") if defined?(Rollbar)
render json: { field_uuid: e.message }, status: :unprocessable_entity
rescue Submitters::SubmitValues::ValidationError => e rescue Submitters::SubmitValues::ValidationError => e
render json: { error: e.message }, status: :unprocessable_entity render json: { error: e.message }, status: :unprocessable_entity
end end
def completed; end def completed
@submitter = Submitter.completed.find_by!(slug: params[:submit_form_slug])
end
def success; end 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
redirect_to start_form_path(@submitter.submission.template.slug)
end
def maybe_render_locked_page
return render :archived if @submitter.submission.template&.archived_at? ||
@submitter.submission.archived_at? ||
@submitter.account.archived_at?
return render :expired if @submitter.submission.expired?
render :declined if @submitter.declined_at?
end
def load_submitter
@submitter = Submitter.find_by!(slug: params[:slug] || params[:submit_form_slug])
end
def build_attachments_index(submission)
ActiveStorage::Attachment.where(record: submission.submitters, name: :attachments)
.preload(:blob).index_by(&:uuid)
end
end end
@@ -1,32 +0,0 @@
# frozen_string_literal: true
class SubmitFormDeclineController < ApplicationController
skip_before_action :authenticate_user!
skip_authorization_check
def create
submitter = Submitter.find_by!(slug: params[:submit_form_slug])
return redirect_to submit_form_path(submitter.slug) if submitter.declined_at? ||
submitter.completed_at? ||
submitter.submission.archived_at? ||
submitter.submission.expired? ||
submitter.submission.template&.archived_at?
ApplicationRecord.transaction do
submitter.update!(declined_at: Time.current)
SubmissionEvents.create_with_tracking_data(submitter, 'decline_form', request, { reason: params[:reason] })
end
user = submitter.submission.created_by_user || submitter.template.author
if user.user_configs.find_by(key: UserConfig::RECEIVE_DECLINED_EMAIL)&.value != false
SubmitterMailer.declined_email(submitter, user).deliver_later!
end
WebhookUrls.enqueue_events(submitter, 'form.declined')
redirect_to submit_form_path(submitter.slug)
end
end
@@ -1,40 +0,0 @@
# frozen_string_literal: true
class SubmitFormDownloadController < ApplicationController
skip_before_action :authenticate_user!
skip_authorization_check
FILES_TTL = 5.minutes
def index
@submitter = Submitter.find_by!(slug: params[:submit_form_slug])
return redirect_to submitter_download_index_path(@submitter.slug) if @submitter.completed_at?
return head :unprocessable_entity if @submitter.declined_at? ||
@submitter.submission.archived_at? ||
@submitter.submission.expired? ||
@submitter.submission.template&.archived_at? ||
AccountConfig.exists?(account_id: @submitter.account_id,
key: AccountConfig::ALLOW_TO_PARTIAL_DOWNLOAD_KEY,
value: false)
last_completed_submitter = @submitter.submission.submitters
.where.not(id: @submitter.id)
.where.not(completed_at: nil)
.max_by(&:completed_at)
attachments =
if last_completed_submitter
Submitters.select_attachments_for_download(last_completed_submitter)
else
@submitter.submission.schema_documents.preload(:blob)
end
urls = attachments.map do |attachment|
ActiveStorage::Blob.proxy_url(attachment.blob, expires_at: FILES_TTL.from_now.to_i)
end
render json: urls
end
end
@@ -12,7 +12,7 @@ class SubmitFormDrawSignatureController < ApplicationController
return redirect_to submit_form_completed_path(@submitter.slug) if @submitter.completed_at? 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?
return redirect_to submit_form_path(@submitter.slug) return redirect_to submit_form_path(@submitter.slug)
end end
@@ -1,60 +0,0 @@
# frozen_string_literal: true
class SubmitFormInviteController < ApplicationController
skip_before_action :authenticate_user!
skip_authorization_check
def create
submitter = Submitter.find_by!(slug: params[:submit_form_slug])
return head :unprocessable_entity unless can_invite?(submitter)
invite_submitters = filter_invite_submitters(submitter, 'invite_by_uuid')
optional_invite_submitters = filter_invite_submitters(submitter, 'optional_invite_by_uuid')
ApplicationRecord.transaction do
(invite_submitters + optional_invite_submitters).each do |item|
attrs = submitters_attributes.find { |e| e[:uuid] == item['uuid'] }
next unless attrs
next if attrs[:email].blank?
submitter.submission.submitters.create!(**attrs, account_id: submitter.account_id)
SubmissionEvents.create_with_tracking_data(submitter, 'invite_party', request, { uuid: submitter.uuid })
end
submitter.submission.update!(submitters_order: :preserved)
end
submitter.submission.submitters.reload
if invite_submitters.all? { |s| submitter.submission.submitters.any? { |e| e.uuid == s['uuid'] } }
Submitters::SubmitValues.call(submitter, ActionController::Parameters.new(completed: 'true'), request)
head :ok
else
head :unprocessable_entity
end
end
private
def can_invite?(submitter)
!submitter.declined_at? &&
!submitter.completed_at? &&
!submitter.submission.archived_at? &&
!submitter.submission.expired? &&
!submitter.submission.template&.archived_at?
end
def filter_invite_submitters(submitter, key = 'invite_by_uuid')
(submitter.submission.template_submitters || submitter.submission.template.submitters).select do |s|
s[key] == submitter.uuid && submitter.submission.submitters.none? { |e| e.uuid == s['uuid'] }
end
end
def submitters_attributes
params.require(:submission).permit(submitters: [%i[uuid email]]).fetch(:submitters, [])
end
end
@@ -7,10 +7,8 @@ class SubmitFormValuesController < ApplicationController
def index def index
submitter = Submitter.find_by!(slug: params[:submit_form_slug]) submitter = Submitter.find_by!(slug: params[:submit_form_slug])
return render json: {} if submitter.completed_at? || submitter.declined_at? return render json: {} if submitter.completed_at?
return render json: {} if submitter.submission.template&.archived_at? || return render json: {} if submitter.submission.template.archived_at? || submitter.submission.archived_at?
submitter.submission.archived_at? ||
submitter.submission.expired?
value = submitter.values[params['field_uuid']] value = submitter.values[params['field_uuid']]
attachment = submitter.attachments.where(created_at: params[:after]..).find_by(uuid: value) if value.present? attachment = submitter.attachments.where(created_at: params[:after]..).find_by(uuid: value) if value.present?
@@ -9,8 +9,7 @@ class SubmittersAutocompleteController < ApplicationController
def index def index
submitters = search_submitters(@submitters) submitters = search_submitters(@submitters)
arel_columns = SELECT_COLUMNS.map { |col| Submitter.arel_table[col] } values = submitters.limit(LIMIT).group(SELECT_COLUMNS.join(', ')).pluck(SELECT_COLUMNS.join(', '))
values = submitters.limit(LIMIT).group(arel_columns).pluck(arel_columns)
attrs = values.map { |row| SELECT_COLUMNS.zip(row).to_h } attrs = values.map { |row| SELECT_COLUMNS.zip(row).to_h }
attrs = attrs.uniq { |e| e[params[:field]] } if params[:field].present? attrs = attrs.uniq { |e| e[params[:field]] } if params[:field].present?
@@ -22,17 +21,13 @@ class SubmittersAutocompleteController < ApplicationController
def search_submitters(submitters) def search_submitters(submitters)
if SELECT_COLUMNS.include?(params[:field]) if SELECT_COLUMNS.include?(params[:field])
if Docuseal.fulltext_search? column = Submitter.arel_table[params[:field].to_sym]
Submitters.fulltext_search_field(current_user, submitters, params[:q], params[:field])
else
column = Submitter.arel_table[params[:field].to_sym]
term = "#{params[:q].downcase}%" term = "#{params[:q].downcase}%"
submitters.where(column.matches(term)) submitters.where(column.matches(term))
end
else else
Submitters.search(current_user, submitters, params[:q]) Submitters.search(submitters, params[:q])
end end
end end
end end
-82
View File
@@ -1,82 +0,0 @@
# frozen_string_literal: true
class SubmittersController < ApplicationController
load_and_authorize_resource :submitter, only: %i[edit update]
def edit
@submitter_email_message =
if @submitter.preferences['email_message_uuid'].present?
@submitter.account.email_messages.find_by(uuid: @submitter.preferences['email_message_uuid'])
end
end
def update
submission = @submitter.submission
if @submitter.submission_events.exists?(event_type: 'start_form') || submission.archived_at? || submission.expired?
return redirect_back fallback_location: submission_path(submission), alert: I18n.t('submitter_cannot_be_updated')
end
if submitter_params.values.all?(&:blank?)
return redirect_back fallback_location: submission_path(submission),
alert: I18n.t('at_least_one_field_must_be_filled')
end
if params[:is_custom_message] != '1'
params.delete(:subject)
params.delete(:body)
end
submitter_preferences = Submitters.normalize_preferences(@submitter.account, current_user, params)
if submitter_preferences.key?('email_message_uuid')
@submitter.preferences['email_message_uuid'] = submitter_preferences['email_message_uuid']
end
assign_submitter_attrs(@submitter, submitter_params)
if @submitter.save
maybe_resend_email_sms(@submitter, params)
SearchEntries.enqueue_reindex(@submitter)
redirect_back fallback_location: submission_path(submission), notice: I18n.t('changes_have_been_saved')
else
redirect_back fallback_location: submission_path(submission), alert: I18n.t('unable_to_save')
end
end
private
def maybe_resend_email_sms(submitter, params)
if params[:send_email] == '1' && submitter.email.present?
is_sent_recently = Docuseal.multitenant? &&
EmailEvent.exists?(email: submitter.email,
tag: 'submitter_invitation',
emailable: submitter,
event_type: 'send',
created_at: 4.hours.ago..Time.current)
SendSubmitterInvitationEmailJob.perform_async('submitter_id' => submitter.id) unless is_sent_recently
end
return if submitter.phone.blank?
return unless params[:send_sms] == '1'
SendSubmitterInvitationSmsJob.perform_async('submitter_id' => submitter.id)
end
def assign_submitter_attrs(submitter, attrs)
submitter.phone = attrs[:phone].to_s.gsub(/[^0-9+]/, '') if attrs.key?(:phone)
submitter.email = Submissions.normalize_email(attrs[:email]) if attrs.key?(:email)
submitter.name = attrs[:name] if attrs.key?(:name)
submitter
end
def submitter_params
params.require(:submitter).permit(:email, :name, :phone).transform_values(&:strip)
end
end
@@ -1,64 +0,0 @@
# frozen_string_literal: true
class SubmittersResubmitController < ApplicationController
load_and_authorize_resource :submitter, parent: false
def update
return redirect_to submit_form_path(slug: @submitter.slug) if @submitter.email != current_user.email
submission = @submitter.account.submissions.new(created_by_user: current_user,
submitters_order: :preserved,
**@submitter.submission.slice(:template_fields,
:account_id,
:name,
:template_id,
:template_schema,
:template_submitters,
:preferences))
@submitter.submission.submitters.each do |submitter|
new_submitter = submission.submitters.new(submitter.slice(:uuid, :email, :phone, :name,
:preferences, :metadata, :account_id))
next unless submitter.uuid == @submitter.uuid
assign_submitter_values(new_submitter, submitter)
@new_submitter ||= new_submitter
end
submission.save!
@submitter.submission.documents_attachments.each do |attachment|
submission.documents_attachments.create!(uuid: attachment.uuid, blob_id: attachment.blob_id)
end
redirect_to submit_form_path(slug: @new_submitter.slug)
end
private
def assign_submitter_values(new_submitter, submitter)
attachments_index = submitter.attachments.index_by(&:uuid)
submitter.submission.template_fields.each do |field|
next if field['submitter_uuid'] != submitter.uuid
next if field['default_value'] == '{{date}}'
next if field['type'] == 'stamp'
next if field['type'] == 'signature'
next if field.dig('preferences', 'formula').present?
value = submitter.values[field['uuid']]
next if value.blank?
if field['type'].in?(%w[image file initials])
Array.wrap(value).each do |attachment_uuid|
new_submitter.attachments << attachments_index[attachment_uuid].dup
end
end
new_submitter.values[field['uuid']] = value
end
end
end
@@ -6,11 +6,11 @@ class SubmittersSendEmailController < ApplicationController
def create def create
if Docuseal.multitenant? && SubmissionEvent.exists?(submitter: @submitter, if Docuseal.multitenant? && SubmissionEvent.exists?(submitter: @submitter,
event_type: 'send_email', event_type: 'send_email',
created_at: 10.hours.ago..Time.current) created_at: 24.hours.ago..Time.current)
Rollbar.warning("Already sent: #{@submitter.id}") if defined?(Rollbar) Rollbar.warning("Already sent: #{@submitter.id}") if defined?(Rollbar)
return redirect_back(fallback_location: submission_path(@submitter.submission), return redirect_back(fallback_location: submission_path(@submitter.submission),
alert: I18n.t('email_has_been_sent_already')) alert: 'Email has been sent already.')
end end
SendSubmitterInvitationEmailJob.perform_async('submitter_id' => @submitter.id) SendSubmitterInvitationEmailJob.perform_async('submitter_id' => @submitter.id)
@@ -18,6 +18,6 @@ class SubmittersSendEmailController < ApplicationController
@submitter.sent_at ||= Time.current @submitter.sent_at ||= Time.current
@submitter.save! @submitter.save!
redirect_back(fallback_location: submission_path(@submitter.submission), notice: I18n.t('email_has_been_sent')) redirect_back(fallback_location: submission_path(@submitter.submission), notice: 'Email has been sent')
end end
end end
@@ -5,7 +5,7 @@ class TemplateDocumentsController < ApplicationController
def create def create
if params[:blobs].blank? && params[:files].blank? if params[:blobs].blank? && params[:files].blank?
return render json: { error: I18n.t('file_is_missing') }, status: :unprocessable_entity return render json: { error: 'File is missing' }, status: :unprocessable_entity
end end
old_fields_hash = @template.fields.hash old_fields_hash = @template.fields.hash
@@ -28,6 +28,6 @@ class TemplateDocumentsController < ApplicationController
) )
} }
rescue Templates::CreateAttachments::PdfEncrypted rescue Templates::CreateAttachments::PdfEncrypted
render json: { error: 'PDF encrypted', status: 'pdf_encrypted' }, status: :unprocessable_entity render json: { error: 'PDF encrypted' }, status: :unprocessable_entity
end end
end end
@@ -6,9 +6,7 @@ class TemplateFoldersAutocompleteController < ApplicationController
LIMIT = 100 LIMIT = 100
def index def index
templates_query = Template.accessible_by(current_ability).where(archived_at: nil) template_folders = @template_folders.joins(:templates).where(templates: { archived_at: nil }).distinct
template_folders = @template_folders.where(id: templates_query.select(:folder_id))
template_folders = TemplateFolders.search(template_folders, params[:q]).limit(LIMIT) template_folders = TemplateFolders.search(template_folders, params[:q]).limit(LIMIT)
render json: template_folders.as_json(only: %i[name archived_at]) render json: template_folders.as_json(only: %i[name archived_at])
+5 -19
View File
@@ -3,15 +3,11 @@
class TemplateFoldersController < ApplicationController class TemplateFoldersController < ApplicationController
load_and_authorize_resource :template_folder load_and_authorize_resource :template_folder
helper_method :selected_order
def show def show
@templates = @template_folder.templates.active.accessible_by(current_ability) @templates = @template_folder.templates.active.preload(:author).order(id: :desc)
.preload(:author, :template_accesses) @templates = Templates.search(@templates, params[:q])
@templates = Templates.search(current_user, @templates, params[:q])
@templates = Templates::Order.call(@templates, current_user, selected_order)
@pagy, @templates = pagy_auto(@templates, limit: 12) @pagy, @templates = pagy(@templates, items: 12)
end end
def edit; end def edit; end
@@ -19,24 +15,14 @@ class TemplateFoldersController < ApplicationController
def update def update
if @template_folder != current_account.default_template_folder && if @template_folder != current_account.default_template_folder &&
@template_folder.update(template_folder_params) @template_folder.update(template_folder_params)
redirect_to folder_path(@template_folder), notice: I18n.t('folder_name_has_been_updated') redirect_to folder_path(@template_folder), notice: 'Folder name has been updated'
else else
redirect_to folder_path(@template_folder), alert: I18n.t('unable_to_rename_folder') redirect_to folder_path(@template_folder), alert: 'Unable to rename folder'
end end
end end
private private
def selected_order
@selected_order ||=
if cookies.permanent[:dashboard_templates_order].blank? ||
(cookies.permanent[:dashboard_templates_order] == 'used_at' && can?(:manage, :countless))
'created_at'
else
cookies.permanent[:dashboard_templates_order]
end
end
def template_folder_params def template_folder_params
params.require(:template_folder).permit(:name) params.require(:template_folder).permit(:name)
end end
@@ -4,9 +4,9 @@ class TemplatesArchivedController < ApplicationController
load_and_authorize_resource :template, parent: false load_and_authorize_resource :template, parent: false
def index def index
@templates = @templates.where.not(archived_at: nil).preload(:author, :folder, :template_accesses).order(id: :desc) @templates = @templates.where.not(archived_at: nil).preload(:author, :folder).order(id: :desc)
@templates = Templates.search(current_user, @templates, params[:q]) @templates = Templates.search(@templates, params[:q])
@pagy, @templates = pagy_auto(@templates, limit: 12) @pagy, @templates = pagy(@templates, items: 12)
end end
end end
@@ -6,16 +6,9 @@ class TemplatesArchivedSubmissionsController < ApplicationController
def index def index
@submissions = @submissions.where.not(archived_at: nil) @submissions = @submissions.where.not(archived_at: nil)
@submissions = Submissions.search(current_user, @submissions, params[:q], search_values: true) @submissions = Submissions.search(@submissions, params[:q], search_values: true)
@submissions = Submissions::Filter.call(@submissions, current_user, params)
@submissions = if params[:completed_at_from].present? || params[:completed_at_to].present? @pagy, @submissions = pagy(@submissions.preload(:submitters).order(id: :desc))
@submissions.order(Submitter.arel_table[:completed_at].maximum.desc)
else
@submissions.order(id: :desc)
end
@pagy, @submissions = pagy_auto(@submissions.preload(submitters: :start_form_submission_events))
rescue ActiveRecord::RecordNotFound rescue ActiveRecord::RecordNotFound
redirect_to root_path redirect_to root_path
end end
@@ -1,37 +0,0 @@
# frozen_string_literal: true
class TemplatesCloneAndReplaceController < ApplicationController
load_and_authorize_resource :template
def create
return head :unprocessable_entity if params[:files].blank?
ActiveRecord::Associations::Preloader.new(
records: [@template],
associations: [schema_documents: :preview_images_attachments]
).call
cloned_template = Templates::Clone.call(@template, author: current_user)
cloned_template.name = File.basename(params[:files].first.original_filename, '.*')
cloned_template.save!
documents = Templates::ReplaceAttachments.call(cloned_template, params, extract_fields: true)
cloned_template.save!
Templates::CloneAttachments.call(template: cloned_template, original_template: @template,
excluded_attachment_uuids: documents.map(&:uuid))
SearchEntries.enqueue_reindex(cloned_template)
respond_to do |f|
f.html { redirect_to edit_template_path(cloned_template) }
f.json { render json: { id: cloned_template.id } }
end
rescue Templates::CreateAttachments::PdfEncrypted
respond_to do |f|
f.html { render turbo_stream: turbo_stream.append(params[:form_id], html: helpers.tag.prompt_password) }
f.json { render json: { error: 'PDF encrypted', status: 'pdf_encrypted' }, status: :unprocessable_entity }
end
end
end
+16 -31
View File
@@ -8,26 +8,20 @@ class TemplatesController < ApplicationController
def show def show
submissions = @template.submissions.accessible_by(current_ability) submissions = @template.submissions.accessible_by(current_ability)
submissions = submissions.active if @template.archived_at.blank? submissions = submissions.active if @template.archived_at.blank?
submissions = Submissions.search(current_user, submissions, params[:q], search_values: true) submissions = Submissions.search(submissions, params[:q], search_values: true)
submissions = Submissions::Filter.call(submissions, current_user, params.except(:status))
@base_submissions = submissions @base_submissions = submissions
submissions = Submissions::Filter.filter_by_status(submissions, params) submissions = submissions.pending if params[:status] == 'pending'
submissions = submissions.completed if params[:status] == 'completed'
submissions = if params[:completed_at_from].present? || params[:completed_at_to].present? @pagy, @submissions = pagy(submissions.preload(:submitters).order(id: :desc))
submissions.order(Submitter.arel_table[:completed_at].maximum.desc)
else
submissions.order(id: :desc)
end
@pagy, @submissions = pagy_auto(submissions.preload(:template_accesses, submitters: :start_form_submission_events))
rescue ActiveRecord::RecordNotFound rescue ActiveRecord::RecordNotFound
redirect_to root_path redirect_to root_path
end end
def new def new
@template.name = "#{@base_template.name} (#{I18n.t('clone')})" if @base_template @template.name = "#{@base_template.name} (Clone)" if @base_template
end end
def edit def edit
@@ -72,9 +66,7 @@ class TemplatesController < ApplicationController
if @template.save if @template.save
Templates::CloneAttachments.call(template: @template, original_template: @base_template) if @base_template Templates::CloneAttachments.call(template: @template, original_template: @base_template) if @base_template
SearchEntries.enqueue_reindex(@template) SendTemplateUpdatedWebhookRequestJob.perform_async('template_id' => @template.id)
WebhookUrls.enqueue_events(@template, 'template.created')
maybe_redirect_to_template(@template) maybe_redirect_to_template(@template)
else else
@@ -83,29 +75,23 @@ class TemplatesController < ApplicationController
end end
def update def update
@template.assign_attributes(template_params) @template.update!(template_params)
is_name_changed = @template.name_changed? SendTemplateUpdatedWebhookRequestJob.perform_async('template_id' => @template.id)
@template.save!
SearchEntries.enqueue_reindex(@template) if is_name_changed
WebhookUrls.enqueue_events(@template, 'template.updated')
head :ok head :ok
end end
def destroy def destroy
notice = notice =
if params[:permanently].in?(['true', true]) if params[:permanently].present?
@template.destroy! @template.destroy!
I18n.t('template_has_been_removed') 'Template has been removed.'
else else
@template.update!(archived_at: Time.current) @template.update!(archived_at: Time.current)
I18n.t('template_has_been_archived') 'Template has been archived.'
end end
redirect_back(fallback_location: root_path, notice:) redirect_back(fallback_location: root_path, notice:)
@@ -116,13 +102,13 @@ class TemplatesController < ApplicationController
def template_params def template_params
params.require(:template).permit( params.require(:template).permit(
:name, :name,
{ schema: [[:attachment_uuid, :name, { conditions: [%i[field_uuid value action operation]] }]], { schema: [%i[attachment_uuid name]],
submitters: [%i[name uuid is_requester linked_to_uuid invite_by_uuid optional_invite_by_uuid email]], submitters: [%i[name uuid]],
fields: [[:uuid, :submitter_uuid, :name, :type, fields: [[:uuid, :submitter_uuid, :name, :type,
:required, :readonly, :default_value, :required, :readonly, :default_value,
:title, :description, :title, :description,
{ preferences: {}, { preferences: {},
conditions: [%i[field_uuid value action operation]], conditions: [%i[field_uuid value action]],
options: [%i[value uuid]], options: [%i[value uuid]],
validation: %i[message pattern], validation: %i[message pattern],
areas: [%i[x y w h cell_w attachment_uuid option_uuid page]] }]] } areas: [%i[x y w h cell_w attachment_uuid option_uuid page]] }]] }
@@ -130,15 +116,14 @@ class TemplatesController < ApplicationController
end end
def authorized_clone_account_id?(account_id) def authorized_clone_account_id?(account_id)
true_user.account_id.to_s == account_id.to_s || true_user.account_id.to_s == account_id.to_s || true_user.account.linked_accounts.exists?(id: account_id)
true_user.account.linked_accounts.accessible_by(current_ability).exists?(id: account_id)
end end
def maybe_redirect_to_template(template) def maybe_redirect_to_template(template)
if template.account == current_account if template.account == current_account
redirect_to(edit_template_path(@template)) redirect_to(edit_template_path(@template))
else else
redirect_back(fallback_location: root_path, notice: I18n.t('template_has_been_cloned')) redirect_back(fallback_location: root_path, notice: 'Template has been clonned')
end end
end end
@@ -8,17 +8,12 @@ class TemplatesDashboardController < ApplicationController
TEMPLATES_PER_PAGE = 12 TEMPLATES_PER_PAGE = 12
FOLDERS_PER_PAGE = 18 FOLDERS_PER_PAGE = 18
helper_method :selected_order
def index def index
@template_folders = @template_folders.where(id: @templates.active.select(:folder_id)) @template_folders = filter_template_folders(@template_folders)
@template_folders = TemplateFolders.search(@template_folders, params[:q])
@template_folders = sort_template_folders(@template_folders, current_user, selected_order)
@pagy, @template_folders = pagy( @pagy, @template_folders = pagy(
@template_folders, @template_folders,
limit: FOLDERS_PER_PAGE, items: FOLDERS_PER_PAGE,
page: @template_folders.count > SHOW_TEMPLATES_FOLDERS_THRESHOLD ? params[:page] : 1 page: @template_folders.count > SHOW_TEMPLATES_FOLDERS_THRESHOLD ? params[:page] : 1
) )
@@ -26,104 +21,43 @@ class TemplatesDashboardController < ApplicationController
@templates = @templates.none @templates = @templates.none
else else
@template_folders = @template_folders.reject { |e| e.name == TemplateFolder::DEFAULT_NAME } @template_folders = @template_folders.reject { |e| e.name == TemplateFolder::DEFAULT_NAME }
@templates = filter_templates(@templates).preload(:author, :template_accesses) @templates = filter_templates(@templates)
@templates = Templates::Order.call(@templates, current_user, selected_order)
limit = items =
if @template_folders.size < 4 if @template_folders.size < 4
TEMPLATES_PER_PAGE TEMPLATES_PER_PAGE
else else
(@template_folders.size < 7 ? 9 : 6) (@template_folders.size < 7 ? 9 : 6)
end end
@pagy, @templates = pagy_auto(@templates, limit:) @pagy, @templates = pagy(@templates, items:)
load_related_submissions if params[:q].present? && @templates.blank?
end end
end end
private private
def filter_template_folders(template_folders)
rel = template_folders.joins(:active_templates)
.order(id: :desc)
.distinct
TemplateFolders.search(rel, params[:q])
end
def filter_templates(templates) def filter_templates(templates)
rel = templates.active rel = templates.active.preload(:author).order(id: :desc)
if params[:q].blank? if params[:q].blank?
if Docuseal.multitenant? ? current_account.testing? : current_account.linked_account_account if Docuseal.multitenant? && !current_account.testing?
shared_account_ids = [current_user.account_id]
shared_account_ids << TemplateSharing::ALL_ID if !Docuseal.multitenant? && !current_account.testing?
shared_template_ids = TemplateSharing.where(account_id: shared_account_ids).select(:template_id)
rel = Template.where(
Template.arel_table[:id].in(
rel.where(folder_id: current_account.default_template_folder.id).select(:id).arel
.union(:all, shared_template_ids.arel)
)
)
else
rel = rel.where(folder_id: current_account.default_template_folder.id) rel = rel.where(folder_id: current_account.default_template_folder.id)
end
end
Templates.search(current_user, rel, params[:q])
end
def sort_template_folders(template_folders, current_user, order)
case order
when 'used_at'
subquery =
Template.left_joins(:submissions)
.group(:folder_id)
.where(account_id: current_user.account_id)
.select(
:folder_id,
Template.arel_table[:updated_at].maximum.as('updated_at_max'),
Submission.arel_table[:created_at].maximum.as('submission_created_at_max')
)
template_folders = template_folders.joins(
Template.arel_table
.join(subquery.arel.as('templates'), Arel::Nodes::OuterJoin)
.on(TemplateFolder.arel_table[:id].eq(Template.arel_table[:folder_id]))
.join_sources
)
template_folders.order(
Arel::Nodes::Case.new
.when(Template.arel_table[:submission_created_at_max].gt(Template.arel_table[:updated_at_max]))
.then(Template.arel_table[:submission_created_at_max])
.else(Template.arel_table[:updated_at_max])
.desc
)
when 'name'
template_folders.order(name: :asc)
else
template_folders.order(id: :desc)
end
end
def selected_order
@selected_order ||=
if cookies.permanent[:dashboard_templates_order].blank? ||
(cookies.permanent[:dashboard_templates_order] == 'used_at' && can?(:manage, :countless))
'created_at'
else else
cookies.permanent[:dashboard_templates_order] shared_template_ids =
TemplateSharing.where(account_id: [current_account.id, TemplateSharing::ALL_ID]).select(:template_id)
rel = rel.where(folder_id: current_account.default_template_folder.id).or(rel.where(id: shared_template_ids))
end end
end end
def load_related_submissions Templates.search(rel, params[:q])
@related_submissions = Submission.accessible_by(current_ability)
.left_joins(:template)
.where(archived_at: nil)
.where(templates: { archived_at: nil })
.preload(:template_accesses, :created_by_user,
template: :author,
submitters: :start_form_submission_events)
@related_submissions = Submissions.search(current_user, @related_submissions, params[:q])
.order(id: :desc)
@related_submissions_pagy, @related_submissions = pagy_auto(@related_submissions, limit: 5)
end end
end end
@@ -6,10 +6,9 @@ class TemplatesDebugController < ApplicationController
def show def show
attachment = @template.documents.first attachment = @template.documents.first
data = attachment.download pdf = HexaPDF::Document.new(io: StringIO.new(attachment.download))
pdf = HexaPDF::Document.new(io: StringIO.new(data))
fields = Templates::FindAcroFields.call(pdf, attachment, data) fields = Templates::FindAcroFields.call(pdf, attachment)
attachment.metadata['pdf'] ||= {} attachment.metadata['pdf'] ||= {}
attachment.metadata['pdf']['fields'] = fields attachment.metadata['pdf']['fields'] = fields
@@ -9,9 +9,9 @@ class TemplatesFoldersController < ApplicationController
@template.folder = TemplateFolders.find_or_create_by_name(current_user, params[:name]) @template.folder = TemplateFolders.find_or_create_by_name(current_user, params[:name])
if @template.save if @template.save
redirect_back(fallback_location: template_path(@template), notice: I18n.t('document_template_has_been_moved')) redirect_back(fallback_location: template_path(@template), notice: 'Document template has been moved')
else else
redirect_back(fallback_location: template_path(@template), notice: I18n.t('unable_to_move_template_into_folder')) redirect_back(fallback_location: template_path(@template), notice: 'Unable to move template into folder')
end end
end end
@@ -13,7 +13,7 @@ class TemplatesFormPreviewController < ApplicationController
@submitter.submission.submitters = @template.submitters.map { |item| Submitter.new(uuid: item['uuid']) } @submitter.submission.submitters = @template.submitters.map { |item| Submitter.new(uuid: item['uuid']) }
Submissions.preload_with_pages(@submitter.submission) Submitters.preload_with_pages(@submitter)
@attachments_index = ActiveStorage::Attachment.where(record: @submitter.submission.submitters, name: :attachments) @attachments_index = ActiveStorage::Attachment.where(record: @submitter.submission.submitters, name: :attachments)
.preload(:blob).index_by(&:uuid) .preload(:blob).index_by(&:uuid)
@@ -9,7 +9,7 @@ class TemplatesPreferencesController < ApplicationController
authorize!(:update, @template) authorize!(:update, @template)
@template.preferences = @template.preferences.merge(template_params[:preferences]) @template.preferences = @template.preferences.merge(template_params[:preferences])
@template.preferences = @template.preferences.reject { |_, v| (v.is_a?(String) || v.is_a?(Hash)) && v.blank? } @template.preferences = @template.preferences.reject { |_, v| v.is_a?(String) && v.blank? }
@template.save! @template.save!
head :ok head :ok
@@ -22,29 +22,13 @@ class TemplatesPreferencesController < ApplicationController
preferences: %i[bcc_completed request_email_subject request_email_body preferences: %i[bcc_completed request_email_subject request_email_body
documents_copy_email_subject documents_copy_email_body documents_copy_email_subject documents_copy_email_body
documents_copy_email_enabled documents_copy_email_attach_audit documents_copy_email_enabled documents_copy_email_attach_audit
documents_copy_email_attach_documents documents_copy_email_reply_to
completed_notification_email_attach_documents completed_notification_email_attach_documents
completed_redirect_url validate_unique_submitters
require_all_submitters submitters_order require_phone_2fa
default_expire_at_duration shared_link_2fa
default_expire_at
completed_notification_email_subject completed_notification_email_body completed_notification_email_subject completed_notification_email_body
completed_notification_email_enabled completed_notification_email_attach_audit] + completed_notification_email_enabled completed_notification_email_attach_audit]
[completed_message: %i[title body],
submitters: [%i[uuid request_email_subject request_email_body]], link_form_fields: []]
).tap do |attrs| ).tap do |attrs|
attrs[:preferences].delete(:submitters) if params[:request_email_per_submitter] != '1'
if (default_expire_at = attrs.dig(:preferences, :default_expire_at).presence)
attrs[:preferences][:default_expire_at] =
(ActiveSupport::TimeZone[current_account.timezone] || Time.zone).parse(default_expire_at).utc
end
attrs[:preferences] = attrs[:preferences].transform_values do |value| attrs[:preferences] = attrs[:preferences].transform_values do |value|
if %w[true false].include?(value) if %w[true false].include?(value)
value == 'true' value == 'true'
elsif value.respond_to?(:compact_blank)
value.compact_blank
else else
value value
end end
@@ -1,63 +0,0 @@
# frozen_string_literal: true
class TemplatesRecipientsController < ApplicationController
load_and_authorize_resource :template
def create
authorize!(:update, @template)
@template.submitters =
submitters_params.map { |s| s.reject { |_, v| v.is_a?(String) && v.blank? } }
@template.save!
render json: { submitters: @template.submitters }
end
private
def submitters_params
permit_params = { submitters: [%i[name uuid is_requester optional_invite_by_uuid
invite_by_uuid linked_to_uuid email option]] }
params.require(:template).permit(permit_params).fetch(:submitters, {}).values.filter_map do |s|
next if s[:uuid].blank?
if s[:is_requester] == '1' && s[:invite_by_uuid].blank? && s[:optional_invite_by_uuid].blank?
s[:is_requester] = true
else
s.delete(:is_requester)
end
s.delete(:invite_by_uuid) if s[:invite_by_uuid].blank?
s.delete(:optional_invite_by_uuid) if s[:optional_invite_by_uuid].blank?
normalize_option_value(s)
end
end
def normalize_option_value(attrs)
option = attrs.delete(:option)
if option.present?
case option
when 'is_requester'
attrs[:is_requester] = true
when 'not_set'
attrs.delete(:is_requester)
attrs.delete(:email)
attrs.delete(:linked_to_uuid)
attrs.delete(:invite_by_uuid)
attrs.delete(:optional_invite_by_uuid)
when /\Alinked_to_(.*)\z/
attrs[:linked_to_uuid] = ::Regexp.last_match(-1)
when /\Aoptional_invite_by_(.*)\z/
attrs[:optional_invite_by_uuid] = ::Regexp.last_match(-1)
when /\Ainvite_by_(.*)\z/
attrs[:invite_by_uuid] = ::Regexp.last_match(-1)
end
end
attrs
end
end
@@ -6,6 +6,6 @@ class TemplatesRestoreController < ApplicationController
def create def create
@template.update!(archived_at: nil) @template.update!(archived_at: nil)
redirect_to template_path(@template), notice: I18n.t('template_has_been_unarchived') redirect_to template_path(@template), notice: 'Template has been unarchived'
end end
end end
@@ -1,25 +0,0 @@
# frozen_string_literal: true
class TemplatesShareLinkController < ApplicationController
load_and_authorize_resource :template
def show; end
def create
authorize!(:update, @template)
@template.update!(template_params)
if params[:redir].present?
redirect_to params[:redir]
else
head :ok
end
end
private
def template_params
params.require(:template).permit(:shared_link)
end
end
@@ -23,9 +23,7 @@ class TemplatesUploadsController < ApplicationController
@template.update!(schema:) @template.update!(schema:)
WebhookUrls.enqueue_events(@template, 'template.created') SendTemplateCreatedWebhookRequestJob.perform_async('template_id' => @template.id)
SearchEntries.enqueue_reindex(@template)
redirect_to edit_template_path(@template) redirect_to edit_template_path(@template)
rescue Templates::CreateAttachments::PdfEncrypted rescue Templates::CreateAttachments::PdfEncrypted
@@ -35,7 +33,7 @@ class TemplatesUploadsController < ApplicationController
raise if Rails.env.local? raise if Rails.env.local?
redirect_to root_path, alert: I18n.t('unable_to_update_file') redirect_to root_path, alert: 'Unable to upload file'
end end
private private
@@ -54,18 +52,23 @@ class TemplatesUploadsController < ApplicationController
def create_file_params_from_url def create_file_params_from_url
tempfile = Tempfile.new tempfile = Tempfile.new
tempfile.binmode tempfile.binmode
tempfile.write(DownloadUtils.call(params[:url]).body) tempfile.write(conn.get(Addressable::URI.parse(params[:url]).display_uri.to_s).body)
tempfile.rewind tempfile.rewind
filename = URI.decode_www_form_component(params[:filename]) if params[:filename].present?
filename ||= File.basename(URI.decode_www_form_component(params[:url]))
file = ActionDispatch::Http::UploadedFile.new( file = ActionDispatch::Http::UploadedFile.new(
tempfile:, tempfile:,
filename:, filename: File.basename(
URI.decode_www_form_component(params[:filename].presence || params[:url]), '.*'
),
type: Marcel::MimeType.for(tempfile) type: Marcel::MimeType.for(tempfile)
) )
{ files: [file] } { files: [file] }
end end
def conn
Faraday.new do |faraday|
faraday.response :follow_redirects
end
end
end end
@@ -4,8 +4,9 @@ class TestingApiSettingsController < ApplicationController
def index def index
authorize!(:manage, current_user.access_token) authorize!(:manage, current_user.access_token)
@webhook_url = current_account.webhook_urls.first_or_initialize @webhook_config =
current_account.encrypted_configs.find_or_initialize_by(key: EncryptedConfig::WEBHOOK_URL_KEY)
authorize!(:manage, @webhook_url) authorize!(:manage, @webhook_config)
end end
end end
@@ -14,12 +14,12 @@ class TimestampServerController < ApplicationController
test_timeserver_url(@encrypted_config.value) if @encrypted_config.value.present? test_timeserver_url(@encrypted_config.value) if @encrypted_config.value.present?
if @encrypted_config.value.present? ? @encrypted_config.save : @encrypted_config.delete if @encrypted_config.value.present? ? @encrypted_config.save : @encrypted_config.delete
redirect_back fallback_location: settings_notifications_path, notice: I18n.t('changes_have_been_saved') redirect_back fallback_location: settings_notifications_path, notice: 'Changes have been saved'
else else
redirect_back fallback_location: settings_notifications_path, alert: I18n.t('unable_to_save') redirect_back fallback_location: settings_notifications_path, alert: 'Unable to save'
end end
rescue SocketError, TimestampError, OpenSSL::Timestamp::TimestampError rescue SocketError, TimestampError, OpenSSL::Timestamp::TimestampError
redirect_back fallback_location: settings_notifications_path, alert: t('invalid_timeserver') redirect_back fallback_location: settings_notifications_path, alert: 'Invalid Timeserver'
end end
private private
+2 -4
View File
@@ -5,8 +5,7 @@ class UserConfigsController < ApplicationController
authorize_resource :user_config authorize_resource :user_config
ALLOWED_KEYS = [ ALLOWED_KEYS = [
UserConfig::RECEIVE_COMPLETED_EMAIL, UserConfig::RECEIVE_COMPLETED_EMAIL
UserConfig::SHOW_APP_TOUR
].freeze ].freeze
InvalidKey = Class.new(StandardError) InvalidKey = Class.new(StandardError)
@@ -27,9 +26,8 @@ class UserConfigsController < ApplicationController
end end
def user_config_params def user_config_params
params.required(:user_config).permit(:key, :value, { value: {} }, { value: [] }).tap do |attrs| params.required(:user_config).permit!.tap do |attrs|
attrs[:value] = attrs[:value] == '1' if attrs[:value].in?(%w[1 0]) attrs[:value] = attrs[:value] == '1' if attrs[:value].in?(%w[1 0])
attrs[:value] = attrs[:value] == 'true' if attrs[:value].in?(%w[true false])
end end
end end
end end
+4 -4
View File
@@ -9,7 +9,7 @@ class UserInitialsController < ApplicationController
def update def update
file = params[:file] file = params[:file]
return redirect_to settings_profile_index_path, notice: I18n.t('unable_to_save_initials') if file.blank? return redirect_to settings_profile_index_path, notice: 'Unable to save initials' if file.blank?
blob = ActiveStorage::Blob.create_and_upload!(io: file.open, blob = ActiveStorage::Blob.create_and_upload!(io: file.open,
filename: file.original_filename, filename: file.original_filename,
@@ -22,16 +22,16 @@ class UserInitialsController < ApplicationController
) )
if @user_config.update(value: attachment.uuid) if @user_config.update(value: attachment.uuid)
redirect_to settings_profile_index_path, notice: I18n.t('initials_has_been_saved') redirect_to settings_profile_index_path, notice: 'Initials has been saved'
else else
redirect_to settings_profile_index_path, notice: I18n.t('unable_to_save_initials') redirect_to settings_profile_index_path, notice: 'Unable to save initials'
end end
end end
def destroy def destroy
@user_config.destroy @user_config.destroy
redirect_to settings_profile_index_path, notice: I18n.t('initials_has_been_removed') redirect_to settings_profile_index_path, notice: 'Initials has been removed'
end end
private private
@@ -9,7 +9,7 @@ class UserSignaturesController < ApplicationController
def update def update
file = params[:file] file = params[:file]
return redirect_to settings_profile_index_path, notice: I18n.t('unable_to_save_signature') if file.blank? return redirect_to settings_profile_index_path, notice: 'Unable to save signature' if file.blank?
blob = ActiveStorage::Blob.create_and_upload!(io: file.open, blob = ActiveStorage::Blob.create_and_upload!(io: file.open,
filename: file.original_filename, filename: file.original_filename,
@@ -22,16 +22,16 @@ class UserSignaturesController < ApplicationController
) )
if @user_config.update(value: attachment.uuid) if @user_config.update(value: attachment.uuid)
redirect_to settings_profile_index_path, notice: I18n.t('signature_has_been_saved') redirect_to settings_profile_index_path, notice: 'Signature has been saved'
else else
redirect_to settings_profile_index_path, notice: I18n.t('unable_to_save_signature') redirect_to settings_profile_index_path, notice: 'Unable to save signature'
end end
end end
def destroy def destroy
@user_config.destroy @user_config.destroy
redirect_to settings_profile_index_path, notice: I18n.t('signature_has_been_removed') redirect_to settings_profile_index_path, notice: 'Signature has been removed'
end end
private private
+28 -23
View File
@@ -9,14 +9,12 @@ class UsersController < ApplicationController
def index def index
@users = @users =
if params[:status] == 'archived' if params[:status] == 'archived'
@users.archived.where.not(role: 'integration') @users.archived
elsif params[:status] == 'integration'
@users.active.where(role: 'integration')
else else
@users.active.where.not(role: 'integration') @users.active
end end
@pagy, @users = pagy(@users.preload(account: :account_accesses).where(account: current_account).order(id: :desc)) @pagy, @users = pagy(@users.where(account: current_account).order(id: :desc))
end end
def new; end def new; end
@@ -24,38 +22,41 @@ class UsersController < ApplicationController
def edit; end def edit; end
def create def create
if User.accessible_by(current_ability).exists?(email: @user.email) existing_user = User.accessible_by(current_ability).find_by(email: @user.email)
@user.errors.add(:email, I18n.t('already_exists'))
return render turbo_stream: turbo_stream.replace(:modal, template: 'users/new'), status: :unprocessable_entity if existing_user
existing_user.archived_at = nil
existing_user.assign_attributes(user_params)
existing_user.account = current_account
@user = existing_user
end end
@user.role = User::ADMIN_ROLE unless role_valid?(@user.role)
if @user.save if @user.save
UserMailer.invitation_email(@user).deliver_later! UserMailer.invitation_email(@user).deliver_later!
redirect_back fallback_location: settings_users_path, notice: I18n.t('user_has_been_invited') redirect_back fallback_location: settings_users_path, notice: 'User has been invited'
else else
render turbo_stream: turbo_stream.replace(:modal, template: 'users/new'), status: :unprocessable_entity render turbo_stream: turbo_stream.replace(:modal, template: 'users/new'), status: :unprocessable_entity
end end
end end
def update def update
return redirect_to settings_users_path, notice: I18n.t('unable_to_update_user') if Docuseal.demo? return redirect_to settings_users_path, notice: 'Unable to update user.' if Docuseal.demo?
attrs = user_params.compact_blank.merge(user_params.slice(:archived_at)) attrs = user_params.compact_blank.merge(user_params.slice(:archived_at))
attrs.delete(:role) if !role_valid?(attrs[:role]) || current_user == @user
if params.dig(:user, :account_id).present? if params.dig(:user, :account_id).present?
account = Account.accessible_by(current_ability).find(params.dig(:user, :account_id)) account = Account.accessible_by(current_ability).find(params[:user][:account_id])
authorize!(:manage, account) authorize!(:manage, account)
@user.account = account @user.account = account
end end
if @user.update(attrs.except(current_user == @user ? :role : nil)) if @user.update(attrs)
redirect_back fallback_location: settings_users_path, notice: I18n.t('user_has_been_updated') redirect_back fallback_location: settings_users_path, notice: 'User has been updated'
else else
render turbo_stream: turbo_stream.replace(:modal, template: 'users/edit'), status: :unprocessable_entity render turbo_stream: turbo_stream.replace(:modal, template: 'users/edit'), status: :unprocessable_entity
end end
@@ -63,12 +64,12 @@ class UsersController < ApplicationController
def destroy def destroy
if Docuseal.demo? || @user.id == current_user.id if Docuseal.demo? || @user.id == current_user.id
return redirect_to settings_users_path, notice: I18n.t('unable_to_remove_user') return redirect_to settings_users_path, notice: 'Unable to remove user'
end end
@user.update!(archived_at: Time.current) @user.update!(archived_at: Time.current)
redirect_back fallback_location: settings_users_path, notice: I18n.t('user_has_been_removed') redirect_back fallback_location: settings_users_path, notice: 'User has been removed'
end end
private private
@@ -78,16 +79,20 @@ class UsersController < ApplicationController
end end
def build_user def build_user
@user = current_account.users.new(user_params) @user = current_account.users.find_by(email: user_params[:email])&.tap do |user|
user.assign_attributes(user_params)
user.archived_at = nil
end
@user ||= current_account.users.new(user_params)
@user
end end
def user_params def user_params
if params.key?(:user) if params.key?(:user)
permitted_params = %i[email first_name last_name password archived_at] params.require(:user).permit(:email, :first_name, :last_name, :password,
:role, :archived_at, :account_id)
permitted_params << :role if role_valid?(params.dig(:user, :role))
params.require(:user).permit(permitted_params)
else else
{} {}
end end
@@ -14,6 +14,6 @@ class VerifyPdfSignatureController < ApplicationController
render turbo_stream: turbo_stream.replace('result', partial: 'result', render turbo_stream: turbo_stream.replace('result', partial: 'result',
locals: { pdfs:, files: params[:files], trusted_certs: }) locals: { pdfs:, files: params[:files], trusted_certs: })
rescue HexaPDF::MalformedPDFError rescue HexaPDF::MalformedPDFError
render turbo_stream: turbo_stream.replace('result', html: helpers.tag.div(I18n.t('invalid_pdf'), id: 'result')) render turbo_stream: turbo_stream.replace('result', html: helpers.tag.div('Invalid PDF', id: 'result'))
end end
end end
@@ -1,66 +0,0 @@
# frozen_string_literal: true
class WebhookEventsController < ApplicationController
load_and_authorize_resource :webhook_url, parent: false, id_param: :webhook_id
before_action :load_webhook_event
def show
return unless current_ability.can?(:read, @webhook_event.record)
@data =
case @webhook_event.event_type
when 'form.started', 'form.completed', 'form.declined', 'form.viewed'
Submitters::SerializeForWebhook.call(@webhook_event.record)
when 'submission.created', 'submission.completed', 'submission.expired'
Submissions::SerializeForApi.call(@webhook_event.record)
when 'template.created', 'template.updated'
Templates::SerializeForApi.call(@webhook_event.record)
when 'submission.archived'
@webhook_event.record.as_json(only: %i[id archived_at])
end
end
def resend
id_key = WebhookUrls::EVENT_TYPE_ID_KEYS.fetch(@webhook_event.event_type.split('.').first)
last_attempt_id = @webhook_event.webhook_attempts.maximum(:id)
WebhookUrls::EVENT_TYPE_TO_JOB_CLASS[@webhook_event.event_type].perform_async(
id_key => @webhook_event.record_id,
'webhook_url_id' => @webhook_event.webhook_url_id,
'event_uuid' => @webhook_event.uuid,
'attempt' => SendWebhookRequest::MANUAL_ATTEMPT,
'last_status' => 0
)
render turbo_stream: [
turbo_stream.after(
params[:button_id],
helpers.tag.submit_form(
helpers.button_to('', refresh_settings_webhook_event_path(@webhook_url.id, @webhook_event.uuid),
params: { last_attempt_id: }),
class: 'hidden', data: { interval: 3_000 }
)
)
]
end
def refresh
return head :ok if @webhook_event.webhook_attempts.maximum(:id) == params[:last_attempt_id].to_i
render turbo_stream: [
turbo_stream.replace(helpers.dom_id(@webhook_event),
partial: 'event_row',
locals: { with_status: true, webhook_url: @webhook_url, webhook_event: @webhook_event }),
turbo_stream.replace(helpers.dom_id(@webhook_event, :drawer_events),
partial: 'drawer_events',
locals: { webhook_url: @webhook_url, webhook_event: @webhook_event })
]
end
private
def load_webhook_event
@webhook_event = @webhook_url.webhook_events.find_by!(uuid: params[:id])
end
end
@@ -1,22 +1,36 @@
# frozen_string_literal: true # frozen_string_literal: true
class WebhookPreferencesController < ApplicationController class WebhookPreferencesController < ApplicationController
load_and_authorize_resource :webhook_url, parent: false EVENTS = %w[
form.viewed
form.started
form.completed
template.created
template.updated
submission.created
submission.archived
].freeze
def update before_action :load_account_config
webhook_preferences_params[:events].each do |event, val| authorize_resource :account_config, parent: false
@webhook_url.events.delete(event) if val == '0'
@webhook_url.events.push(event) if val == '1' && @webhook_url.events.exclude?(event)
end
@webhook_url.save! def create
@account_config.value[account_config_params[:event]] = account_config_params[:value] == '1'
@account_config.save!
head :ok head :ok
end end
private private
def webhook_preferences_params def load_account_config
params.require(:webhook_url).permit(events: {}) @account_config =
current_account.account_configs.find_or_initialize_by(key: AccountConfig::WEBHOOK_PREFERENCES_KEY)
@account_config.value ||= {}
end
def account_config_params
params.permit(:event, :value)
end end
end end
+16 -9
View File
@@ -1,22 +1,29 @@
# frozen_string_literal: true # frozen_string_literal: true
class WebhookSecretController < ApplicationController class WebhookSecretController < ApplicationController
load_and_authorize_resource :webhook_url, parent: false before_action :load_encrypted_config
authorize_resource :encrypted_config, parent: false
def show; end def index; end
def update def create
@webhook_url.update!(secret: { @encrypted_config.assign_attributes(value: {
webhook_secret_params[:key] => webhook_secret_params[:value] encrypted_config_params[:key] => encrypted_config_params[:value]
}.compact_blank) }.compact_blank)
redirect_back(fallback_location: settings_webhook_path(@webhook_url), @encrypted_config.value.present? ? @encrypted_config.save! : @encrypted_config.delete
notice: I18n.t('webhook_secret_has_been_saved'))
redirect_back(fallback_location: settings_webhooks_path, notice: 'Webhook Secret has been saved.')
end end
private private
def webhook_secret_params def load_encrypted_config
params.require(:webhook_url).permit(secret: %i[key value]).fetch(:secret, {}) @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
end end
+15 -60
View File
@@ -1,81 +1,36 @@
# frozen_string_literal: true # frozen_string_literal: true
class WebhookSettingsController < ApplicationController class WebhookSettingsController < ApplicationController
load_and_authorize_resource :webhook_url, parent: false, only: %i[index show new create update destroy] before_action :load_encrypted_config
load_and_authorize_resource :webhook_url, only: %i[resend], id_param: :webhook_id authorize_resource :encrypted_config, parent: false
def index def show; end
@webhook_urls = @webhook_urls.order(id: :desc)
@webhook_url = @webhook_urls.first_or_initialize
if @webhook_urls.size > 1
render :index
else
@webhook_events = @webhook_url.webhook_events
@webhook_events = @webhook_events.where(status: params[:status]) if %w[success error].include?(params[:status])
@pagy, @webhook_events = pagy_countless(@webhook_events.order(id: :desc))
render :show
end
end
def show
@webhook_events = @webhook_url.webhook_events
@webhook_events = @webhook_events.where(status: params[:status]) if %w[success error].include?(params[:status])
@pagy, @webhook_events = pagy_countless(@webhook_events.order(id: :desc))
end
def new; end
def create def create
@webhook_url.save! @encrypted_config.assign_attributes(encrypted_config_params)
redirect_to settings_webhooks_path, notice: I18n.t('webhook_url_has_been_saved') @encrypted_config.value.present? ? @encrypted_config.save! : @encrypted_config.delete
redirect_back(fallback_location: settings_webhooks_path, notice: 'Webhook URL has been saved.')
end end
def update def update
@webhook_url.update!(update_params)
redirect_back(fallback_location: settings_webhook_path(@webhook_url),
notice: I18n.t('webhook_url_has_been_updated'))
end
def destroy
@webhook_url.destroy!
redirect_to settings_webhooks_path, notice: I18n.t('webhook_url_has_been_deleted')
end
def resend
submitter = current_account.submitters.where.not(completed_at: nil).order(:id).last submitter = current_account.submitters.where.not(completed_at: nil).order(:id).last
authorize!(:read, submitter) SendFormCompletedWebhookRequestJob.perform_async({ 'submitter_id' => submitter.id,
'encrypted_config_id' => @encrypted_config.id })
if submitter.blank? || @webhook_url.blank? redirect_back(fallback_location: settings_webhooks_path, notice: 'Webhook request has been sent.')
return redirect_back(fallback_location: settings_webhooks_path,
alert: I18n.t('unable_to_resend_webhook_request'))
end
SendTestWebhookRequestJob.perform_async(
'submitter_id' => submitter.id,
'event_uuid' => SecureRandom.uuid,
'webhook_url_id' => @webhook_url.id
)
redirect_back(fallback_location: settings_webhooks_path, notice: I18n.t('webhook_request_has_been_sent'))
end end
private private
def create_params def load_encrypted_config
params.require(:webhook_url).permit(:url, events: []).reverse_merge(events: []) @encrypted_config =
current_account.encrypted_configs.find_or_initialize_by(key: EncryptedConfig::WEBHOOK_URL_KEY)
end end
def update_params def encrypted_config_params
params.require(:webhook_url).permit(:url) params.require(:encrypted_config).permit(:value)
end end
end end
+5 -45
View File
@@ -24,22 +24,12 @@ import SubmitForm from './elements/submit_form'
import PromptPassword from './elements/prompt_password' import PromptPassword from './elements/prompt_password'
import EmailsTextarea from './elements/emails_textarea' import EmailsTextarea from './elements/emails_textarea'
import ToggleOnSubmit from './elements/toggle_on_submit' import ToggleOnSubmit from './elements/toggle_on_submit'
import CheckOnClick from './elements/check_on_click'
import PasswordInput from './elements/password_input' import PasswordInput from './elements/password_input'
import SearchInput from './elements/search_input'
import ToggleAttribute from './elements/toggle_attribute'
import LinkedInput from './elements/linked_input'
import CheckboxGroup from './elements/checkbox_group'
import MaskedInput from './elements/masked_input'
import SetDateButton from './elements/set_date_button'
import IndeterminateCheckbox from './elements/indeterminate_checkbox'
import AppTour from './elements/app_tour'
import DashboardDropzone from './elements/dashboard_dropzone'
import RequiredCheckboxGroup from './elements/required_checkbox_group'
import PageContainer from './elements/page_container'
import * as TurboInstantClick from './lib/turbo_instant_click' import * as TurboInstantClick from './lib/turbo_instant_click'
import './images/preview.png'
TurboInstantClick.start() TurboInstantClick.start()
document.addEventListener('turbo:before-cache', () => { document.addEventListener('turbo:before-cache', () => {
@@ -97,23 +87,9 @@ safeRegisterElement('emails-textarea', EmailsTextarea)
safeRegisterElement('toggle-cookies', ToggleCookies) safeRegisterElement('toggle-cookies', ToggleCookies)
safeRegisterElement('toggle-on-submit', ToggleOnSubmit) safeRegisterElement('toggle-on-submit', ToggleOnSubmit)
safeRegisterElement('password-input', PasswordInput) safeRegisterElement('password-input', PasswordInput)
safeRegisterElement('search-input', SearchInput)
safeRegisterElement('toggle-attribute', ToggleAttribute)
safeRegisterElement('linked-input', LinkedInput)
safeRegisterElement('checkbox-group', CheckboxGroup)
safeRegisterElement('masked-input', MaskedInput)
safeRegisterElement('set-date-button', SetDateButton)
safeRegisterElement('indeterminate-checkbox', IndeterminateCheckbox)
safeRegisterElement('app-tour', AppTour)
safeRegisterElement('dashboard-dropzone', DashboardDropzone)
safeRegisterElement('check-on-click', CheckOnClick)
safeRegisterElement('required-checkbox-group', RequiredCheckboxGroup)
safeRegisterElement('page-container', PageContainer)
safeRegisterElement('template-builder', class extends HTMLElement { safeRegisterElement('template-builder', class extends HTMLElement {
connectedCallback () { connectedCallback () {
document.addEventListener('turbo:submit-end', this.onSubmit)
this.appElem = document.createElement('div') this.appElem = document.createElement('div')
this.appElem.classList.add('md:h-screen') this.appElem.classList.add('md:h-screen')
@@ -123,38 +99,23 @@ safeRegisterElement('template-builder', class extends HTMLElement {
backgroundColor: '#faf7f5', backgroundColor: '#faf7f5',
locale: this.dataset.locale, locale: this.dataset.locale,
withPhone: this.dataset.withPhone === 'true', withPhone: this.dataset.withPhone === 'true',
withVerification: ['true', 'false'].includes(this.dataset.withVerification) ? this.dataset.withVerification === 'true' : null,
withLogo: this.dataset.withLogo !== 'false', withLogo: this.dataset.withLogo !== 'false',
editable: this.dataset.editable !== 'false', editable: this.dataset.editable !== 'false',
authenticityToken: document.querySelector('meta[name="csrf-token"]')?.content, authenticityToken: document.querySelector('meta[name="csrf-token"]')?.content,
withPayment: this.dataset.withPayment === 'true', withPayment: this.dataset.withPayment === 'true',
isPaymentConnected: this.dataset.isPaymentConnected === 'true', isPaymentConnected: this.dataset.isPaymentConnected === 'true',
withFormula: this.dataset.withFormula === 'true', withFormula: this.dataset.withFormula === 'true',
withSendButton: this.dataset.withSendButton !== 'false',
withSignYourselfButton: this.dataset.withSignYourselfButton !== 'false',
withConditions: this.dataset.withConditions === 'true', withConditions: this.dataset.withConditions === 'true',
withReplaceAndCloneUpload: true,
currencies: (this.dataset.currencies || '').split(',').filter(Boolean), currencies: (this.dataset.currencies || '').split(',').filter(Boolean),
acceptFileTypes: this.dataset.acceptFileTypes, acceptFileTypes: this.dataset.acceptFileTypes
showTourStartForm: this.dataset.showTourStartForm === 'true'
}) })
this.component = this.app.mount(this.appElem) this.app.mount(this.appElem)
this.appendChild(this.appElem) this.appendChild(this.appElem)
} }
onSubmit = (e) => {
if (e.detail.success && e.detail?.formSubmission?.formElement?.id === 'submitters_form') {
e.detail.fetchResponse.response.json().then((data) => {
this.component.template.submitters = data.submitters
})
}
}
disconnectedCallback () { disconnectedCallback () {
document.removeEventListener('turbo:submit-end', this.onSubmit)
this.app?.unmount() this.app?.unmount()
this.appElem?.remove() this.appElem?.remove()
} }
@@ -167,8 +128,7 @@ safeRegisterElement('import-list', class extends HTMLElement {
this.app = createApp(ImportList, { this.app = createApp(ImportList, {
template: JSON.parse(this.dataset.template), template: JSON.parse(this.dataset.template),
multitenant: this.dataset.multitenant === 'true', multitenant: this.dataset.multitenant === 'true',
authenticityToken: document.querySelector('meta[name="csrf-token"]')?.content, authenticityToken: document.querySelector('meta[name="csrf-token"]')?.content
i18n: JSON.parse(this.dataset.i18n || '{}')
}) })
this.app.mount(this.appElem) this.app.mount(this.appElem)
+4 -25
View File
@@ -97,27 +97,10 @@ button[disabled] .enabled {
bottom: auto; bottom: auto;
} }
.tooltip-bottom-start:before {
transform: translateX(-31%);
top: var(--tooltip-offset);
left: 100%;
right: auto;
bottom: auto;
}
.tooltip-bottom-start:after {
transform: translateX(-25%);
border-color: transparent transparent var(--tooltip-color) transparent;
top: var(--tooltip-tail-offset);
left: 50%;
right: auto;
bottom: auto;
}
.autocomplete { .autocomplete {
background: white; background: white;
z-index: 1000; z-index: 1000;
font: 16px/25px "-apple-system", BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; font: 14px/22px "-apple-system", BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
overflow: auto; overflow: auto;
box-sizing: border-box; box-sizing: border-box;
@apply border border-base-300 mt-1 rounded-md; @apply border border-base-300 mt-1 rounded-md;
@@ -128,7 +111,7 @@ button[disabled] .enabled {
} }
.autocomplete > div { .autocomplete > div {
@apply px-2 py-1.5 font-normal; @apply px-2 py-1 font-normal text-sm;
} }
.autocomplete .group { .autocomplete .group {
@@ -148,10 +131,6 @@ button[disabled] .enabled {
outline-color: hsl(var(--bc) / 0.2); outline-color: hsl(var(--bc) / 0.2);
} }
.font-times { select:required:invalid {
font-family: "Times New Roman", Times, ui-serif, serif, Cambria, Georgia; color: gray !important;
}
.font-courier {
font-family: "Courier New", Consolas, "Liberation Mono", monospace, ui-monospace, SFMono-Regular, Menlo, Monaco;
} }
+17 -24
View File
@@ -1,6 +1,5 @@
import SignaturePad from 'signature_pad' import SignaturePad from 'signature_pad'
import { cropCanvasAndExportToPNG } from './submission_form/crop_canvas' import { cropCanvasAndExportToPNG } from './submission_form/crop_canvas'
import { isValidSignatureCanvas } from './submission_form/validate_signature'
window.customElements.define('draw-signature', class extends HTMLElement { window.customElements.define('draw-signature', class extends HTMLElement {
connectedCallback () { connectedCallback () {
@@ -13,10 +12,6 @@ window.customElements.define('draw-signature', class extends HTMLElement {
this.pad = new SignaturePad(this.canvas) this.pad = new SignaturePad(this.canvas)
if (this.dataset.color) {
this.pad.penColor = this.dataset.color
}
this.pad.addEventListener('endStroke', () => { this.pad.addEventListener('endStroke', () => {
this.updateSubmitButtonVisibility() this.updateSubmitButtonVisibility()
}) })
@@ -44,8 +39,6 @@ window.customElements.define('draw-signature', class extends HTMLElement {
return response return response
}) })
}).catch(error => {
console.log(error)
}).finally(() => { }).finally(() => {
this.submitButton.disabled = false this.submitButton.disabled = false
}) })
@@ -68,26 +61,26 @@ window.customElements.define('draw-signature', class extends HTMLElement {
} }
async submitImage () { async submitImage () {
if (!isValidSignatureCanvas(this.pad.toData())) { return new Promise((resolve, reject) => {
alert('Signature is too small or simple. Please redraw.') cropCanvasAndExportToPNG(this.canvas, { errorOnTooSmall: true }).then(async (blob) => {
const file = new File([blob], 'signature.png', { type: 'image/png' })
return Promise.reject(new Error('Image too small or simple')) const formData = new FormData()
}
return cropCanvasAndExportToPNG(this.canvas).then(async (blob) => { formData.append('file', file)
const file = new File([blob], 'signature.png', { type: 'image/png' }) formData.append('submitter_slug', this.dataset.slug)
formData.append('name', 'attachments')
formData.append('remember_signature', 'true')
const formData = new FormData() return fetch('/api/attachments', {
method: 'POST',
formData.append('file', file) body: formData
formData.append('submitter_slug', this.dataset.slug) }).then((resp) => resp.json()).then((attachment) => {
formData.append('name', 'attachments') return resolve(attachment)
formData.append('remember_signature', 'true') })
}).catch((error) => {
return fetch('/api/attachments', { return reject(error)
method: 'POST', })
body: formData
}).then(resp => resp.json())
}) })
} }
-338
View File
@@ -1,338 +0,0 @@
export default class extends HTMLElement {
async connectedCallback () {
this.tourType = this.dataset.type
this.nextPagePath = this.dataset.nextPagePath
this.I18n = JSON.parse(this.dataset.i18n || '{}')
if (this.dataset.showTour === 'true') this.start()
}
async start () {
if (window.innerWidth < 768) return
const [{ driver }] = await Promise.all([
import('driver.js'),
import('driver.js/dist/driver.css')
])
this.driverObj = driver({
showProgress: true,
nextBtnText: this.I18n.next,
prevBtnText: this.I18n.previous,
doneBtnText: this.I18n.done,
onDestroyStarted: () => {
this.disableAppGuide().finally(() => { this.destroy() })
},
onHighlightStarted: (element) => {
if (element) {
const clickHandler = () => {
this.disableAppGuide().finally(() => { this.destroy() })
element.removeEventListener('click', clickHandler)
}
element.addEventListener('click', clickHandler)
}
}
})
if (this.tourType === 'dashboard') {
this.showDashboardTour()
} else if (this.tourType === 'builder') {
this.showTemplateBuilderTour()
} else if (this.tourType === 'account') {
this.showAccountTour()
} else if (this.tourType === 'template') {
this.showTemplateTour()
}
}
disconnectedCallback () {
if (this.driverObj) this.destroy()
}
destroy () {
if (this.builderTemplate) this.builderTemplate.fields.shift()
if (this.driverObj) this.driverObj.destroy()
}
showTemplateTour () {
const steps = [
{
element: '#share_link_clipboard',
popover: {
title: this.I18n.copy_and_share_link,
description: this.I18n.copy_and_share_link_description,
side: 'bottom',
align: 'end'
}
},
{
element: '#sign_yourself_button',
popover: {
title: this.I18n.sign_the_document,
description: this.I18n.sign_the_document_description,
side: 'top',
align: 'center'
}
},
{
element: '#send_to_recipients_button',
popover: {
title: this.I18n.send_for_signing,
description: this.I18n.add_recipients_description,
side: 'top',
align: 'center'
}
},
{
element: '#add_recipients_button',
popover: {
title: this.I18n.add_recipients,
description: this.I18n.add_recipients_description,
side: 'bottom',
align: 'end'
}
},
{
element: '#account_settings_button',
popover: {
title: this.I18n.settings,
description: this.I18n.settings_template_description,
side: 'right',
align: 'start',
showButtons: this.nextPagePath ? ['next', 'previous', 'close'] : ['previous', 'close'],
onNextClick: () => {
if (this.nextPagePath) {
window.Turbo.visit(this.nextPagePath)
}
}
}
}
].filter((step) => document.querySelector(step.element))
this.driverObj.setSteps(steps)
this.driverObj.drive()
}
showDashboardTour () {
this.driverObj.setSteps([
{
element: '#templates_submissions_toggle',
popover: {
title: this.I18n.template_and_submissions,
description: this.I18n.template_and_submissions_description,
side: 'right',
align: 'start'
}
},
{
element: '#templates_upload_button',
popover: {
title: this.I18n.upload_a_pdf_file,
description: this.I18n.upload_a_pdf_file_description,
side: 'left',
align: 'start',
showButtons: this.nextPagePath ? ['next', 'previous', 'close'] : ['previous', 'close'],
onNextClick: () => {
if (this.nextPagePath) {
window.Turbo.visit(this.nextPagePath)
}
}
},
onHighlightStarted: () => {}
}
])
this.driverObj.drive()
}
showAccountTour () {
this.driverObj.setSteps([
{
element: '#account_settings_menu',
popover: {
title: this.I18n.settings,
description: this.I18n.settings_account_description,
side: 'right',
align: 'start'
}
},
{
element: '#support_channels',
popover: {
title: this.I18n.support,
description: this.I18n.support_description,
side: 'left',
align: 'start'
}
}
].filter((step) => document.querySelector(step.element)))
this.driverObj.drive()
}
showTemplateBuilderTour () {
const builderComponent = document.querySelector('template-builder')?.component
this.builderTemplate = builderComponent?.template
if (this.builderTemplate) {
this.builderTemplate.fields.unshift({
uuid: 'b387399b-88dc-4345-9d37-743e97a9b2b3',
submitter_uuid: this.builderTemplate.submitters[0].uuid,
name: 'First Name',
type: 'text'
})
builderComponent.$nextTick(() => {
this.driverObj.setSteps([
{
element: '.roles-dropdown',
popover: {
title: this.I18n.select_a_signer_party,
description: this.I18n.select_a_signer_party_description,
side: 'left',
align: 'start',
onPopoverRender: () => {
const rolesDropdown = document.querySelector('.roles-dropdown')
rolesDropdown.dispatchEvent(new Event('mouseenter', { bubbles: true, cancelable: true }))
rolesDropdown.classList.add('dropdown-open')
}
}
},
{
element: '.roles-dropdown .dropdown-content',
popover: {
title: this.I18n.available_parties,
description: this.I18n.available_parties_description,
side: 'left',
align: 'start',
onPopoverRender: () => {
document.querySelector('.roles-dropdown .dropdown-content').classList.remove('driver-active-element')
},
onNextClick: () => {
document.querySelector('.roles-dropdown').classList.remove('dropdown-open')
this.driverObj.moveNext()
}
}
},
{
element: '#field-types-grid',
popover: {
title: this.I18n.available_field_types,
description: this.I18n.available_field_types_description,
side: 'right',
align: 'start',
onPrevClick: () => {
document.querySelector('.roles-dropdown').classList.add('dropdown-open')
this.driverObj.movePrevious()
}
}
},
{
element: '#text_type_field_button',
popover: {
title: this.I18n.text_input_field,
description: this.I18n.text_input_field_description,
side: 'left',
align: 'start'
}
},
{
element: '#signature_type_field_button',
popover: {
title: this.I18n.signature_field,
description: this.I18n.signature_field_description,
side: 'left',
align: 'start'
}
},
{
element: '.fields',
popover: {
title: this.I18n.added_fields,
description: this.I18n.added_fields_description,
side: 'right',
align: 'start'
}
},
{
element: '.list-field label:has(svg.tabler-icon-settings)',
popover: {
title: this.I18n.open_field_settings,
description: this.I18n.open_field_settings_description,
side: 'bottom',
align: 'end',
onPopoverRender: () => {
const settingsDropdown = document.querySelector('.list-field div:first-child span:has(svg.tabler-icon-settings)')
document.querySelectorAll('.list-field div:first-child .text-transparent').forEach((e) => e.classList.remove('text-transparent'))
settingsDropdown.dispatchEvent(new Event('mouseenter', { bubbles: true, cancelable: true }))
settingsDropdown.classList.add('dropdown-open')
}
}
},
{
element: '.list-field div:first-child span:has(svg.tabler-icon-settings) .dropdown-content',
popover: {
title: this.I18n.field_settings,
description: this.I18n.field_settings_description,
side: 'left',
align: 'start',
onPopoverRender: () => {
document.querySelector('.list-field div:first-child span:has(svg.tabler-icon-settings) .dropdown-content').classList.remove('driver-active-element')
},
onNextClick: () => {
document.querySelector('.list-field div:first-child span:has(svg.tabler-icon-settings)').classList.remove('dropdown-open')
this.driverObj.moveNext()
}
}
},
{
element: '#send_button',
popover: {
title: this.I18n.send_document,
description: this.I18n.send_document_description,
side: 'bottom',
align: 'end',
onPrevClick: () => {
document.querySelector('.list-field div:first-child span:has(svg.tabler-icon-settings)').classList.add('dropdown-open')
this.driverObj.movePrevious()
}
}
},
{
element: '#sign_yourself_button',
popover: {
title: this.I18n.sign_yourself,
description: this.I18n.sign_yourself_description,
side: 'bottom',
align: 'end',
onNextClick: () => {
if (this.nextPagePath) {
window.Turbo.visit(this.nextPagePath)
} else {
this.destroy()
}
}
}
}
])
this.driverObj.drive()
})
}
}
async disableAppGuide () {
return fetch('/user_configs', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRF-Token': document.querySelector('meta[name="csrf-token"]').content
},
body: JSON.stringify({ key: 'show_app_tour', value: false })
})
}
}
@@ -3,8 +3,6 @@ export default class extends HTMLElement {
this.resize() this.resize()
this.textarea.addEventListener('input', () => this.resize()) this.textarea.addEventListener('input', () => this.resize())
this.observeVisibility()
} }
resize () { resize () {
@@ -13,28 +11,6 @@ export default class extends HTMLElement {
} }
} }
observeVisibility () {
this.observer = new IntersectionObserver(
(entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
this.resize()
this.observer.unobserve(this.textarea)
}
})
},
{
threshold: 0.1
}
)
this.observer.observe(this.textarea)
}
disconnectedCallback () {
this.observer.unobserve(this.textarea)
}
get textarea () { get textarea () {
return this.querySelector('textarea') return this.querySelector('textarea')
} }
-14
View File
@@ -1,14 +0,0 @@
export default class extends HTMLElement {
connectedCallback () {
this.addEventListener('click', () => {
if (this.element && !this.element.disabled && !this.element.checked) {
this.element.checked = true
this.element.dispatchEvent(new Event('change', { bubbles: true }))
}
})
}
get element () {
return document.getElementById(this.dataset.elementId)
}
}
-15
View File
@@ -1,15 +0,0 @@
export default class extends HTMLElement {
connectedCallback () {
this.items.forEach((item) => {
item.addEventListener('change', (e) => {
this.items.forEach((item) => {
item.checked = item === e.target && e.target.checked
})
})
})
}
get items () {
return this.querySelectorAll('input[type="checkbox"]')
}
}
@@ -3,6 +3,8 @@ export default class extends HTMLElement {
this.clearChecked() this.clearChecked()
this.addEventListener('click', (e) => { this.addEventListener('click', (e) => {
e.stopPropagation()
const text = this.dataset.text || this.innerText.trim() const text = this.dataset.text || this.innerText.trim()
if (navigator.clipboard) { if (navigator.clipboard) {
Loaded 100 of 594 files, more files were not shown because too many files have changed in this diff. Show more