mirror of
https://github.com/docusealco/docuseal.git
synced 2026-08-07 15:25:16 +00:00
Compare commits
470 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 6ab68cc36b | |||
| ee2400a5c5 | |||
| 80be5d2cf5 | |||
| 76cf44a31b | |||
| eb63113227 | |||
| b7045fb3d9 | |||
| 11a5b5842d | |||
| 362994afd5 | |||
| d7682f4ebd | |||
| 56d4fe7fa9 | |||
| 93a852ac14 | |||
| 964db0ab08 | |||
| 8547295da7 | |||
| c75050fa46 | |||
| 65372c52aa | |||
| 12c767b7d3 | |||
| 2fc5e9cfb3 | |||
| bccc78ee65 | |||
| c0793f3ce7 | |||
| a8dd8699a3 | |||
| 91bb25e8b6 | |||
| c3f4480cdc | |||
| 33e226861e | |||
| 4f5acbec9d | |||
| d2c9bbf6ab | |||
| b5e57e1f2f | |||
| c82c8b2272 | |||
| 43f1b598a7 | |||
| 5081e387d0 | |||
| 1e5f55c1d6 | |||
| aa38773be3 | |||
| 96530bd809 | |||
| 127f95a9d0 | |||
| cf31e12348 | |||
| 4adaa531b7 | |||
| c7659563b8 | |||
| 7b5494468f | |||
| d12c67fd54 | |||
| e20eacb5ee | |||
| 98055f0762 | |||
| a46c42c3a0 | |||
| c3d1503361 | |||
| 65b47d27cd | |||
| 968c07f8be | |||
| 91814ca105 | |||
| 9128f24270 | |||
| 264897bbd5 | |||
| 67d59ff1ba | |||
| 81eee56e1e | |||
| 92ea8af786 | |||
| df0616f77e | |||
| a337dff85c | |||
| 48e349c1d2 | |||
| df9fa0fec7 | |||
| b3e72f0726 | |||
| 700ebc4727 | |||
| 745a18efca | |||
| b4a8147ff0 | |||
| 0543c25054 | |||
| b1a0afea66 | |||
| 43c41e2557 | |||
| bc8eb33f05 | |||
| d1cebc92f7 | |||
| bf02cf4058 | |||
| 5212773d0a | |||
| bf53204a41 | |||
| 092a19966c | |||
| 6060a2b798 | |||
| ce12af68b2 | |||
| c05f8d47aa | |||
| f8be19c20e | |||
| 2fe32a2145 | |||
| 794ea4d0e8 | |||
| 307b0f5afd | |||
| 9011e3f4b4 | |||
| 0b45ce5c0c | |||
| a8ab93a4cc | |||
| d1768be636 | |||
| b2cd7c82a7 | |||
| b88d99afe0 | |||
| 4523d38551 | |||
| c4ba892559 | |||
| 40f8093e00 | |||
| 4375459856 | |||
| b3eb8a70bf | |||
| 6245a487b6 | |||
| a89f4c1b10 | |||
| 7df729b8b5 | |||
| f1db7c7e82 | |||
| 00af69cbf2 | |||
| c6819a19bd | |||
| f5cbcb3d24 | |||
| 6ca75c53ef | |||
| e0ec443e10 | |||
| 9eed2fc2ef | |||
| b9c6d3ff9f | |||
| 03c25a89b2 | |||
| f5b4a0c5ab | |||
| b10ed46ccc | |||
| c55036a1b1 | |||
| 962a5dade5 | |||
| 369a09a6df | |||
| e9a39e2571 | |||
| 1cf9cc18f1 | |||
| 1984d4ec8d | |||
| fa99b4ebdc | |||
| 83c793a2a5 | |||
| d078727070 | |||
| 0d8e2c5ff0 | |||
| ea062d7494 | |||
| d525e0597a | |||
| 964b5d4e74 | |||
| abf17eb09f | |||
| 178809dad7 | |||
| 881d189715 | |||
| 48a8973910 | |||
| ae891932c5 | |||
| a8b5e00814 | |||
| e98432d7e4 | |||
| 29ff58ed98 | |||
| a621575ffc | |||
| d7c7efc203 | |||
| 4f013ca927 | |||
| d9823feef7 | |||
| 676d775544 | |||
| 840ad17258 | |||
| ce0cd09b70 | |||
| bb752fbc00 | |||
| c71fb04404 | |||
| e856cf8787 | |||
| 71aae65bc6 | |||
| 7a33444f9b | |||
| e12ec9d986 | |||
| 94d1fb7dcb | |||
| 6e43a42274 | |||
| 0e0aa0259e | |||
| 75ab6d39ef | |||
| 6f84d84f96 | |||
| cf46593e9b | |||
| 1808346348 | |||
| ea59a68499 | |||
| cba2d02990 | |||
| e92fc0d3e1 | |||
| f1eaa77092 | |||
| 5934bdec55 | |||
| 7a7d3c6ee3 | |||
| cb2f58c04c | |||
| a1c117f1ca | |||
| 1aacd98460 | |||
| 840162c9ca | |||
| 3519b0e009 | |||
| 53fae56c51 | |||
| f4552dbaa8 | |||
| dfab4c331c | |||
| d2a0937038 | |||
| fa1ef44b22 | |||
| c1a5c91299 | |||
| e6280e8f5a | |||
| fd2ba57325 | |||
| dc178ef03f | |||
| 68936e10c5 | |||
| 41f16e7d39 | |||
| 453ebbf90a | |||
| 7ca290c080 | |||
| dd5b6a6aa6 | |||
| bf2ebb995b | |||
| 3e929758fb | |||
| 4a53116586 | |||
| 2d1981acfa | |||
| c610df269f | |||
| 96dac635f5 | |||
| abd579103d | |||
| 7ed27bb413 | |||
| 180c542a9f | |||
| 89613113de | |||
| 3102900776 | |||
| 1a68053f4e | |||
| 5d4a032763 | |||
| 7481d27822 | |||
| 22afd01fd2 | |||
| fd9eb09dd6 | |||
| c53ed7f984 | |||
| 1d87e210be | |||
| 5fec952b8c | |||
| c4fbcc103a | |||
| 9333a1ba45 | |||
| de5cd92dba | |||
| 83e32bec9f | |||
| 4603efc04e | |||
| 6c73c82a27 | |||
| 8e71d9f9e9 | |||
| 80f60a5d88 | |||
| 2c33ec382d | |||
| 884865cffc | |||
| 9537fe8c91 | |||
| c8fe36a2b6 | |||
| 7ec3282b56 | |||
| ab8c41a520 | |||
| 4c1c70ff04 | |||
| d7a6e80bb1 | |||
| f87ef670d1 | |||
| daba2505bc | |||
| 71528eda45 | |||
| 269e2beeaa | |||
| 72dd2c6bc5 | |||
| 9758f7a15f | |||
| 2521eb5a4a | |||
| bf6afc5e61 | |||
| 291d869843 | |||
| cdfecb2a8f | |||
| c0df8e0ec3 | |||
| a21f7d1653 | |||
| 79e2514416 | |||
| 80a7385183 | |||
| 1c8a7b6a7c | |||
| b46ced2b6f | |||
| 5ae479644e | |||
| cb66eb1293 | |||
| b4d5a2cad4 | |||
| 32691b0c0d | |||
| 9163f744a3 | |||
| 23ecce51a3 | |||
| 747ffa8cf1 | |||
| 4e230899ed | |||
| aff9f753c7 | |||
| 1127fac500 | |||
| 7707e1cd2b | |||
| 30274f7f1a | |||
| a5da6cc781 | |||
| 201e4010c5 | |||
| b9bbdbffae | |||
| 9e8e1996e8 | |||
| b99235397a | |||
| c0fd06daf3 | |||
| 90536294df | |||
| f8e9787675 | |||
| bf9b785946 | |||
| dab3a6f031 | |||
| 88466193c5 | |||
| c2f7601dad | |||
| 189f7b2d7b | |||
| 5d87160ee9 | |||
| 0cdcdb8898 | |||
| c20619b5c8 | |||
| 93f84abce4 | |||
| 3c6a32caba | |||
| 72c08e5fdb | |||
| 022d80b5ce | |||
| f48da51802 | |||
| 709d04b33f | |||
| 3a251dd063 | |||
| 9cc92b59b3 | |||
| e0469b10a7 | |||
| 39eb67b162 | |||
| 8d9bea3b0f | |||
| 7015d8dde8 | |||
| 6b54072cb5 | |||
| b898708306 | |||
| 1b1cf36839 | |||
| 18b89edc19 | |||
| 70c7ef4247 | |||
| a8196709be | |||
| a9d249ccf0 | |||
| cd83d918e5 | |||
| b64a84a362 | |||
| f4426a8ee0 | |||
| df8c44d2ce | |||
| 65ce3d4822 | |||
| e5b1d94579 | |||
| 0b4754ca24 | |||
| b3dc8d55be | |||
| e5ab7667b9 | |||
| 2a4e6435f1 | |||
| 804431c44a | |||
| f3b26bf7a0 | |||
| aa27d90017 | |||
| 0fa8cf2dd0 | |||
| 634b5c37ba | |||
| 6e16f81fc6 | |||
| ffc4024f70 | |||
| a6981185b2 | |||
| 0361fabb66 | |||
| 04e7f101be | |||
| 86d680cfdf | |||
| 7bc2152057 | |||
| 74ed9a5040 | |||
| 30115fc0dc | |||
| 8e8e5e0e29 | |||
| b58ce1b571 | |||
| df5c4e4210 | |||
| ba2e0999f1 | |||
| a1f5dd08c7 | |||
| c1123fef63 | |||
| 4dc6149530 | |||
| 1245ff2cce | |||
| 284204fd78 | |||
| f7be74eb73 | |||
| aeea619059 | |||
| ea2b7f20e7 | |||
| f40f1d25de | |||
| c04bb2d7cf | |||
| 4c1ccd65bf | |||
| 54e064b0fc | |||
| 9a8b72883c | |||
| 5d75ee2e47 | |||
| 7357fa4871 | |||
| f6850b5427 | |||
| 1e9a181e60 | |||
| 7224a1ccec | |||
| 9b935a8180 | |||
| c4a693846e | |||
| 02c7cdcd97 | |||
| 36bee92e8e | |||
| 6542804f44 | |||
| d805fd614c | |||
| 21299dc65f | |||
| 0df0946c2a | |||
| 1bb38d50a1 | |||
| 242987cb41 | |||
| 5456abde54 | |||
| 4a07427440 | |||
| cf60dae2ff | |||
| 659997c6c1 | |||
| 389a47a152 | |||
| b6b81ca487 | |||
| 719c1786f1 | |||
| 4685bac4b3 | |||
| a05275cc08 | |||
| 1abf1b1658 | |||
| 8893dc5698 | |||
| f8467b2da1 | |||
| f39b8b000e | |||
| 961036fa31 | |||
| 8b9056894e | |||
| 7f979e9396 | |||
| 8db1192d0e | |||
| 3bd515951a | |||
| 2bd5b9676b | |||
| 993698ec01 | |||
| fcdc44ddbe | |||
| 0cb6b0ae12 | |||
| 0388927c5b | |||
| 2c8f5d2129 | |||
| 2bffbf83c6 | |||
| ec01529014 | |||
| ea1abf5662 | |||
| fcd5263af1 | |||
| 1fa8616dbc | |||
| f627277b45 | |||
| 809c3ea270 | |||
| 9caa9d79d4 | |||
| a8f1c1c323 | |||
| 59fbea5c5f | |||
| d3273d879a | |||
| ea227e82c9 | |||
| 676b330aa3 | |||
| 8d3295e125 | |||
| 8cb74e0bcc | |||
| 26749a4dbc | |||
| 14915d37b4 | |||
| 16d6d58efb | |||
| b2b97a313a | |||
| 2974e6a64f | |||
| dae34d0aa5 | |||
| e9d0671184 | |||
| 897e33bf6a | |||
| f37e93a409 | |||
| 561ac63c93 | |||
| 149e4be07a | |||
| eb98dc9e06 | |||
| ce7d965358 | |||
| bb73024859 | |||
| 631a442e2a | |||
| 20a082ae8e | |||
| 154d706be1 | |||
| aaae257c75 | |||
| ec944a2283 | |||
| 69d63ecffe | |||
| 06a1e2992d | |||
| ec9293bac0 | |||
| 8bf727c43c | |||
| 708b8afdf5 | |||
| 6f7b9b1ebf | |||
| 4f48c4fd9f | |||
| 9dac90e6cc | |||
| 0741a6f485 | |||
| 1f338dfd2b | |||
| 0310c26edd | |||
| e43570d4c1 | |||
| d7142c2935 | |||
| fa6dc04ef1 | |||
| c5ec7a4b37 | |||
| bd3d643318 | |||
| 1ae635963c | |||
| a61c3e798d | |||
| 931addedbe | |||
| 9aa4a547cf | |||
| 82a7df5e3f | |||
| 0ff87383d0 | |||
| ea3dec443f | |||
| d7a895b98b | |||
| b90b7b7845 | |||
| 3d05d75f2d | |||
| e701881377 | |||
| 4615e3e48f | |||
| 988a5361a6 | |||
| 1b60b42428 | |||
| 7b74487214 | |||
| 91d98ed267 | |||
| 24fbb7de06 | |||
| 91df5b2407 | |||
| eef2d9d599 | |||
| 8ad70b7e74 | |||
| 8b775103c7 | |||
| 8c2475aac1 | |||
| d3236ae4a1 | |||
| 8fd9af3a6a | |||
| 4f94858901 | |||
| d0ac4ad222 | |||
| 10e4476f4a | |||
| 74411d883a | |||
| 18ee2d5316 | |||
| b4f80422eb | |||
| 7fe9ab7601 | |||
| 13ad614e6b | |||
| 7b1b4bcf2c | |||
| a6715d4034 | |||
| 254995c6c6 | |||
| 0e33e23d1f | |||
| 57503eefd2 | |||
| cd477c880c | |||
| bf2f5b24dc | |||
| 7eba4865bc | |||
| 64cd24a24d | |||
| de5acd0949 | |||
| 6ebbc3edac | |||
| 2020f6c136 | |||
| ee4acd5a82 | |||
| 2e487ac936 | |||
| dee5fc7c07 | |||
| e130584fec | |||
| ade5c4832f | |||
| 33b14d5278 | |||
| 48e73c2c81 | |||
| 894d98e92b | |||
| 14c84eff93 | |||
| 01d982b093 | |||
| 35852ef6ab | |||
| 47b235dc5f | |||
| 0e11d2a581 | |||
| e57e11f206 | |||
| 51f639065c | |||
| 6c1d4b466f | |||
| b940c597dc | |||
| 1ce67bcb39 | |||
| 5050a67d08 | |||
| 76321cef3c | |||
| 8f3a203994 | |||
| ea24562861 | |||
| 4a888d048e | |||
| 36331bb993 | |||
| 6f0e7c0ca8 | |||
| 2491b7b7e3 | |||
| 7965c17f46 | |||
| 5519459519 | |||
| d8bf2cc101 | |||
| 2aece98d67 | |||
| f602497255 | |||
| 11db68cdc7 | |||
| bed98f4344 |
@@ -6,12 +6,13 @@ jobs:
|
||||
rubocop:
|
||||
name: Rubocop
|
||||
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
|
||||
ruby-version: 4.0.1
|
||||
- name: Cache gems
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
@@ -30,12 +31,13 @@ jobs:
|
||||
erblint:
|
||||
name: Erblint
|
||||
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
|
||||
ruby-version: 4.0.1
|
||||
- name: Cache gems
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
@@ -54,12 +56,13 @@ jobs:
|
||||
eslint:
|
||||
name: ESLint
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 10
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Install Node.js
|
||||
uses: actions/setup-node@v1
|
||||
with:
|
||||
node-version: 20.9.0
|
||||
node-version: 20.19.0
|
||||
- name: Cache directory path
|
||||
id: yarn-cache-dir-path
|
||||
run: echo "::set-output name=dir::$(yarn cache dir)"
|
||||
@@ -80,12 +83,13 @@ jobs:
|
||||
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
|
||||
ruby-version: 4.0.1
|
||||
- name: Cache gems
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
@@ -107,6 +111,7 @@ jobs:
|
||||
rspec:
|
||||
name: RSpec
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 10
|
||||
|
||||
services:
|
||||
postgres:
|
||||
@@ -127,11 +132,11 @@ jobs:
|
||||
- name: Install Ruby
|
||||
uses: ruby/setup-ruby@v1
|
||||
with:
|
||||
ruby-version: 3.4.2
|
||||
ruby-version: 4.0.1
|
||||
- name: Set up Node
|
||||
uses: actions/setup-node@v1
|
||||
with:
|
||||
node-version: 20.9.0
|
||||
node-version: 20.19.0
|
||||
- name: Install Chrome
|
||||
uses: browser-actions/setup-chrome@latest
|
||||
with:
|
||||
|
||||
@@ -8,6 +8,7 @@ on:
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-24.04-arm
|
||||
timeout-minutes: 30
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
@@ -15,15 +16,13 @@ jobs:
|
||||
with:
|
||||
submodules: recursive
|
||||
|
||||
-
|
||||
name: Docker meta
|
||||
- name: Docker meta
|
||||
id: meta
|
||||
uses: docker/metadata-action@v4
|
||||
with:
|
||||
images: |
|
||||
docuseal/docuseal
|
||||
tags: |
|
||||
type=semver,pattern={{version}}
|
||||
images: docuseal/docuseal
|
||||
tags: type=semver,pattern={{version}}
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3
|
||||
|
||||
|
||||
@@ -37,3 +37,4 @@ yarn-debug.log*
|
||||
/docuseal
|
||||
/ee
|
||||
dump.rdb
|
||||
*.onnx
|
||||
|
||||
+25
-4
@@ -1,4 +1,4 @@
|
||||
require:
|
||||
plugins:
|
||||
- rubocop-performance
|
||||
- rubocop-rails
|
||||
- rubocop-rspec
|
||||
@@ -10,7 +10,7 @@ AllCops:
|
||||
- node_modules/**/*
|
||||
- bin/*
|
||||
- vendor/**/*
|
||||
TargetRubyVersion: '3.3'
|
||||
TargetRubyVersion: '4.0'
|
||||
SuggestExtensions: false
|
||||
|
||||
Metrics/BlockLength:
|
||||
@@ -42,6 +42,18 @@ Metrics/CyclomaticComplexity:
|
||||
Metrics/PerceivedComplexity:
|
||||
Max: 15
|
||||
|
||||
Style/MultipleComparison:
|
||||
Enabled: false
|
||||
|
||||
Style/NumericPredicate:
|
||||
Enabled: false
|
||||
|
||||
Style/MinMaxComparison:
|
||||
Enabled: false
|
||||
|
||||
Naming/PredicateMethod:
|
||||
Enabled: false
|
||||
|
||||
Layout/LineLength:
|
||||
AllowedPatterns: ['\A\s*#']
|
||||
|
||||
@@ -63,10 +75,13 @@ RSpec/MultipleExpectations:
|
||||
Max: 25
|
||||
|
||||
RSpec/ExampleLength:
|
||||
Max: 50
|
||||
Max: 500
|
||||
|
||||
RSpec/MultipleMemoizedHelpers:
|
||||
Max: 9
|
||||
Max: 15
|
||||
|
||||
RSpec/AnyInstance:
|
||||
Enabled: false
|
||||
|
||||
Metrics/BlockNesting:
|
||||
Max: 5
|
||||
@@ -85,3 +100,9 @@ Rails/ApplicationController:
|
||||
|
||||
Rails/Output:
|
||||
Enabled: false
|
||||
|
||||
Rails/StrongParametersExpect:
|
||||
Enabled: false
|
||||
|
||||
Rails/RedirectBackOrTo:
|
||||
Enabled: false
|
||||
|
||||
+28
-22
@@ -1,4 +1,4 @@
|
||||
FROM ruby:3.4.2-alpine AS download
|
||||
FROM ruby:4.0.1-alpine AS download
|
||||
|
||||
WORKDIR /fonts
|
||||
|
||||
@@ -9,13 +9,14 @@ RUN apk --no-cache add fontforge wget && \
|
||||
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 /model.onnx "https://github.com/docusealco/fields-detection/releases/download/2.0.0/model_704_int8.onnx" && \
|
||||
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")'
|
||||
|
||||
FROM ruby:3.4.2-alpine AS webpack
|
||||
FROM ruby:4.0.1-alpine AS webpack
|
||||
|
||||
ENV RAILS_ENV=production
|
||||
ENV NODE_ENV=production
|
||||
@@ -41,16 +42,18 @@ COPY ./app/views ./app/views
|
||||
|
||||
RUN echo "gem 'shakapacker'" > Gemfile && ./bin/shakapacker
|
||||
|
||||
FROM ruby:3.4.2-alpine AS app
|
||||
FROM ruby:4.0.1-alpine AS app
|
||||
|
||||
ENV RAILS_ENV=production
|
||||
ENV BUNDLE_WITHOUT="development:test"
|
||||
ENV LD_PRELOAD=/lib/libgcompat.so.0
|
||||
ENV OPENSSL_CONF=/app/openssl_legacy.cnf
|
||||
ENV OPENSSL_CONF=/etc/openssl_legacy.cnf
|
||||
|
||||
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 vips-dev yaml-dev redis libheif vips-heif gcompat ttf-freefont onnxruntime && mkdir /fonts && rm /usr/share/fonts/freefont/FreeSans.otf
|
||||
|
||||
RUN addgroup -g 2000 docuseal && adduser -u 2000 -G docuseal -s /bin/sh -D -h /home/docuseal docuseal
|
||||
|
||||
RUN echo $'.include = /etc/ssl/openssl.cnf\n\
|
||||
\n\
|
||||
@@ -62,33 +65,36 @@ legacy = legacy_sect\n\
|
||||
activate = 1\n\
|
||||
\n\
|
||||
[legacy_sect]\n\
|
||||
activate = 1' >> /app/openssl_legacy.cnf
|
||||
activate = 1' >> /etc/openssl_legacy.cnf
|
||||
|
||||
COPY ./Gemfile ./Gemfile.lock ./
|
||||
COPY --chown=docuseal:docuseal ./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 git && bundle install && apk del --no-cache build-base git && 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 && ln -sf /usr/lib/libonnxruntime.so.1 $(ruby -e "print Dir[Gem::Specification.find_by_name('onnxruntime').gem_dir + '/vendor/*.so'].first")
|
||||
|
||||
COPY ./bin ./bin
|
||||
COPY ./app ./app
|
||||
COPY ./config ./config
|
||||
COPY ./db/migrate ./db/migrate
|
||||
COPY ./log ./log
|
||||
COPY ./lib ./lib
|
||||
COPY ./public ./public
|
||||
COPY ./tmp ./tmp
|
||||
COPY LICENSE README.md Rakefile config.ru .version ./
|
||||
COPY .version ./public/version
|
||||
COPY --chown=docuseal:docuseal ./bin ./bin
|
||||
COPY --chown=docuseal:docuseal ./app ./app
|
||||
COPY --chown=docuseal:docuseal ./config ./config
|
||||
COPY --chown=docuseal:docuseal ./db/migrate ./db/migrate
|
||||
COPY --chown=docuseal:docuseal ./log ./log
|
||||
COPY --chown=docuseal:docuseal ./lib ./lib
|
||||
COPY --chown=docuseal:docuseal ./public ./public
|
||||
COPY --chown=docuseal:docuseal ./tmp ./tmp
|
||||
COPY --chown=docuseal:docuseal LICENSE README.md Rakefile config.ru .version ./
|
||||
COPY --chown=docuseal:docuseal .version ./public/version
|
||||
|
||||
COPY --from=download /fonts/GoNotoKurrent-Regular.ttf /fonts/GoNotoKurrent-Bold.ttf /fonts/DancingScript-Regular.otf /fonts/OFL.txt /fonts
|
||||
COPY --chown=docuseal:docuseal --from=download /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=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 --chown=docuseal:docuseal --from=download /model.onnx /app/tmp/model.onnx
|
||||
COPY --chown=docuseal:docuseal --from=webpack /app/public/packs ./public/packs
|
||||
|
||||
RUN ln -s /fonts /app/public/fonts
|
||||
RUN bundle exec bootsnap precompile --gemfile app/ lib/
|
||||
RUN ln -s /fonts /app/public/fonts && \
|
||||
bundle exec bootsnap precompile -j 1 --gemfile app/ lib/ && \
|
||||
chown -R docuseal:docuseal /app/tmp/cache
|
||||
|
||||
WORKDIR /data/docuseal
|
||||
ENV HOME=/home/docuseal
|
||||
ENV WORKDIR=/data/docuseal
|
||||
|
||||
EXPOSE 3000
|
||||
|
||||
@@ -2,15 +2,16 @@
|
||||
|
||||
source 'https://rubygems.org'
|
||||
|
||||
ruby '3.4.2'
|
||||
ruby '4.0.1'
|
||||
|
||||
gem 'arabic-letter-connector', require: 'arabic-letter-connector/logic'
|
||||
gem 'arabic-letter-connector', require: false
|
||||
gem 'aws-sdk-s3', require: false
|
||||
gem 'aws-sdk-secretsmanager', require: false
|
||||
gem 'azure-storage-blob', require: false
|
||||
gem 'azure-blob', require: false
|
||||
gem 'bootsnap', require: false
|
||||
gem 'cancancan'
|
||||
gem 'csv'
|
||||
gem 'csv', require: false
|
||||
gem 'csv-safe', require: false
|
||||
gem 'devise'
|
||||
gem 'devise-two-factor'
|
||||
gem 'dotenv', require: false
|
||||
@@ -20,10 +21,11 @@ gem 'faraday-follow_redirects'
|
||||
gem 'google-cloud-storage', require: false
|
||||
gem 'hexapdf'
|
||||
gem 'image_processing'
|
||||
gem 'jwt'
|
||||
gem 'jwt', require: false
|
||||
gem 'lograge'
|
||||
gem 'mysql2', require: false
|
||||
gem 'numo-narray-alt', require: false
|
||||
gem 'oj'
|
||||
gem 'onnxruntime', require: false
|
||||
gem 'pagy'
|
||||
gem 'pg', require: false
|
||||
gem 'premailer-rails'
|
||||
@@ -34,13 +36,15 @@ gem 'rails'
|
||||
gem 'rails_autolink'
|
||||
gem 'rails-i18n'
|
||||
gem 'rotp'
|
||||
gem 'rqrcode'
|
||||
gem 'rouge', require: false
|
||||
gem 'rqrcode', require: false
|
||||
gem 'ruby-vips'
|
||||
gem 'rubyXL'
|
||||
gem 'rubyXL', require: false
|
||||
gem 'shakapacker'
|
||||
gem 'sidekiq'
|
||||
gem 'sqlite3', require: false
|
||||
gem 'strip_attributes'
|
||||
gem 'trilogy', github: 'trilogy-libraries/trilogy', glob: 'contrib/ruby/*.gemspec', require: false
|
||||
gem 'turbo-rails'
|
||||
gem 'twitter_cldr', require: false
|
||||
gem 'tzinfo-data'
|
||||
|
||||
+338
-304
@@ -1,29 +1,39 @@
|
||||
GIT
|
||||
remote: https://github.com/trilogy-libraries/trilogy.git
|
||||
revision: 3963d490459df7a2b5bedb42424c3285f25eab22
|
||||
glob: contrib/ruby/*.gemspec
|
||||
specs:
|
||||
trilogy (2.10.0)
|
||||
bigdecimal
|
||||
|
||||
GEM
|
||||
remote: https://rubygems.org/
|
||||
specs:
|
||||
actioncable (8.0.1)
|
||||
actionpack (= 8.0.1)
|
||||
activesupport (= 8.0.1)
|
||||
action_text-trix (2.1.16)
|
||||
railties
|
||||
actioncable (8.1.2)
|
||||
actionpack (= 8.1.2)
|
||||
activesupport (= 8.1.2)
|
||||
nio4r (~> 2.0)
|
||||
websocket-driver (>= 0.6.1)
|
||||
zeitwerk (~> 2.6)
|
||||
actionmailbox (8.0.1)
|
||||
actionpack (= 8.0.1)
|
||||
activejob (= 8.0.1)
|
||||
activerecord (= 8.0.1)
|
||||
activestorage (= 8.0.1)
|
||||
activesupport (= 8.0.1)
|
||||
actionmailbox (8.1.2)
|
||||
actionpack (= 8.1.2)
|
||||
activejob (= 8.1.2)
|
||||
activerecord (= 8.1.2)
|
||||
activestorage (= 8.1.2)
|
||||
activesupport (= 8.1.2)
|
||||
mail (>= 2.8.0)
|
||||
actionmailer (8.0.1)
|
||||
actionpack (= 8.0.1)
|
||||
actionview (= 8.0.1)
|
||||
activejob (= 8.0.1)
|
||||
activesupport (= 8.0.1)
|
||||
actionmailer (8.1.2)
|
||||
actionpack (= 8.1.2)
|
||||
actionview (= 8.1.2)
|
||||
activejob (= 8.1.2)
|
||||
activesupport (= 8.1.2)
|
||||
mail (>= 2.8.0)
|
||||
rails-dom-testing (~> 2.2)
|
||||
actionpack (8.0.1)
|
||||
actionview (= 8.0.1)
|
||||
activesupport (= 8.0.1)
|
||||
actionpack (8.1.2)
|
||||
actionview (= 8.1.2)
|
||||
activesupport (= 8.1.2)
|
||||
nokogiri (>= 1.8.5)
|
||||
rack (>= 2.2.4)
|
||||
rack-session (>= 1.0.1)
|
||||
@@ -31,97 +41,97 @@ GEM
|
||||
rails-dom-testing (~> 2.2)
|
||||
rails-html-sanitizer (~> 1.6)
|
||||
useragent (~> 0.16)
|
||||
actiontext (8.0.1)
|
||||
actionpack (= 8.0.1)
|
||||
activerecord (= 8.0.1)
|
||||
activestorage (= 8.0.1)
|
||||
activesupport (= 8.0.1)
|
||||
actiontext (8.1.2)
|
||||
action_text-trix (~> 2.1.15)
|
||||
actionpack (= 8.1.2)
|
||||
activerecord (= 8.1.2)
|
||||
activestorage (= 8.1.2)
|
||||
activesupport (= 8.1.2)
|
||||
globalid (>= 0.6.0)
|
||||
nokogiri (>= 1.8.5)
|
||||
actionview (8.0.1)
|
||||
activesupport (= 8.0.1)
|
||||
actionview (8.1.2)
|
||||
activesupport (= 8.1.2)
|
||||
builder (~> 3.1)
|
||||
erubi (~> 1.11)
|
||||
rails-dom-testing (~> 2.2)
|
||||
rails-html-sanitizer (~> 1.6)
|
||||
activejob (8.0.1)
|
||||
activesupport (= 8.0.1)
|
||||
activejob (8.1.2)
|
||||
activesupport (= 8.1.2)
|
||||
globalid (>= 0.3.6)
|
||||
activemodel (8.0.1)
|
||||
activesupport (= 8.0.1)
|
||||
activerecord (8.0.1)
|
||||
activemodel (= 8.0.1)
|
||||
activesupport (= 8.0.1)
|
||||
activemodel (8.1.2)
|
||||
activesupport (= 8.1.2)
|
||||
activerecord (8.1.2)
|
||||
activemodel (= 8.1.2)
|
||||
activesupport (= 8.1.2)
|
||||
timeout (>= 0.4.0)
|
||||
activestorage (8.0.1)
|
||||
actionpack (= 8.0.1)
|
||||
activejob (= 8.0.1)
|
||||
activerecord (= 8.0.1)
|
||||
activesupport (= 8.0.1)
|
||||
activestorage (8.1.2)
|
||||
actionpack (= 8.1.2)
|
||||
activejob (= 8.1.2)
|
||||
activerecord (= 8.1.2)
|
||||
activesupport (= 8.1.2)
|
||||
marcel (~> 1.0)
|
||||
activesupport (8.0.1)
|
||||
activesupport (8.1.2)
|
||||
base64
|
||||
benchmark (>= 0.3)
|
||||
bigdecimal
|
||||
concurrent-ruby (~> 1.0, >= 1.3.1)
|
||||
connection_pool (>= 2.2.5)
|
||||
drb
|
||||
i18n (>= 1.6, < 2)
|
||||
json
|
||||
logger (>= 1.4.2)
|
||||
minitest (>= 5.1)
|
||||
securerandom (>= 0.3)
|
||||
tzinfo (~> 2.0, >= 2.0.5)
|
||||
uri (>= 0.13.1)
|
||||
addressable (2.8.7)
|
||||
public_suffix (>= 2.0.2, < 7.0)
|
||||
annotaterb (4.14.0)
|
||||
addressable (2.8.8)
|
||||
public_suffix (>= 2.0.2, < 8.0)
|
||||
annotaterb (4.20.0)
|
||||
activerecord (>= 6.0.0)
|
||||
activesupport (>= 6.0.0)
|
||||
arabic-letter-connector (0.1.1)
|
||||
ast (2.4.2)
|
||||
aws-eventstream (1.3.0)
|
||||
aws-partitions (1.1027.0)
|
||||
aws-sdk-core (3.214.0)
|
||||
ast (2.4.3)
|
||||
aws-eventstream (1.4.0)
|
||||
aws-partitions (1.1209.0)
|
||||
aws-sdk-core (3.241.4)
|
||||
aws-eventstream (~> 1, >= 1.3.0)
|
||||
aws-partitions (~> 1, >= 1.992.0)
|
||||
aws-sigv4 (~> 1.9)
|
||||
base64
|
||||
bigdecimal
|
||||
jmespath (~> 1, >= 1.6.1)
|
||||
aws-sdk-kms (1.96.0)
|
||||
aws-sdk-core (~> 3, >= 3.210.0)
|
||||
logger
|
||||
aws-sdk-kms (1.121.0)
|
||||
aws-sdk-core (~> 3, >= 3.241.4)
|
||||
aws-sigv4 (~> 1.5)
|
||||
aws-sdk-s3 (1.176.1)
|
||||
aws-sdk-core (~> 3, >= 3.210.0)
|
||||
aws-sdk-s3 (1.212.0)
|
||||
aws-sdk-core (~> 3, >= 3.241.4)
|
||||
aws-sdk-kms (~> 1)
|
||||
aws-sigv4 (~> 1.5)
|
||||
aws-sdk-secretsmanager (1.110.0)
|
||||
aws-sdk-core (~> 3, >= 3.210.0)
|
||||
aws-sdk-secretsmanager (1.128.0)
|
||||
aws-sdk-core (~> 3, >= 3.241.4)
|
||||
aws-sigv4 (~> 1.5)
|
||||
aws-sigv4 (1.10.1)
|
||||
aws-sigv4 (1.12.1)
|
||||
aws-eventstream (~> 1, >= 1.0.2)
|
||||
azure-storage-blob (2.0.3)
|
||||
azure-storage-common (~> 2.0)
|
||||
nokogiri (~> 1, >= 1.10.8)
|
||||
azure-storage-common (2.0.4)
|
||||
faraday (~> 1.0)
|
||||
faraday_middleware (~> 1.0, >= 1.0.0.rc1)
|
||||
net-http-persistent (~> 4.0)
|
||||
nokogiri (~> 1, >= 1.10.8)
|
||||
base64 (0.2.0)
|
||||
bcrypt (3.1.20)
|
||||
benchmark (0.4.0)
|
||||
better_html (2.1.1)
|
||||
actionview (>= 6.0)
|
||||
activesupport (>= 6.0)
|
||||
azure-blob (0.8.0)
|
||||
cgi
|
||||
rexml
|
||||
base64 (0.3.0)
|
||||
bcrypt (3.1.21)
|
||||
better_html (2.2.0)
|
||||
actionview (>= 7.0)
|
||||
activesupport (>= 7.0)
|
||||
ast (~> 2.0)
|
||||
erubi (~> 1.4)
|
||||
parser (>= 2.4)
|
||||
smart_properties
|
||||
bigdecimal (3.1.8)
|
||||
bigdecimal (4.0.1)
|
||||
bindex (0.8.1)
|
||||
bootsnap (1.18.4)
|
||||
bootsnap (1.21.1)
|
||||
msgpack (~> 1.2)
|
||||
brakeman (7.0.0)
|
||||
brakeman (7.1.2)
|
||||
racc
|
||||
builder (3.3.0)
|
||||
bullet (8.0.0)
|
||||
bullet (8.1.0)
|
||||
activesupport (>= 3.0.0)
|
||||
uniform_notifier (~> 1.11)
|
||||
camertron-eprun (1.1.1)
|
||||
@@ -135,26 +145,29 @@ GEM
|
||||
rack-test (>= 0.6.3)
|
||||
regexp_parser (>= 1.5, < 3.0)
|
||||
xpath (~> 3.2)
|
||||
cgi (0.5.1)
|
||||
childprocess (5.1.0)
|
||||
logger (~> 1.5)
|
||||
chunky_png (1.4.0)
|
||||
cldr-plurals-runtime-rb (1.1.0)
|
||||
cmdparse (3.0.7)
|
||||
coderay (1.1.3)
|
||||
concurrent-ruby (1.3.4)
|
||||
connection_pool (2.4.1)
|
||||
crack (1.0.0)
|
||||
concurrent-ruby (1.3.6)
|
||||
connection_pool (3.0.2)
|
||||
crack (1.0.1)
|
||||
bigdecimal
|
||||
rexml
|
||||
crass (1.0.6)
|
||||
css_parser (1.21.0)
|
||||
css_parser (1.21.1)
|
||||
addressable
|
||||
csv (3.3.2)
|
||||
cuprite (0.15.1)
|
||||
csv (3.3.5)
|
||||
csv-safe (3.3.1)
|
||||
csv (~> 3.0)
|
||||
cuprite (0.17)
|
||||
capybara (~> 3.0)
|
||||
ferrum (~> 0.15.0)
|
||||
date (3.4.1)
|
||||
debug (1.10.0)
|
||||
ferrum (~> 0.17.0)
|
||||
date (3.5.1)
|
||||
debug (1.11.1)
|
||||
irb (~> 1.10)
|
||||
reline (>= 0.3.8)
|
||||
declarative (0.0.20)
|
||||
@@ -164,19 +177,20 @@ GEM
|
||||
railties (>= 4.1.0)
|
||||
responders
|
||||
warden (~> 1.2.3)
|
||||
devise-two-factor (6.1.0)
|
||||
activesupport (>= 7.0, < 8.1)
|
||||
devise (~> 4.0)
|
||||
railties (>= 7.0, < 8.1)
|
||||
devise-two-factor (6.3.1)
|
||||
activesupport (>= 7.0, < 8.2)
|
||||
devise (>= 4.0, < 5.0)
|
||||
railties (>= 7.0, < 8.2)
|
||||
rotp (~> 6.0)
|
||||
diff-lcs (1.5.1)
|
||||
digest-crc (0.6.5)
|
||||
diff-lcs (1.6.2)
|
||||
digest-crc (0.7.0)
|
||||
rake (>= 12.0.0, < 14.0.0)
|
||||
docile (1.4.1)
|
||||
dotenv (3.1.7)
|
||||
drb (2.2.1)
|
||||
dotenv (3.2.0)
|
||||
drb (2.2.3)
|
||||
email_typo (0.2.3)
|
||||
erb_lint (0.7.0)
|
||||
erb (6.0.1)
|
||||
erb_lint (0.9.0)
|
||||
activesupport
|
||||
better_html (>= 2.0.1)
|
||||
parser (>= 2.7.1.4)
|
||||
@@ -184,115 +198,101 @@ GEM
|
||||
rubocop (>= 1)
|
||||
smart_properties
|
||||
erubi (1.13.1)
|
||||
factory_bot (6.5.0)
|
||||
activesupport (>= 5.0.0)
|
||||
factory_bot_rails (6.4.4)
|
||||
factory_bot (6.5.6)
|
||||
activesupport (>= 6.1.0)
|
||||
factory_bot_rails (6.5.1)
|
||||
factory_bot (~> 6.5)
|
||||
railties (>= 5.0.0)
|
||||
faker (3.5.1)
|
||||
railties (>= 6.1.0)
|
||||
faker (3.6.0)
|
||||
i18n (>= 1.8.11, < 2)
|
||||
faraday (1.10.4)
|
||||
faraday-em_http (~> 1.0)
|
||||
faraday-em_synchrony (~> 1.0)
|
||||
faraday-excon (~> 1.1)
|
||||
faraday-httpclient (~> 1.0)
|
||||
faraday-multipart (~> 1.0)
|
||||
faraday-net_http (~> 1.0)
|
||||
faraday-net_http_persistent (~> 1.0)
|
||||
faraday-patron (~> 1.0)
|
||||
faraday-rack (~> 1.0)
|
||||
faraday-retry (~> 1.0)
|
||||
ruby2_keywords (>= 0.0.4)
|
||||
faraday-em_http (1.0.0)
|
||||
faraday-em_synchrony (1.0.0)
|
||||
faraday-excon (1.1.0)
|
||||
faraday-follow_redirects (0.3.0)
|
||||
faraday (2.14.0)
|
||||
faraday-net_http (>= 2.0, < 3.5)
|
||||
json
|
||||
logger
|
||||
faraday-follow_redirects (0.5.0)
|
||||
faraday (>= 1, < 3)
|
||||
faraday-httpclient (1.0.1)
|
||||
faraday-multipart (1.1.0)
|
||||
multipart-post (~> 2.0)
|
||||
faraday-net_http (1.0.2)
|
||||
faraday-net_http_persistent (1.2.0)
|
||||
faraday-patron (1.0.0)
|
||||
faraday-rack (1.0.0)
|
||||
faraday-retry (1.0.3)
|
||||
faraday_middleware (1.2.1)
|
||||
faraday (~> 1.0)
|
||||
ferrum (0.15)
|
||||
faraday-net_http (3.4.2)
|
||||
net-http (~> 0.5)
|
||||
ferrum (0.17.1)
|
||||
addressable (~> 2.5)
|
||||
base64 (~> 0.2)
|
||||
concurrent-ruby (~> 1.1)
|
||||
webrick (~> 1.7)
|
||||
websocket-driver (~> 0.7)
|
||||
ffi (1.17.1)
|
||||
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)
|
||||
ffi (1.17.3)
|
||||
ffi (1.17.3-aarch64-linux-gnu)
|
||||
ffi (1.17.3-aarch64-linux-musl)
|
||||
ffi (1.17.3-arm64-darwin)
|
||||
ffi (1.17.3-x86_64-linux-gnu)
|
||||
ffi (1.17.3-x86_64-linux-musl)
|
||||
foreman (0.90.0)
|
||||
thor (~> 1.4)
|
||||
geom2d (0.4.1)
|
||||
globalid (1.2.1)
|
||||
globalid (1.3.0)
|
||||
activesupport (>= 6.1)
|
||||
google-apis-core (0.15.1)
|
||||
addressable (~> 2.5, >= 2.5.1)
|
||||
googleauth (~> 1.9)
|
||||
httpclient (>= 2.8.3, < 3.a)
|
||||
mini_mime (~> 1.0)
|
||||
mutex_m
|
||||
google-apis-core (1.0.2)
|
||||
addressable (~> 2.8, >= 2.8.7)
|
||||
faraday (~> 2.13)
|
||||
faraday-follow_redirects (~> 0.3)
|
||||
googleauth (~> 1.14)
|
||||
mini_mime (~> 1.1)
|
||||
representable (~> 3.0)
|
||||
retriable (>= 2.0, < 4.a)
|
||||
google-apis-iamcredentials_v1 (0.22.0)
|
||||
retriable (~> 3.1)
|
||||
google-apis-iamcredentials_v1 (0.26.0)
|
||||
google-apis-core (>= 0.15.0, < 2.a)
|
||||
google-apis-storage_v1 (0.49.0)
|
||||
google-apis-storage_v1 (0.59.0)
|
||||
google-apis-core (>= 0.15.0, < 2.a)
|
||||
google-cloud-core (1.7.1)
|
||||
google-cloud-core (1.8.0)
|
||||
google-cloud-env (>= 1.0, < 3.a)
|
||||
google-cloud-errors (~> 1.0)
|
||||
google-cloud-env (2.2.1)
|
||||
google-cloud-env (2.3.1)
|
||||
base64 (~> 0.2)
|
||||
faraday (>= 1.0, < 3.a)
|
||||
google-cloud-errors (1.4.0)
|
||||
google-cloud-storage (1.54.0)
|
||||
google-cloud-errors (1.5.0)
|
||||
google-cloud-storage (1.58.0)
|
||||
addressable (~> 2.8)
|
||||
digest-crc (~> 0.4)
|
||||
google-apis-core (~> 0.13)
|
||||
google-apis-core (>= 0.18, < 2)
|
||||
google-apis-iamcredentials_v1 (~> 0.18)
|
||||
google-apis-storage_v1 (~> 0.38)
|
||||
google-apis-storage_v1 (>= 0.42)
|
||||
google-cloud-core (~> 1.6)
|
||||
googleauth (~> 1.9)
|
||||
mini_mime (~> 1.0)
|
||||
google-logging-utils (0.1.0)
|
||||
googleauth (1.12.2)
|
||||
google-logging-utils (0.2.0)
|
||||
googleauth (1.16.1)
|
||||
faraday (>= 1.0, < 3.a)
|
||||
google-cloud-env (~> 2.2)
|
||||
google-logging-utils (~> 0.1)
|
||||
jwt (>= 1.4, < 3.0)
|
||||
jwt (>= 1.4, < 4.0)
|
||||
multi_json (~> 1.11)
|
||||
os (>= 0.9, < 2.0)
|
||||
signet (>= 0.16, < 2.a)
|
||||
hashdiff (1.1.2)
|
||||
hexapdf (1.0.3)
|
||||
hashdiff (1.2.1)
|
||||
hexapdf (1.5.0)
|
||||
cmdparse (~> 3.0, >= 3.0.3)
|
||||
geom2d (~> 0.4, >= 0.4.1)
|
||||
openssl (>= 2.2.1)
|
||||
htmlentities (4.3.4)
|
||||
httpclient (2.8.3)
|
||||
i18n (1.14.6)
|
||||
strscan (>= 3.1.2)
|
||||
htmlentities (4.4.2)
|
||||
i18n (1.14.8)
|
||||
concurrent-ruby (~> 1.0)
|
||||
image_processing (1.13.0)
|
||||
mini_magick (>= 4.9.5, < 5)
|
||||
image_processing (1.14.0)
|
||||
mini_magick (>= 4.9.5, < 6)
|
||||
ruby-vips (>= 2.0.17, < 3)
|
||||
io-console (0.8.0)
|
||||
irb (1.14.3)
|
||||
io-console (0.8.2)
|
||||
irb (1.16.0)
|
||||
pp (>= 0.6.0)
|
||||
rdoc (>= 4.0.0)
|
||||
reline (>= 0.4.2)
|
||||
jmespath (1.6.2)
|
||||
json (2.9.1)
|
||||
jwt (2.9.3)
|
||||
json (2.18.0)
|
||||
jwt (3.1.2)
|
||||
base64
|
||||
language_server-protocol (3.17.0.3)
|
||||
launchy (3.0.1)
|
||||
language_server-protocol (3.17.0.5)
|
||||
launchy (3.1.1)
|
||||
addressable (~> 2.8)
|
||||
childprocess (~> 5.0)
|
||||
logger (~> 1.6)
|
||||
letter_opener (1.10.0)
|
||||
launchy (>= 2.2, < 4)
|
||||
letter_opener_web (3.0.0)
|
||||
@@ -300,71 +300,86 @@ GEM
|
||||
letter_opener (~> 1.9)
|
||||
railties (>= 6.1)
|
||||
rexml
|
||||
logger (1.6.4)
|
||||
lint_roller (1.1.0)
|
||||
logger (1.7.0)
|
||||
lograge (0.14.0)
|
||||
actionpack (>= 4)
|
||||
activesupport (>= 4)
|
||||
railties (>= 4)
|
||||
request_store (~> 1.0)
|
||||
loofah (2.23.1)
|
||||
loofah (2.25.0)
|
||||
crass (~> 1.0.2)
|
||||
nokogiri (>= 1.12.0)
|
||||
mail (2.8.1)
|
||||
mail (2.9.0)
|
||||
logger
|
||||
mini_mime (>= 0.1.1)
|
||||
net-imap
|
||||
net-pop
|
||||
net-smtp
|
||||
marcel (1.0.4)
|
||||
matrix (0.4.2)
|
||||
marcel (1.1.0)
|
||||
matrix (0.4.3)
|
||||
method_source (1.1.0)
|
||||
mini_magick (4.13.2)
|
||||
mini_magick (5.3.1)
|
||||
logger
|
||||
mini_mime (1.1.5)
|
||||
mini_portile2 (2.8.8)
|
||||
minitest (5.25.4)
|
||||
msgpack (1.7.5)
|
||||
multi_json (1.15.0)
|
||||
multipart-post (2.4.1)
|
||||
mutex_m (0.3.0)
|
||||
mysql2 (0.5.6)
|
||||
net-http-persistent (4.0.5)
|
||||
connection_pool (~> 2.2)
|
||||
net-imap (0.5.6)
|
||||
minitest (6.0.1)
|
||||
prism (~> 1.5)
|
||||
msgpack (1.8.0)
|
||||
multi_json (1.19.1)
|
||||
net-http (0.9.1)
|
||||
uri (>= 0.11.1)
|
||||
net-imap (0.6.2)
|
||||
date
|
||||
net-protocol
|
||||
net-pop (0.1.2)
|
||||
net-protocol
|
||||
net-protocol (0.2.2)
|
||||
timeout
|
||||
net-smtp (0.5.0)
|
||||
net-smtp (0.5.1)
|
||||
net-protocol
|
||||
nio4r (2.7.4)
|
||||
nokogiri (1.18.8)
|
||||
mini_portile2 (~> 2.8.2)
|
||||
nio4r (2.7.5)
|
||||
nokogiri (1.19.0-aarch64-linux-gnu)
|
||||
racc (~> 1.4)
|
||||
nokogiri (1.18.8-aarch64-linux-gnu)
|
||||
nokogiri (1.19.0-aarch64-linux-musl)
|
||||
racc (~> 1.4)
|
||||
nokogiri (1.18.8-aarch64-linux-musl)
|
||||
nokogiri (1.19.0-arm64-darwin)
|
||||
racc (~> 1.4)
|
||||
nokogiri (1.18.8-arm64-darwin)
|
||||
nokogiri (1.19.0-x86_64-linux-gnu)
|
||||
racc (~> 1.4)
|
||||
nokogiri (1.18.8-x86_64-linux-gnu)
|
||||
nokogiri (1.19.0-x86_64-linux-musl)
|
||||
racc (~> 1.4)
|
||||
nokogiri (1.18.8-x86_64-linux-musl)
|
||||
racc (~> 1.4)
|
||||
oj (3.16.8)
|
||||
numo-narray-alt (0.9.13)
|
||||
oj (3.16.13)
|
||||
bigdecimal (>= 3.0)
|
||||
ostruct (>= 0.2)
|
||||
openssl (3.3.0)
|
||||
onnxruntime (0.10.1)
|
||||
ffi
|
||||
onnxruntime (0.10.1-aarch64-linux)
|
||||
ffi
|
||||
onnxruntime (0.10.1-arm64-darwin)
|
||||
ffi
|
||||
onnxruntime (0.10.1-x86_64-linux)
|
||||
ffi
|
||||
openssl (4.0.0)
|
||||
orm_adapter (0.5.0)
|
||||
os (1.1.4)
|
||||
ostruct (0.6.1)
|
||||
package_json (0.1.0)
|
||||
pagy (9.3.3)
|
||||
parallel (1.26.3)
|
||||
parser (3.3.6.0)
|
||||
ostruct (0.6.3)
|
||||
package_json (0.2.0)
|
||||
pagy (43.2.8)
|
||||
json
|
||||
yaml
|
||||
parallel (1.27.0)
|
||||
parser (3.3.10.1)
|
||||
ast (~> 2.4.1)
|
||||
racc
|
||||
pg (1.5.9)
|
||||
pg (1.6.3)
|
||||
pg (1.6.3-aarch64-linux)
|
||||
pg (1.6.3-aarch64-linux-musl)
|
||||
pg (1.6.3-arm64-darwin)
|
||||
pg (1.6.3-x86_64-linux)
|
||||
pg (1.6.3-x86_64-linux-musl)
|
||||
pp (0.6.3)
|
||||
prettyprint
|
||||
premailer (1.27.0)
|
||||
addressable
|
||||
css_parser (>= 1.19.0)
|
||||
@@ -373,73 +388,80 @@ GEM
|
||||
actionmailer (>= 3)
|
||||
net-smtp
|
||||
premailer (~> 1.7, >= 1.7.9)
|
||||
pretender (0.5.0)
|
||||
actionpack (>= 6.1)
|
||||
pry (0.15.0)
|
||||
pretender (0.6.0)
|
||||
actionpack (>= 7.1)
|
||||
prettyprint (0.2.0)
|
||||
prism (1.8.0)
|
||||
pry (0.16.0)
|
||||
coderay (~> 1.1)
|
||||
method_source (~> 1.0)
|
||||
reline (>= 0.6.0)
|
||||
pry-rails (0.3.11)
|
||||
pry (>= 0.13.0)
|
||||
psych (5.2.2)
|
||||
psych (5.3.1)
|
||||
date
|
||||
stringio
|
||||
public_suffix (6.0.1)
|
||||
puma (6.5.0)
|
||||
public_suffix (7.0.2)
|
||||
puma (7.2.0)
|
||||
nio4r (~> 2.0)
|
||||
racc (1.8.1)
|
||||
rack (3.1.16)
|
||||
rack (3.2.4)
|
||||
rack-proxy (0.7.7)
|
||||
rack
|
||||
rack-session (2.0.0)
|
||||
rack-session (2.1.1)
|
||||
base64 (>= 0.1.0)
|
||||
rack (>= 3.0.0)
|
||||
rack-test (2.1.0)
|
||||
rack-test (2.2.0)
|
||||
rack (>= 1.3)
|
||||
rackup (2.2.1)
|
||||
rackup (2.3.1)
|
||||
rack (>= 3)
|
||||
rails (8.0.1)
|
||||
actioncable (= 8.0.1)
|
||||
actionmailbox (= 8.0.1)
|
||||
actionmailer (= 8.0.1)
|
||||
actionpack (= 8.0.1)
|
||||
actiontext (= 8.0.1)
|
||||
actionview (= 8.0.1)
|
||||
activejob (= 8.0.1)
|
||||
activemodel (= 8.0.1)
|
||||
activerecord (= 8.0.1)
|
||||
activestorage (= 8.0.1)
|
||||
activesupport (= 8.0.1)
|
||||
rails (8.1.2)
|
||||
actioncable (= 8.1.2)
|
||||
actionmailbox (= 8.1.2)
|
||||
actionmailer (= 8.1.2)
|
||||
actionpack (= 8.1.2)
|
||||
actiontext (= 8.1.2)
|
||||
actionview (= 8.1.2)
|
||||
activejob (= 8.1.2)
|
||||
activemodel (= 8.1.2)
|
||||
activerecord (= 8.1.2)
|
||||
activestorage (= 8.1.2)
|
||||
activesupport (= 8.1.2)
|
||||
bundler (>= 1.15.0)
|
||||
railties (= 8.0.1)
|
||||
rails-dom-testing (2.2.0)
|
||||
railties (= 8.1.2)
|
||||
rails-dom-testing (2.3.0)
|
||||
activesupport (>= 5.0.0)
|
||||
minitest
|
||||
nokogiri (>= 1.6)
|
||||
rails-html-sanitizer (1.6.2)
|
||||
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)
|
||||
rails-i18n (8.0.1)
|
||||
rails-i18n (8.1.0)
|
||||
i18n (>= 0.7, < 2)
|
||||
railties (>= 8.0.0, < 9)
|
||||
rails_autolink (1.1.8)
|
||||
actionview (> 3.1)
|
||||
activesupport (> 3.1)
|
||||
railties (> 3.1)
|
||||
railties (8.0.1)
|
||||
actionpack (= 8.0.1)
|
||||
activesupport (= 8.0.1)
|
||||
railties (8.1.2)
|
||||
actionpack (= 8.1.2)
|
||||
activesupport (= 8.1.2)
|
||||
irb (~> 1.13)
|
||||
rackup (>= 1.0.0)
|
||||
rake (>= 12.2)
|
||||
thor (~> 1.0, >= 1.2.2)
|
||||
tsort (>= 0.2)
|
||||
zeitwerk (~> 2.6)
|
||||
rainbow (3.1.1)
|
||||
rake (13.2.1)
|
||||
rdoc (6.10.0)
|
||||
rake (13.3.1)
|
||||
rdoc (7.1.0)
|
||||
erb
|
||||
psych (>= 4.0.0)
|
||||
redis-client (0.23.0)
|
||||
tsort
|
||||
redis-client (0.26.4)
|
||||
connection_pool
|
||||
regexp_parser (2.9.3)
|
||||
reline (0.6.0)
|
||||
regexp_parser (2.11.3)
|
||||
reline (0.6.3)
|
||||
io-console (~> 0.5)
|
||||
representable (3.2.0)
|
||||
declarative (< 0.1.0)
|
||||
@@ -447,119 +469,126 @@ GEM
|
||||
uber (< 0.2.0)
|
||||
request_store (1.7.0)
|
||||
rack (>= 1.4)
|
||||
responders (3.1.1)
|
||||
actionpack (>= 5.2)
|
||||
railties (>= 5.2)
|
||||
retriable (3.1.2)
|
||||
rexml (3.4.0)
|
||||
rotp (6.3.0)
|
||||
rqrcode (2.2.0)
|
||||
chunky_png (~> 1.0)
|
||||
rqrcode_core (~> 1.0)
|
||||
rqrcode_core (1.2.0)
|
||||
rspec-core (3.13.2)
|
||||
rspec-support (~> 3.13.0)
|
||||
rspec-expectations (3.13.3)
|
||||
diff-lcs (>= 1.2.0, < 2.0)
|
||||
rspec-support (~> 3.13.0)
|
||||
rspec-mocks (3.13.2)
|
||||
diff-lcs (>= 1.2.0, < 2.0)
|
||||
rspec-support (~> 3.13.0)
|
||||
rspec-rails (7.1.0)
|
||||
responders (3.2.0)
|
||||
actionpack (>= 7.0)
|
||||
activesupport (>= 7.0)
|
||||
railties (>= 7.0)
|
||||
retriable (3.1.2)
|
||||
rexml (3.4.4)
|
||||
rotp (6.3.0)
|
||||
rouge (4.7.0)
|
||||
rqrcode (3.2.0)
|
||||
chunky_png (~> 1.0)
|
||||
rqrcode_core (~> 2.0)
|
||||
rqrcode_core (2.1.0)
|
||||
rspec-core (3.13.6)
|
||||
rspec-support (~> 3.13.0)
|
||||
rspec-expectations (3.13.5)
|
||||
diff-lcs (>= 1.2.0, < 2.0)
|
||||
rspec-support (~> 3.13.0)
|
||||
rspec-mocks (3.13.7)
|
||||
diff-lcs (>= 1.2.0, < 2.0)
|
||||
rspec-support (~> 3.13.0)
|
||||
rspec-rails (8.0.2)
|
||||
actionpack (>= 7.2)
|
||||
activesupport (>= 7.2)
|
||||
railties (>= 7.2)
|
||||
rspec-core (~> 3.13)
|
||||
rspec-expectations (~> 3.13)
|
||||
rspec-mocks (~> 3.13)
|
||||
rspec-support (~> 3.13)
|
||||
rspec-support (3.13.2)
|
||||
rubocop (1.69.2)
|
||||
rspec-support (3.13.6)
|
||||
rubocop (1.82.1)
|
||||
json (~> 2.3)
|
||||
language_server-protocol (>= 3.17.0)
|
||||
language_server-protocol (~> 3.17.0.2)
|
||||
lint_roller (~> 1.1.0)
|
||||
parallel (~> 1.10)
|
||||
parser (>= 3.3.0.2)
|
||||
rainbow (>= 2.2.2, < 4.0)
|
||||
regexp_parser (>= 2.9.3, < 3.0)
|
||||
rubocop-ast (>= 1.36.2, < 2.0)
|
||||
rubocop-ast (>= 1.48.0, < 2.0)
|
||||
ruby-progressbar (~> 1.7)
|
||||
unicode-display_width (>= 2.4.0, < 4.0)
|
||||
rubocop-ast (1.37.0)
|
||||
parser (>= 3.3.1.0)
|
||||
rubocop-performance (1.23.0)
|
||||
rubocop (>= 1.48.1, < 2.0)
|
||||
rubocop-ast (>= 1.31.1, < 2.0)
|
||||
rubocop-rails (2.27.0)
|
||||
rubocop-ast (1.49.0)
|
||||
parser (>= 3.3.7.2)
|
||||
prism (~> 1.7)
|
||||
rubocop-performance (1.26.1)
|
||||
lint_roller (~> 1.1)
|
||||
rubocop (>= 1.75.0, < 2.0)
|
||||
rubocop-ast (>= 1.47.1, < 2.0)
|
||||
rubocop-rails (2.34.3)
|
||||
activesupport (>= 4.2.0)
|
||||
lint_roller (~> 1.1)
|
||||
rack (>= 1.1)
|
||||
rubocop (>= 1.52.0, < 2.0)
|
||||
rubocop-ast (>= 1.31.1, < 2.0)
|
||||
rubocop-rspec (3.3.0)
|
||||
rubocop (~> 1.61)
|
||||
rubocop (>= 1.75.0, < 2.0)
|
||||
rubocop-ast (>= 1.44.0, < 2.0)
|
||||
rubocop-rspec (3.9.0)
|
||||
lint_roller (~> 1.1)
|
||||
rubocop (~> 1.81)
|
||||
ruby-progressbar (1.13.0)
|
||||
ruby-vips (2.2.2)
|
||||
ruby-vips (2.3.0)
|
||||
ffi (~> 1.12)
|
||||
logger
|
||||
ruby2_keywords (0.0.5)
|
||||
rubyXL (3.4.33)
|
||||
rubyXL (3.4.35)
|
||||
nokogiri (>= 1.10.8)
|
||||
rubyzip (>= 1.3.0)
|
||||
rubyzip (2.3.2)
|
||||
rubyzip (>= 3.2.2)
|
||||
rubyzip (3.2.2)
|
||||
securerandom (0.4.1)
|
||||
semantic_range (3.1.0)
|
||||
shakapacker (8.0.2)
|
||||
shakapacker (9.5.0)
|
||||
activesupport (>= 5.2)
|
||||
package_json
|
||||
rack-proxy (>= 0.6.1)
|
||||
railties (>= 5.2)
|
||||
semantic_range (>= 2.3.0)
|
||||
sidekiq (7.3.7)
|
||||
connection_pool (>= 2.3.0)
|
||||
logger
|
||||
rack (>= 2.2.4)
|
||||
redis-client (>= 0.22.2)
|
||||
signet (0.19.0)
|
||||
sidekiq (8.1.0)
|
||||
connection_pool (>= 3.0.0)
|
||||
json (>= 2.16.0)
|
||||
logger (>= 1.7.0)
|
||||
rack (>= 3.2.0)
|
||||
redis-client (>= 0.26.0)
|
||||
signet (0.21.0)
|
||||
addressable (~> 2.8)
|
||||
faraday (>= 0.17.5, < 3.a)
|
||||
jwt (>= 1.5, < 3.0)
|
||||
jwt (>= 1.5, < 4.0)
|
||||
multi_json (~> 1.10)
|
||||
simplecov (0.22.0)
|
||||
docile (~> 1.1)
|
||||
simplecov-html (~> 0.11)
|
||||
simplecov_json_formatter (~> 0.1)
|
||||
simplecov-html (0.13.1)
|
||||
simplecov-html (0.13.2)
|
||||
simplecov_json_formatter (0.1.4)
|
||||
smart_properties (1.17.0)
|
||||
sqlite3 (2.5.0)
|
||||
mini_portile2 (~> 2.8.0)
|
||||
sqlite3 (2.5.0-aarch64-linux-gnu)
|
||||
sqlite3 (2.5.0-aarch64-linux-musl)
|
||||
sqlite3 (2.5.0-arm64-darwin)
|
||||
sqlite3 (2.5.0-x86_64-linux-gnu)
|
||||
sqlite3 (2.5.0-x86_64-linux-musl)
|
||||
stringio (3.1.2)
|
||||
strip_attributes (1.14.1)
|
||||
sqlite3 (2.9.0-aarch64-linux-gnu)
|
||||
sqlite3 (2.9.0-aarch64-linux-musl)
|
||||
sqlite3 (2.9.0-arm64-darwin)
|
||||
sqlite3 (2.9.0-x86_64-linux-gnu)
|
||||
sqlite3 (2.9.0-x86_64-linux-musl)
|
||||
stringio (3.2.0)
|
||||
strip_attributes (2.0.1)
|
||||
activemodel (>= 3.0, < 9.0)
|
||||
thor (1.3.2)
|
||||
timeout (0.4.3)
|
||||
strscan (3.1.7)
|
||||
thor (1.5.0)
|
||||
timeout (0.6.0)
|
||||
trailblazer-option (0.1.2)
|
||||
turbo-rails (2.0.11)
|
||||
actionpack (>= 6.0.0)
|
||||
railties (>= 6.0.0)
|
||||
twitter_cldr (6.12.1)
|
||||
tsort (0.2.0)
|
||||
turbo-rails (2.0.21)
|
||||
actionpack (>= 7.1.0)
|
||||
railties (>= 7.1.0)
|
||||
twitter_cldr (6.14.0)
|
||||
base64
|
||||
camertron-eprun
|
||||
cldr-plurals-runtime-rb (~> 1.1)
|
||||
tzinfo
|
||||
tzinfo (2.0.6)
|
||||
concurrent-ruby (~> 1.0)
|
||||
tzinfo-data (1.2024.2)
|
||||
tzinfo-data (1.2025.3)
|
||||
tzinfo (>= 1.0.0)
|
||||
uber (0.1.0)
|
||||
unicode-display_width (3.1.2)
|
||||
unicode-emoji (~> 4.0, >= 4.0.4)
|
||||
unicode-emoji (4.0.4)
|
||||
uniform_notifier (1.16.0)
|
||||
uri (1.0.3)
|
||||
unicode-display_width (3.2.0)
|
||||
unicode-emoji (~> 4.1)
|
||||
unicode-emoji (4.2.0)
|
||||
uniform_notifier (1.18.0)
|
||||
uri (1.1.1)
|
||||
useragent (0.16.11)
|
||||
warden (1.2.9)
|
||||
rack (>= 2.0.9)
|
||||
@@ -568,23 +597,24 @@ GEM
|
||||
activemodel (>= 6.0.0)
|
||||
bindex (>= 0.4.0)
|
||||
railties (>= 6.0.0)
|
||||
webmock (3.24.0)
|
||||
webmock (3.26.1)
|
||||
addressable (>= 2.8.0)
|
||||
crack (>= 0.3.2)
|
||||
hashdiff (>= 0.4.0, < 2.0.0)
|
||||
webrick (1.9.1)
|
||||
websocket-driver (0.7.6)
|
||||
webrick (1.9.2)
|
||||
websocket-driver (0.8.0)
|
||||
base64
|
||||
websocket-extensions (>= 0.1.0)
|
||||
websocket-extensions (0.1.5)
|
||||
xpath (3.2.0)
|
||||
nokogiri (~> 1.8)
|
||||
zeitwerk (2.7.1)
|
||||
yaml (0.4.0)
|
||||
zeitwerk (2.7.4)
|
||||
|
||||
PLATFORMS
|
||||
aarch64-linux
|
||||
aarch64-linux-musl
|
||||
arm64-darwin
|
||||
ruby
|
||||
x86_64-linux
|
||||
x86_64-linux-musl
|
||||
|
||||
@@ -593,7 +623,7 @@ DEPENDENCIES
|
||||
arabic-letter-connector
|
||||
aws-sdk-s3
|
||||
aws-sdk-secretsmanager
|
||||
azure-storage-blob
|
||||
azure-blob
|
||||
better_html
|
||||
bootsnap
|
||||
brakeman
|
||||
@@ -601,6 +631,7 @@ DEPENDENCIES
|
||||
cancancan
|
||||
capybara
|
||||
csv
|
||||
csv-safe
|
||||
cuprite
|
||||
debug
|
||||
devise
|
||||
@@ -619,8 +650,9 @@ DEPENDENCIES
|
||||
jwt
|
||||
letter_opener_web
|
||||
lograge
|
||||
mysql2
|
||||
numo-narray-alt
|
||||
oj
|
||||
onnxruntime
|
||||
pagy
|
||||
pg
|
||||
premailer-rails
|
||||
@@ -632,6 +664,7 @@ DEPENDENCIES
|
||||
rails-i18n
|
||||
rails_autolink
|
||||
rotp
|
||||
rouge
|
||||
rqrcode
|
||||
rspec-rails
|
||||
rubocop
|
||||
@@ -645,6 +678,7 @@ DEPENDENCIES
|
||||
simplecov
|
||||
sqlite3
|
||||
strip_attributes
|
||||
trilogy!
|
||||
turbo-rails
|
||||
twitter_cldr
|
||||
tzinfo-data
|
||||
@@ -652,7 +686,7 @@ DEPENDENCIES
|
||||
webmock
|
||||
|
||||
RUBY VERSION
|
||||
ruby 3.4.2p28
|
||||
ruby 4.0.1
|
||||
|
||||
BUNDLED WITH
|
||||
2.5.3
|
||||
4.0.3
|
||||
|
||||
@@ -42,7 +42,7 @@ DocuSeal is an open source platform that provides secure and efficient digital d
|
||||
- PDF signature verification
|
||||
- Users management
|
||||
- Mobile-optimized
|
||||
- 6 UI languages with signing available in 14 languages
|
||||
- 7 UI languages with signing available in 14 languages
|
||||
- API and Webhooks for integrations
|
||||
- Easy to deploy in minutes
|
||||
|
||||
|
||||
@@ -15,9 +15,11 @@ class AccountConfigsController < ApplicationController
|
||||
AccountConfig::ESIGNING_PREFERENCE_KEY,
|
||||
AccountConfig::FORM_WITH_CONFETTI_KEY,
|
||||
AccountConfig::DOWNLOAD_LINKS_AUTH_KEY,
|
||||
AccountConfig::DOWNLOAD_LINKS_EXPIRE_KEY,
|
||||
AccountConfig::FORCE_SSO_AUTH_KEY,
|
||||
AccountConfig::FLATTEN_RESULT_PDF_KEY,
|
||||
AccountConfig::ENFORCE_SIGNING_ORDER_KEY,
|
||||
AccountConfig::WITH_FILE_LINKS_KEY,
|
||||
AccountConfig::WITH_SIGNATURE_ID,
|
||||
AccountConfig::COMBINE_PDF_RESULT_KEY,
|
||||
AccountConfig::REQUIRE_SIGNING_REASON_KEY,
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class AccountCustomFieldsController < ApplicationController
|
||||
before_action :load_account_config, only: :create
|
||||
|
||||
def create
|
||||
authorize!(:create, Template)
|
||||
|
||||
@account_config.update!(account_config_params)
|
||||
|
||||
render json: @account_config.value
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def load_account_config
|
||||
@account_config =
|
||||
AccountConfig.find_or_initialize_by(account: current_account, key: AccountConfig::TEMPLATE_CUSTOM_FIELDS_KEY)
|
||||
end
|
||||
|
||||
def account_config_params
|
||||
params.permit(
|
||||
value: [[:uuid, :name, :type,
|
||||
:required, :readonly, :default_value,
|
||||
:title, :description,
|
||||
{ preferences: {},
|
||||
default_value: [],
|
||||
options: [%i[value uuid]],
|
||||
validation: %i[message pattern min max step],
|
||||
areas: [%i[x y w h cell_w option_uuid]] }]]
|
||||
)
|
||||
end
|
||||
end
|
||||
@@ -8,7 +8,8 @@ class AccountsController < ApplicationController
|
||||
'es-ES' => 'Español',
|
||||
'pt-PT' => 'Português',
|
||||
'de-DE' => 'Deutsch',
|
||||
'it-IT' => 'Italiano'
|
||||
'it-IT' => 'Italiano',
|
||||
'nl-NL' => 'Nederlands'
|
||||
}.freeze
|
||||
|
||||
before_action :load_account
|
||||
@@ -27,7 +28,7 @@ class AccountsController < ApplicationController
|
||||
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'))
|
||||
|
||||
return render :show, status: :unprocessable_entity
|
||||
return render :show, status: :unprocessable_content
|
||||
end
|
||||
|
||||
@encrypted_config.save!
|
||||
@@ -39,13 +40,15 @@ class AccountsController < ApplicationController
|
||||
redirect_to settings_account_path, notice: I18n.t('account_information_has_been_updated')
|
||||
end
|
||||
rescue ActiveRecord::RecordInvalid
|
||||
render :show, status: :unprocessable_entity
|
||||
render :show, status: :unprocessable_content
|
||||
end
|
||||
|
||||
def destroy
|
||||
authorize!(:manage, current_account)
|
||||
|
||||
true_user.skip_reconfirmation!
|
||||
true_user.update!(locked_at: Time.current, email: true_user.email.sub('@', '+removed@'))
|
||||
true_user.account.update!(archived_at: Time.current)
|
||||
|
||||
# rubocop:disable Layout/LineLength
|
||||
render turbo_stream: turbo_stream.replace(
|
||||
|
||||
@@ -13,7 +13,7 @@ module Api
|
||||
def show
|
||||
blob_uuid, purp, exp = ApplicationRecord.signed_id_verifier.verified(params[:signed_uuid])
|
||||
|
||||
if blob_uuid.blank? || (purp.present? && purp != 'blob') || (exp && exp < Time.current.to_i)
|
||||
if blob_uuid.blank? || purp != 'blob'
|
||||
Rollbar.error('Blob not found') if defined?(Rollbar)
|
||||
|
||||
return head :not_found
|
||||
@@ -24,8 +24,9 @@ module Api
|
||||
attachment = blob.attachments.take
|
||||
|
||||
@record = attachment.record
|
||||
@record = @record.record if @record.is_a?(ActiveStorage::Attachment)
|
||||
|
||||
authorization_check!(attachment) if exp.blank?
|
||||
authorization_check!(attachment, @record, exp)
|
||||
|
||||
if request.headers['Range'].present?
|
||||
send_blob_byte_range_data blob, request.headers['Range']
|
||||
@@ -41,16 +42,22 @@ module Api
|
||||
|
||||
private
|
||||
|
||||
def authorization_check!(attachment)
|
||||
is_authorized = attachment.name.in?(%w[logo preview_images]) ||
|
||||
(current_user && attachment.record.account.id == current_user.account_id) ||
|
||||
(current_user && !Docuseal.multitenant? && current_user.role == 'superadmin') ||
|
||||
!attachment.record.account.account_configs
|
||||
.find_or_initialize_by(key: AccountConfig::DOWNLOAD_LINKS_AUTH_KEY).value
|
||||
def authorization_check!(attachment, record, exp)
|
||||
return if attachment.name == 'logo'
|
||||
return if exp.to_i >= Time.current.to_i
|
||||
return if current_user && current_ability.can?(:read, record)
|
||||
|
||||
return if is_authorized
|
||||
if exp.blank?
|
||||
configs = record.account.account_configs.where(key: [AccountConfig::DOWNLOAD_LINKS_AUTH_KEY,
|
||||
AccountConfig::DOWNLOAD_LINKS_EXPIRE_KEY])
|
||||
|
||||
Rollbar.error('Blob aunauthorized') if defined?(Rollbar)
|
||||
require_auth = configs.any? { |c| c.key == AccountConfig::DOWNLOAD_LINKS_AUTH_KEY && c.value }
|
||||
require_ttl = configs.none? { |c| c.key == AccountConfig::DOWNLOAD_LINKS_EXPIRE_KEY && c.value == false }
|
||||
|
||||
return if !require_ttl && !require_auth
|
||||
end
|
||||
|
||||
Rollbar.error('Blob unauthorized') if defined?(Rollbar)
|
||||
|
||||
raise CanCan::AccessDenied
|
||||
end
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
module Api
|
||||
class ApiBaseController < ActionController::API
|
||||
include ActiveStorage::SetCurrent
|
||||
include Pagy::Backend
|
||||
include Pagy::Method
|
||||
|
||||
DEFAULT_LIMIT = 10
|
||||
MAX_LIMIT = 100
|
||||
@@ -16,7 +16,7 @@ module Api
|
||||
check_authorization
|
||||
|
||||
rescue_from Params::BaseValidator::InvalidParameterError do |e|
|
||||
render json: { error: e.message }, status: :unprocessable_entity
|
||||
render json: { error: e.message }, status: :unprocessable_content
|
||||
end
|
||||
|
||||
rescue_from RateLimit::LimitApproached do |e|
|
||||
@@ -33,7 +33,7 @@ module Api
|
||||
rescue_from JSON::ParserError do |e|
|
||||
Rollbar.warning(e) if defined?(Rollbar)
|
||||
|
||||
render json: { error: "JSON parse error: #{e.message}" }, status: :unprocessable_entity
|
||||
render json: { error: "JSON parse error: #{e.message}" }, status: :unprocessable_content
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@@ -10,6 +10,23 @@ module Api
|
||||
def create
|
||||
submitter = Submitter.find_by!(slug: params[:submitter_slug])
|
||||
|
||||
if params[:type].in?(%w[initials signature])
|
||||
image = Vips::Image.new_from_file(params[:file].path)
|
||||
|
||||
if ImageUtils.blank?(image)
|
||||
Rollbar.error("Empty signature: #{submitter.id}") if defined?(Rollbar)
|
||||
|
||||
return render json: { error: "#{params[:type]} is empty" }, status: :unprocessable_content
|
||||
end
|
||||
|
||||
if ImageUtils.error?(image)
|
||||
Rollbar.error("Error signature: #{submitter.id}") if defined?(Rollbar)
|
||||
|
||||
return render json: { error: "#{params[:type]} error, try to sign on another device" },
|
||||
status: :unprocessable_content
|
||||
end
|
||||
end
|
||||
|
||||
attachment = Submitters.create_attachment!(submitter, params)
|
||||
|
||||
if params[:remember_signature] == 'true' && submitter.email.present?
|
||||
@@ -17,6 +34,10 @@ module Api
|
||||
end
|
||||
|
||||
render json: attachment.as_json(only: %i[uuid created_at], methods: %i[url filename content_type])
|
||||
rescue Submitters::MaliciousFileExtension => e
|
||||
Rollbar.error(e) if defined?(Rollbar)
|
||||
|
||||
render json: { error: e.message }, status: :unprocessable_content
|
||||
end
|
||||
|
||||
def build_new_cookie_signatures_json(submitter, attachment)
|
||||
|
||||
@@ -11,18 +11,21 @@ module Api
|
||||
params[:before] = Time.zone.at(params[:before].to_i) if params[:before].present?
|
||||
|
||||
submitters = paginate(
|
||||
submitters.preload(template: :folder, submission: [:submitters, { audit_trail_attachment: :blob,
|
||||
combined_document_attachment: :blob }],
|
||||
submitters.preload(template: { folder: :parent_folder },
|
||||
submission: [:submitters, { audit_trail_attachment: :blob,
|
||||
combined_document_attachment: :blob }],
|
||||
documents_attachments: :blob, attachments_attachments: :blob),
|
||||
field: :completed_at
|
||||
)
|
||||
|
||||
expires_at = Accounts.link_expires_at(current_account)
|
||||
|
||||
render json: {
|
||||
data: submitters.map do |s|
|
||||
{
|
||||
event_type: 'form.completed',
|
||||
timestamp: s.completed_at,
|
||||
data: Submitters::SerializeForWebhook.call(s)
|
||||
data: Submitters::SerializeForWebhook.call(s, expires_at:)
|
||||
}
|
||||
end,
|
||||
pagination: {
|
||||
|
||||
@@ -5,41 +5,71 @@ module Api
|
||||
load_and_authorize_resource :submission
|
||||
|
||||
def index
|
||||
is_merge = params[:merge] == 'true' &&
|
||||
(@submission.schema_documents || @submission.template.schema_documents).size > 1
|
||||
|
||||
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
|
||||
build_completed_documents(@submission, merge: is_merge)
|
||||
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
|
||||
build_preview_documents(@submission, merge: is_merge)
|
||||
end
|
||||
|
||||
ActiveRecord::Associations::Preloader.new(
|
||||
records: documents,
|
||||
associations: [:blob]
|
||||
).call
|
||||
ActiveRecord::Associations::Preloader.new(records: documents, associations: [:blob]).call
|
||||
|
||||
expires_at = Accounts.link_expires_at(current_account)
|
||||
|
||||
render json: {
|
||||
id: @submission.id,
|
||||
documents: documents.map do |attachment|
|
||||
{ name: attachment.filename.base, url: ActiveStorage::Blob.proxy_url(attachment.blob) }
|
||||
{ name: attachment.filename.base, url: ActiveStorage::Blob.proxy_url(attachment.blob, expires_at:) }
|
||||
end
|
||||
}
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def build_completed_documents(submission, merge: false)
|
||||
last_submitter = submission.submitters.max_by(&:completed_at)
|
||||
|
||||
if merge
|
||||
if submission.merged_document_attachment.blank?
|
||||
submission.merged_document_attachment =
|
||||
Submissions::GenerateCombinedAttachment.call(last_submitter, with_audit: false)
|
||||
end
|
||||
|
||||
[submission.merged_document_attachment]
|
||||
else
|
||||
if last_submitter.documents_attachments.blank?
|
||||
last_submitter.documents_attachments = Submissions::EnsureResultGenerated.call(last_submitter)
|
||||
end
|
||||
|
||||
last_submitter.documents_attachments
|
||||
end
|
||||
end
|
||||
|
||||
def build_preview_documents(submission, merge: false)
|
||||
values_hash = Submissions::GeneratePreviewAttachments.build_values_hash(submission)
|
||||
|
||||
if merge
|
||||
if submission.preview_merged_document_attachment.present? &&
|
||||
submission.preview_merged_document_attachment.metadata['values_hash'] == values_hash
|
||||
[submission.preview_merged_document_attachment]
|
||||
else
|
||||
ApplicationRecord.no_touching { submission.preview_merged_document_attachment&.destroy }
|
||||
|
||||
Submissions::GeneratePreviewAttachments.call(submission, values_hash:, merge: true)
|
||||
end
|
||||
elsif 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
|
||||
end
|
||||
end
|
||||
|
||||
@@ -14,16 +14,19 @@ module Api
|
||||
:created_by_user, :submission_events,
|
||||
template: :folder,
|
||||
submitters: { documents_attachments: :blob, attachments_attachments: :blob },
|
||||
audit_trail_attachment: :blob
|
||||
audit_trail_attachment: :blob,
|
||||
combined_document_attachment: :blob
|
||||
),
|
||||
field: :completed_at)
|
||||
|
||||
expires_at = Accounts.link_expires_at(current_account)
|
||||
|
||||
render json: {
|
||||
data: submissions.map do |s|
|
||||
{
|
||||
event_type: 'submission.completed',
|
||||
timestamp: s.completed_at,
|
||||
data: Submissions::SerializeForApi.call(s, s.submitters)
|
||||
data: Submissions::SerializeForApi.call(s, s.submitters, expires_at:)
|
||||
}
|
||||
end,
|
||||
pagination: {
|
||||
|
||||
@@ -14,14 +14,16 @@ module Api
|
||||
submissions = filter_submissions(submissions, params)
|
||||
|
||||
submissions = paginate(submissions.preload(:created_by_user, :submitters,
|
||||
template: :folder,
|
||||
template: { folder: :parent_folder },
|
||||
combined_document_attachment: :blob,
|
||||
audit_trail_attachment: :blob))
|
||||
|
||||
expires_at = Accounts.link_expires_at(current_account)
|
||||
|
||||
render json: {
|
||||
data: submissions.map do |s|
|
||||
Submissions::SerializeForApi.call(s, s.submitters, params,
|
||||
with_events: false, with_documents: false, with_values: false)
|
||||
with_events: false, with_documents: false, with_values: false, expires_at:)
|
||||
end,
|
||||
pagination: {
|
||||
count: submissions.size,
|
||||
@@ -41,7 +43,7 @@ module Api
|
||||
end
|
||||
|
||||
if @submission.audit_trail_attachment.blank? && submitters.all?(&:completed_at?)
|
||||
@submission.audit_trail_attachment = Submissions::GenerateAuditTrail.call(@submission)
|
||||
@submission.audit_trail_attachment = Submissions::EnsureAuditGenerated.call(@submission)
|
||||
end
|
||||
|
||||
render json: Submissions::SerializeForApi.call(@submission, submitters, params)
|
||||
@@ -50,12 +52,12 @@ module Api
|
||||
def create
|
||||
Params::SubmissionCreateValidator.call(params)
|
||||
|
||||
return render json: { error: 'Template not found' }, status: :unprocessable_entity if @template.nil?
|
||||
return render json: { error: 'Template not found' }, status: :unprocessable_content if @template.nil?
|
||||
|
||||
if @template.fields.blank?
|
||||
Rollbar.warning("Template does not contain fields: #{@template.id}") if defined?(Rollbar)
|
||||
|
||||
return render json: { error: 'Template does not contain fields' }, status: :unprocessable_entity
|
||||
return render json: { error: 'Template does not contain fields' }, status: :unprocessable_content
|
||||
end
|
||||
|
||||
params[:send_email] = true unless params.key?(:send_email)
|
||||
@@ -63,12 +65,7 @@ module Api
|
||||
|
||||
submissions = create_submissions(@template, params)
|
||||
|
||||
WebhookUrls.for_account_id(@template.account_id, 'submission.created').each do |webhook_url|
|
||||
submissions.each do |submission|
|
||||
SendSubmissionCreatedWebhookRequestJob.perform_async('submission_id' => submission.id,
|
||||
'webhook_url_id' => webhook_url.id)
|
||||
end
|
||||
end
|
||||
WebhookUrls.enqueue_events(submissions, 'submission.created')
|
||||
|
||||
Submissions.send_signature_requests(submissions)
|
||||
|
||||
@@ -87,7 +84,7 @@ module Api
|
||||
DownloadUtils::UnableToDownload => e
|
||||
Rollbar.warning(e) if defined?(Rollbar)
|
||||
|
||||
render json: { error: e.message }, status: :unprocessable_entity
|
||||
render json: { error: e.message }, status: :unprocessable_content
|
||||
end
|
||||
|
||||
def destroy
|
||||
@@ -96,10 +93,7 @@ module Api
|
||||
else
|
||||
@submission.update!(archived_at: Time.current)
|
||||
|
||||
WebhookUrls.for_account_id(@submission.account_id, 'submission.archived').each do |webhook_url|
|
||||
SendSubmissionArchivedWebhookRequestJob.perform_async('submission_id' => @submission.id,
|
||||
'webhook_url_id' => webhook_url.id)
|
||||
end
|
||||
WebhookUrls.enqueue_events(@submission, 'submission.archived')
|
||||
end
|
||||
|
||||
render json: @submission.as_json(only: %i[id archived_at])
|
||||
@@ -112,7 +106,10 @@ module Api
|
||||
submissions = submissions.where(slug: params[:slug]) if params[:slug].present?
|
||||
|
||||
if params[:template_folder].present?
|
||||
submissions = submissions.joins(template: :folder).where(folder: { name: params[:template_folder] })
|
||||
folders =
|
||||
TemplateFolders.filter_by_full_name(TemplateFolder.accessible_by(current_ability), params[:template_folder])
|
||||
|
||||
submissions = submissions.joins(:template).where(template: { folder_id: folders.pluck(:id) })
|
||||
end
|
||||
|
||||
if params.key?(:archived)
|
||||
@@ -150,7 +147,7 @@ module Api
|
||||
is_send_email = !params[:send_email].in?(['false', false])
|
||||
|
||||
if (emails = (params[:emails] || params[:email]).presence) &&
|
||||
(params[:submission].blank? && params[:submitters].blank?)
|
||||
params[:submission].blank? && params[:submitters].blank?
|
||||
Submissions.create_from_emails(template:,
|
||||
user: current_user,
|
||||
source: :api,
|
||||
@@ -185,15 +182,17 @@ module Api
|
||||
def submissions_params
|
||||
permitted_attrs = [
|
||||
:send_email, :send_sms, :bcc_completed, :completed_redirect_url, :reply_to, :go_to_last,
|
||||
:expire_at, :name,
|
||||
:require_phone_2fa, :require_email_2fa, :expire_at, :name,
|
||||
{
|
||||
variables: {},
|
||||
message: %i[subject body],
|
||||
submitters: [[:send_email, :send_sms, :completed_redirect_url, :uuid, :name, :email, :role,
|
||||
:completed, :phone, :application_key, :external_id, :reply_to, :go_to_last,
|
||||
:require_phone_2fa, :require_email_2fa, :order, :index, :invite_by,
|
||||
{ metadata: {}, values: {}, roles: [], readonly_fields: [], message: %i[subject body],
|
||||
fields: [:name, :uuid, :default_value, :value, :title, :description,
|
||||
:readonly, :required, :validation_pattern, :invalid_message,
|
||||
{ default_value: [], value: [], preferences: {} }] }]]
|
||||
{ default_value: [], value: [], preferences: {}, validation: {} }] }]]
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
@@ -13,10 +13,7 @@ module Api
|
||||
|
||||
SubmissionEvents.create_with_tracking_data(@submitter, 'view_form', request)
|
||||
|
||||
WebhookUrls.for_account_id(@submitter.account_id, 'form.viewed').each do |webhook_url|
|
||||
SendFormViewedWebhookRequestJob.perform_async('submitter_id' => @submitter.id,
|
||||
'webhook_url_id' => webhook_url.id)
|
||||
end
|
||||
WebhookUrls.enqueue_events(@submitter, 'form.viewed')
|
||||
|
||||
render json: {}
|
||||
end
|
||||
|
||||
@@ -14,9 +14,11 @@ module Api
|
||||
documents_attachments: :blob, attachments_attachments: :blob)
|
||||
)
|
||||
|
||||
expires_at = Accounts.link_expires_at(current_account)
|
||||
|
||||
render json: {
|
||||
data: submitters.map do |s|
|
||||
Submitters::SerializeForApi.call(s, with_template: true, with_events: true, params:)
|
||||
Submitters::SerializeForApi.call(s, with_template: true, with_events: true, params:, expires_at:)
|
||||
end,
|
||||
pagination: {
|
||||
count: submitters.size,
|
||||
@@ -34,7 +36,7 @@ module Api
|
||||
|
||||
def update
|
||||
if @submitter.completed_at?
|
||||
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_content
|
||||
end
|
||||
|
||||
submission = @submitter.submission
|
||||
@@ -74,7 +76,7 @@ module Api
|
||||
rescue Submitters::NormalizeValues::BaseError, DownloadUtils::UnableToDownload => e
|
||||
Rollbar.warning(e) if defined?(Rollbar)
|
||||
|
||||
render json: { error: e.message }, status: :unprocessable_entity
|
||||
render json: { error: e.message }, status: :unprocessable_content
|
||||
end
|
||||
|
||||
def submitter_params
|
||||
@@ -82,7 +84,7 @@ module Api
|
||||
|
||||
submitter_params.permit(
|
||||
:send_email, :send_sms, :reply_to, :completed_redirect_url, :uuid, :name, :email, :role,
|
||||
:completed, :phone, :application_key, :external_id, :go_to_last,
|
||||
:completed, :phone, :application_key, :external_id, :go_to_last, :require_phone_2fa, :require_email_2fa,
|
||||
{ metadata: {}, values: {}, readonly_fields: [], message: %i[subject body],
|
||||
fields: [[:name, :uuid, :default_value, :value, :required,
|
||||
:readonly, :validation_pattern, :invalid_message,
|
||||
@@ -193,6 +195,10 @@ module Api
|
||||
|
||||
submitter.preferences['send_sms'] = submitter_preferences['send_sms'] if submitter_preferences.key?('send_sms')
|
||||
submitter.preferences['reply_to'] = submitter_preferences['reply_to'] if submitter_preferences.key?('reply_to')
|
||||
if submitter_preferences.key?('require_phone_2fa')
|
||||
submitter.preferences['require_phone_2fa'] = submitter_preferences['require_phone_2fa']
|
||||
end
|
||||
|
||||
if submitter_preferences.key?('go_to_last')
|
||||
submitter.preferences['go_to_last'] = submitter_preferences['go_to_last']
|
||||
end
|
||||
|
||||
@@ -26,16 +26,15 @@ module Api
|
||||
original_template: @template,
|
||||
documents: params[:documents])
|
||||
|
||||
Templates.maybe_assign_access(cloned_template)
|
||||
|
||||
cloned_template.save!
|
||||
|
||||
WebhookUrls.for_account_id(cloned_template.account_id, 'template.created').each do |webhook_url|
|
||||
SendTemplateCreatedWebhookRequestJob.perform_async('template_id' => cloned_template.id,
|
||||
'webhook_url_id' => webhook_url.id)
|
||||
end
|
||||
WebhookUrls.enqueue_events(cloned_template, 'template.created')
|
||||
|
||||
SearchEntries.enqueue_reindex(cloned_template)
|
||||
|
||||
render json: Templates::SerializeForApi.call(cloned_template, schema_documents)
|
||||
render json: Templates::SerializeForApi.call(cloned_template, schema_documents:)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -7,7 +7,7 @@ module Api
|
||||
def index
|
||||
templates = filter_templates(@templates, params)
|
||||
|
||||
templates = paginate(templates.preload(:author, :folder))
|
||||
templates = paginate(templates.preload(:author, folder: :parent_folder))
|
||||
|
||||
schema_documents =
|
||||
ActiveStorage::Attachment.where(record_id: templates.map(&:id),
|
||||
@@ -24,13 +24,14 @@ module Api
|
||||
name: :preview_images)
|
||||
.preload(:blob)
|
||||
|
||||
expires_at = Accounts.link_expires_at(current_account)
|
||||
|
||||
render json: {
|
||||
data: templates.map do |t|
|
||||
Templates::SerializeForApi.call(
|
||||
t,
|
||||
schema_documents.select { |e| e.record_id == t.id },
|
||||
preview_image_attachments
|
||||
)
|
||||
Templates::SerializeForApi.call(t,
|
||||
schema_documents: schema_documents.select { |e| e.record_id == t.id },
|
||||
preview_image_attachments:,
|
||||
expires_at:)
|
||||
end,
|
||||
pagination: {
|
||||
count: templates.size,
|
||||
@@ -67,10 +68,7 @@ module Api
|
||||
|
||||
SearchEntries.enqueue_reindex(@template)
|
||||
|
||||
WebhookUrls.for_account_id(@template.account_id, 'template.updated').each do |webhook_url|
|
||||
SendTemplateUpdatedWebhookRequestJob.perform_async('template_id' => @template.id,
|
||||
'webhook_url_id' => webhook_url.id)
|
||||
end
|
||||
WebhookUrls.enqueue_events(@template, 'template.updated')
|
||||
|
||||
render json: @template.as_json(only: %i[id updated_at])
|
||||
end
|
||||
@@ -93,7 +91,12 @@ module Api
|
||||
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(slug: params[:slug]) if params[:slug].present?
|
||||
templates = templates.joins(:folder).where(folder: { name: params[:folder] }) if params[:folder].present?
|
||||
|
||||
if params[:folder].present?
|
||||
folders = TemplateFolders.filter_by_full_name(TemplateFolder.accessible_by(current_ability), params[:folder])
|
||||
|
||||
templates = templates.where(folder_id: folders.pluck(:id))
|
||||
end
|
||||
|
||||
templates
|
||||
end
|
||||
@@ -104,14 +107,15 @@ module Api
|
||||
: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 is_requester invite_by_uuid optional_invite_by_uuid linked_to_uuid email order]],
|
||||
fields: [[:uuid, :submitter_uuid, :name, :type,
|
||||
:required, :readonly, :default_value,
|
||||
:title, :description,
|
||||
:title, :description, :prefillable,
|
||||
{ preferences: {},
|
||||
default_value: [],
|
||||
conditions: [%i[field_uuid value action operation]],
|
||||
options: [%i[value uuid]],
|
||||
validation: %i[message pattern],
|
||||
validation: %i[message pattern min max step],
|
||||
areas: [%i[x y w h cell_w attachment_uuid option_uuid page]] }]]
|
||||
}
|
||||
]
|
||||
|
||||
@@ -7,8 +7,8 @@ module Api
|
||||
def merge
|
||||
files = params[:files] || []
|
||||
|
||||
return render json: { error: 'Files are required' }, status: :unprocessable_entity if files.blank?
|
||||
return render json: { error: 'At least 2 files are required' }, status: :unprocessable_entity if files.size < 2
|
||||
return render json: { error: 'Files are required' }, status: :unprocessable_content if files.blank?
|
||||
return render json: { error: 'At least 2 files are required' }, status: :unprocessable_content if files.size < 2
|
||||
|
||||
render json: {
|
||||
data: Base64.encode64(PdfUtils.merge(files.map { |base64| StringIO.new(Base64.decode64(base64)) }).string)
|
||||
@@ -35,7 +35,7 @@ module Api
|
||||
end
|
||||
}
|
||||
rescue HexaPDF::MalformedPDFError
|
||||
render json: { error: 'Malformed PDF' }, status: :unprocessable_entity
|
||||
render json: { error: 'Malformed PDF' }, status: :unprocessable_content
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -4,7 +4,7 @@ class ApplicationController < ActionController::Base
|
||||
BROWSER_LOCALE_REGEXP = /\A\w{2}(?:-\w{2})?/
|
||||
|
||||
include ActiveStorage::SetCurrent
|
||||
include Pagy::Backend
|
||||
include Pagy::Method
|
||||
|
||||
check_authorization unless: :devise_controller?
|
||||
|
||||
@@ -13,14 +13,17 @@ class ApplicationController < ActionController::Base
|
||||
before_action :maybe_redirect_to_setup, unless: :signed_in?
|
||||
before_action :authenticate_user!, unless: :devise_controller?
|
||||
|
||||
before_action :set_csp, if: -> { request.get? && !request.headers['HTTP_X_TURBO'] }
|
||||
|
||||
helper_method :button_title,
|
||||
:current_account,
|
||||
:true_ability,
|
||||
:form_link_host,
|
||||
:svg_icon
|
||||
|
||||
impersonates :user, with: ->(uuid) { User.find_by(uuid:) }
|
||||
|
||||
rescue_from Pagy::OverflowError do
|
||||
rescue_from Pagy::RangeError do
|
||||
redirect_to request.path
|
||||
end
|
||||
|
||||
@@ -57,7 +60,7 @@ class ApplicationController < ActionController::Base
|
||||
|
||||
def pagy_auto(collection, **keyword_args)
|
||||
if current_ability.can?(:manage, :countless)
|
||||
pagy_countless(collection, **keyword_args)
|
||||
pagy(:countless, collection, **keyword_args)
|
||||
else
|
||||
pagy(collection, **keyword_args)
|
||||
end
|
||||
@@ -100,6 +103,10 @@ class ApplicationController < ActionController::Base
|
||||
current_user&.account
|
||||
end
|
||||
|
||||
def true_ability
|
||||
@true_ability ||= Ability.new(true_user)
|
||||
end
|
||||
|
||||
def maybe_redirect_to_setup
|
||||
redirect_to setup_index_path unless User.exists?
|
||||
end
|
||||
@@ -123,4 +130,21 @@ class ApplicationController < ActionController::Base
|
||||
|
||||
redirect_to request.url.gsub('.co/', '.com/'), allow_other_host: true, status: :moved_permanently
|
||||
end
|
||||
|
||||
def set_csp
|
||||
request.content_security_policy = current_content_security_policy.tap do |policy|
|
||||
policy.default_src :self
|
||||
policy.script_src :self
|
||||
policy.style_src :self, :unsafe_inline
|
||||
policy.img_src :self, :https, :http, :blob, :data
|
||||
policy.font_src :self, :https, :http, :blob, :data
|
||||
policy.manifest_src :self
|
||||
policy.media_src :self
|
||||
policy.frame_src :self
|
||||
policy.worker_src :self, :blob
|
||||
policy.connect_src :self
|
||||
|
||||
policy.directives['connect-src'] << 'ws:' if Rails.env.development?
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -17,8 +17,10 @@ class ConsoleRedirectController < ApplicationController
|
||||
scope: :console,
|
||||
exp: 1.minute.from_now.to_i)
|
||||
|
||||
path = Addressable::URI.parse(params[:redir]).path if params[:redir].to_s.starts_with?(Docuseal::CONSOLE_URL)
|
||||
redir_uri = Addressable::URI.parse(params[:redir])
|
||||
path = redir_uri.path if params[:redir].to_s.starts_with?(Docuseal::CONSOLE_URL)
|
||||
|
||||
redirect_to("#{Docuseal::CONSOLE_URL}#{path}?#{{ auth: }.to_query}", allow_other_host: true)
|
||||
redirect_to "#{Docuseal::CONSOLE_URL}#{path}?#{{ **redir_uri&.query_values, 'auth' => auth }.to_query}",
|
||||
allow_other_host: true
|
||||
end
|
||||
end
|
||||
|
||||
@@ -15,12 +15,12 @@ class EmailSmtpSettingsController < ApplicationController
|
||||
|
||||
redirect_to settings_email_index_path, notice: I18n.t('changes_have_been_saved')
|
||||
else
|
||||
render :index, status: :unprocessable_entity
|
||||
render :index, status: :unprocessable_content
|
||||
end
|
||||
rescue StandardError => e
|
||||
flash[:alert] = e.message
|
||||
|
||||
render :index, status: :unprocessable_entity
|
||||
render :index, status: :unprocessable_content
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
@@ -5,8 +5,12 @@ class ErrorsController < ActionController::Base
|
||||
'This feature is available in Pro Edition: https://www.docuseal.com/pricing'
|
||||
|
||||
ENTERPRISE_PATHS = [
|
||||
'/submissions/html',
|
||||
'/api/submissions/html',
|
||||
'/templates/html',
|
||||
'/api/templates/html',
|
||||
'/submissions/pdf',
|
||||
'/api/submissions/pdf',
|
||||
'/templates/pdf',
|
||||
'/api/templates/pdf',
|
||||
'/templates/doc',
|
||||
|
||||
@@ -53,7 +53,7 @@ class EsignSettingsController < ApplicationController
|
||||
@cert_record.errors.add(:name, I18n.t('already_exists'))
|
||||
|
||||
return render turbo_stream: turbo_stream.replace(:modal, template: 'esign_settings/new'),
|
||||
status: :unprocessable_entity
|
||||
status: :unprocessable_content
|
||||
end
|
||||
|
||||
save_new_cert!(@encrypted_config, @cert_record)
|
||||
@@ -64,7 +64,7 @@ class EsignSettingsController < ApplicationController
|
||||
|
||||
@cert_record.errors.add(:password, e.message)
|
||||
|
||||
render turbo_stream: turbo_stream.replace(:modal, template: 'esign_settings/new'), status: :unprocessable_entity
|
||||
render turbo_stream: turbo_stream.replace(:modal, template: 'esign_settings/new'), status: :unprocessable_content
|
||||
end
|
||||
|
||||
def update
|
||||
|
||||
@@ -1,4 +1,11 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class InvitationsController < Devise::PasswordsController
|
||||
def update
|
||||
super do |resource|
|
||||
resource.confirmed_at ||= Time.current if resource.errors.empty?
|
||||
|
||||
PasswordsController::Current.user = resource
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -24,7 +24,7 @@ class MfaSetupController < ApplicationController
|
||||
|
||||
@error_message = I18n.t('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_content
|
||||
end
|
||||
end
|
||||
|
||||
@@ -36,7 +36,7 @@ class MfaSetupController < ApplicationController
|
||||
else
|
||||
@error_message = I18n.t('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_content
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@@ -1,6 +1,12 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class PasswordsController < Devise::PasswordsController
|
||||
# rubocop:disable Rails/LexicallyScopedActionFilter
|
||||
skip_before_action :require_no_authentication, only: %i[edit update]
|
||||
# rubocop:enable Rails/LexicallyScopedActionFilter
|
||||
|
||||
around_action :with_browser_locale
|
||||
|
||||
class Current < ActiveSupport::CurrentAttributes
|
||||
attribute :user
|
||||
end
|
||||
@@ -16,4 +22,10 @@ class PasswordsController < Devise::PasswordsController
|
||||
Current.user = resource
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def after_resetting_password_path_for(_)
|
||||
new_session_path(resource_name)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -4,6 +4,7 @@ class PersonalizationSettingsController < ApplicationController
|
||||
ALLOWED_KEYS = [
|
||||
AccountConfig::FORM_COMPLETED_BUTTON_KEY,
|
||||
AccountConfig::SUBMITTER_INVITATION_EMAIL_KEY,
|
||||
AccountConfig::SUBMITTER_INVITATION_REMINDER_EMAIL_KEY,
|
||||
AccountConfig::SUBMITTER_DOCUMENTS_COPY_EMAIL_KEY,
|
||||
AccountConfig::SUBMITTER_COMPLETED_EMAIL_KEY,
|
||||
AccountConfig::FORM_COMPLETED_MESSAGE_KEY,
|
||||
|
||||
@@ -9,18 +9,25 @@ class ProfileController < ApplicationController
|
||||
|
||||
def update_contact
|
||||
if current_user.update(contact_params)
|
||||
redirect_to settings_profile_index_path, notice: I18n.t('contact_information_has_been_update')
|
||||
if current_user.try(:pending_reconfirmation?) && current_user.previous_changes.key?(:unconfirmed_email)
|
||||
SendConfirmationInstructionsJob.perform_async('user_id' => current_user.id)
|
||||
|
||||
redirect_to settings_profile_index_path,
|
||||
notice: I18n.t('a_confirmation_email_has_been_sent_to_the_new_email_address')
|
||||
else
|
||||
redirect_to settings_profile_index_path, notice: I18n.t('contact_information_has_been_update')
|
||||
end
|
||||
else
|
||||
render :index, status: :unprocessable_entity
|
||||
render :index, status: :unprocessable_content
|
||||
end
|
||||
end
|
||||
|
||||
def update_password
|
||||
if current_user.update(password_params)
|
||||
if current_user.update_with_password(password_params)
|
||||
bypass_sign_in(current_user)
|
||||
redirect_to settings_profile_index_path, notice: I18n.t('password_has_been_changed')
|
||||
else
|
||||
render :index, status: :unprocessable_entity
|
||||
render :index, status: :unprocessable_content
|
||||
end
|
||||
end
|
||||
|
||||
@@ -31,6 +38,6 @@ class ProfileController < ApplicationController
|
||||
end
|
||||
|
||||
def password_params
|
||||
params.require(:user).permit(:password, :password_confirmation)
|
||||
params.require(:user).permit(:password, :password_confirmation, :current_password)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class RevealAccessTokenController < ApplicationController
|
||||
def show
|
||||
authorize!(:manage, current_user.access_token)
|
||||
end
|
||||
|
||||
def create
|
||||
authorize!(:manage, current_user.access_token)
|
||||
|
||||
if current_user.valid_password?(params[:password])
|
||||
render turbo_stream: turbo_stream.replace(:access_token_container,
|
||||
partial: 'reveal_access_token/access_token',
|
||||
locals: { token: current_user.access_token.token })
|
||||
else
|
||||
render turbo_stream: turbo_stream.replace(:modal, template: 'reveal_access_token/show',
|
||||
locals: { error_message: I18n.t('wrong_password') }),
|
||||
status: :unprocessable_content
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,17 @@
|
||||
# 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
|
||||
@@ -11,11 +11,16 @@ class SendSubmissionEmailController < ApplicationController
|
||||
|
||||
def create
|
||||
if params[:template_slug]
|
||||
@submitter = Submitter.completed.joins(submission: :template).find_by!(email: params[:email].to_s.downcase,
|
||||
template: { slug: params[:template_slug] })
|
||||
template = Template.find_by!(slug: params[:template_slug])
|
||||
|
||||
@submitter =
|
||||
Submitter.completed.where(submission: template.submissions).find_by!(email: params[:email].to_s.downcase)
|
||||
elsif params[:submission_slug]
|
||||
@submitter = Submitter.completed.joins(:submission).find_by(email: params[:email].to_s.downcase,
|
||||
submission: { slug: params[:submission_slug] })
|
||||
submission = Submission.find_by(slug: params[:submission_slug])
|
||||
|
||||
if submission
|
||||
@submitter = Submitter.completed.find_by(submission: submission, email: params[:email].to_s.downcase)
|
||||
end
|
||||
|
||||
return redirect_to submissions_preview_completed_path(params[:submission_slug], status: :error) unless @submitter
|
||||
else
|
||||
@@ -24,14 +29,21 @@ class SendSubmissionEmailController < ApplicationController
|
||||
|
||||
RateLimit.call("send-email-#{@submitter.id}", limit: 2, ttl: 5.minutes)
|
||||
|
||||
unless EmailEvent.exists?(tag: :submitter_documents_copy, email: @submitter.email, emailable: @submitter,
|
||||
event_type: :send, created_at: SEND_DURATION.ago..Time.current)
|
||||
SubmitterMailer.documents_copy_email(@submitter, sig: true).deliver_later!
|
||||
end
|
||||
SubmitterMailer.documents_copy_email(@submitter, sig: true).deliver_later! if can_send?(@submitter)
|
||||
|
||||
respond_to do |f|
|
||||
f.html { render :success }
|
||||
f.json { head :ok }
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def can_send?(submitter)
|
||||
return false if submitter.account.archived_at?
|
||||
return false if EmailEvent.exists?(tag: :submitter_documents_copy, email: submitter.email, emailable: submitter,
|
||||
event_type: :send, created_at: SEND_DURATION.ago..Time.current)
|
||||
|
||||
true
|
||||
end
|
||||
end
|
||||
|
||||
@@ -16,7 +16,7 @@ class SessionsController < Devise::SessionsController
|
||||
end
|
||||
|
||||
if User.exists?(email:, otp_required_for_login: true) && sign_in_params[:otp_attempt].blank?
|
||||
return render :otp, locals: { resource: User.new(sign_in_params) }, status: :unprocessable_entity
|
||||
return render :otp, locals: { resource: User.new(sign_in_params) }, status: :unprocessable_content
|
||||
end
|
||||
|
||||
super
|
||||
|
||||
@@ -23,10 +23,10 @@ class SetupController < ApplicationController
|
||||
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'))
|
||||
|
||||
return render :index, status: :unprocessable_entity
|
||||
return render :index, status: :unprocessable_content
|
||||
end
|
||||
|
||||
return render :index, status: :unprocessable_entity unless @account.valid?
|
||||
return render :index, status: :unprocessable_content unless @account.valid?
|
||||
|
||||
if @user.save
|
||||
encrypted_configs = [
|
||||
@@ -34,6 +34,7 @@ class SetupController < ApplicationController
|
||||
{ key: EncryptedConfig::ESIGN_CERTS_KEY, value: GenerateCertificate.call.transform_values(&:to_pem) }
|
||||
]
|
||||
@account.encrypted_configs.create!(encrypted_configs)
|
||||
@account.account_configs.create!(key: :fulltext_search, value: true) if SearchEntry.table_exists?
|
||||
|
||||
Docuseal.refresh_default_url_options!
|
||||
|
||||
@@ -41,7 +42,7 @@ class SetupController < ApplicationController
|
||||
|
||||
redirect_to newsletter_path
|
||||
else
|
||||
render :index, status: :unprocessable_entity
|
||||
render :index, status: :unprocessable_content
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@@ -6,20 +6,26 @@ class StartFormController < ApplicationController
|
||||
skip_before_action :authenticate_user!
|
||||
skip_authorization_check
|
||||
|
||||
around_action :with_browser_locale, only: %i[show completed]
|
||||
around_action :with_browser_locale, only: %i[show update completed]
|
||||
before_action :maybe_redirect_com, only: %i[show completed]
|
||||
before_action :load_resubmit_submitter, only: :update
|
||||
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
|
||||
raise ActionController::RoutingError, I18n.t('not_found') if @template.preferences['require_phone_2fa']
|
||||
if @template.preferences['require_phone_2fa'] || @template.preferences['require_email_2fa']
|
||||
raise ActionController::RoutingError, I18n.t('not_found')
|
||||
end
|
||||
|
||||
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)
|
||||
|
||||
@@ -33,12 +39,12 @@ class StartFormController < ApplicationController
|
||||
@submitter = find_or_initialize_submitter(@template, submitter_params)
|
||||
|
||||
if @submitter.completed_at?
|
||||
redirect_to start_form_completed_path(@template.slug, email: submitter_params[:email])
|
||||
redirect_to start_form_completed_path(@template.slug, submitter_params.compact_blank)
|
||||
else
|
||||
if filter_undefined_submitters(@template).size > 1 && @submitter.new_record?
|
||||
@error_message = multiple_submitters_error_message
|
||||
|
||||
return render :show
|
||||
return render :show, status: :unprocessable_content
|
||||
end
|
||||
|
||||
if (is_new_record = @submitter.new_record?)
|
||||
@@ -49,21 +55,14 @@ class StartFormController < ApplicationController
|
||||
@submitter.assign_attributes(ip: request.remote_ip, ua: request.user_agent)
|
||||
end
|
||||
|
||||
if @submitter.save
|
||||
if is_new_record
|
||||
enqueue_submission_create_webhooks(@submitter)
|
||||
|
||||
SearchEntries.enqueue_reindex(@submitter)
|
||||
|
||||
if @submitter.submission.expire_at?
|
||||
ProcessSubmissionExpiredJob.perform_at(@submitter.submission.expire_at,
|
||||
'submission_id' => @submitter.submission_id)
|
||||
end
|
||||
end
|
||||
if @template.preferences['shared_link_2fa'] == true
|
||||
handle_require_2fa(@submitter, is_new_record:)
|
||||
elsif @submitter.errors.blank? && @submitter.save
|
||||
enqueue_new_submitter_jobs(@submitter) if is_new_record
|
||||
|
||||
redirect_to submit_form_path(@submitter.slug)
|
||||
else
|
||||
render :show
|
||||
render :show, status: :unprocessable_content
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -71,13 +70,34 @@ class StartFormController < ApplicationController
|
||||
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)
|
||||
.where.not(completed_at: nil)
|
||||
.find_by!(email: params[:email])
|
||||
.find_by!(required_params.except('name'))
|
||||
end
|
||||
|
||||
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'])
|
||||
@@ -96,23 +116,35 @@ class StartFormController < ApplicationController
|
||||
redirect_to start_form_path(@template.slug)
|
||||
end
|
||||
|
||||
def enqueue_submission_create_webhooks(submitter)
|
||||
WebhookUrls.for_account_id(submitter.account_id, 'submission.created').each do |webhook_url|
|
||||
SendSubmissionCreatedWebhookRequestJob.perform_async('submission_id' => submitter.submission_id,
|
||||
'webhook_url_id' => webhook_url.id)
|
||||
end
|
||||
end
|
||||
|
||||
def find_or_initialize_submitter(template, submitter_params)
|
||||
Submitter
|
||||
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(ip: [nil, request.remote_ip])
|
||||
.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(email: submitter_params[:email], **submitter_params.compact_blank)
|
||||
.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)
|
||||
@@ -125,6 +157,8 @@ class StartFormController < ApplicationController
|
||||
metadata: @resubmit_submitter&.metadata.presence || {}
|
||||
)
|
||||
|
||||
submitter.assign_attributes(@resubmit_submitter.slice(:name, :email, :phone)) if @resubmit_submitter
|
||||
|
||||
if submitter.values.present?
|
||||
@resubmit_submitter.attachments.each do |attachment|
|
||||
submitter.attachments << attachment.dup if submitter.values.value?(attachment.uuid)
|
||||
@@ -148,7 +182,7 @@ class StartFormController < ApplicationController
|
||||
end
|
||||
|
||||
def submitter_params
|
||||
return current_user.slice(:email) if params[:selfsign]
|
||||
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|
|
||||
@@ -172,4 +206,39 @@ class StartFormController < ApplicationController
|
||||
I18n.t('not_found')
|
||||
end
|
||||
end
|
||||
|
||||
def handle_require_2fa(submitter, is_new_record:)
|
||||
return render :show, status: :unprocessable_content 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_content
|
||||
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
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
# 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
|
||||
@@ -13,7 +13,7 @@ class StorageSettingsController < ApplicationController
|
||||
|
||||
redirect_to settings_storage_index_path, notice: I18n.t('changes_have_been_saved')
|
||||
else
|
||||
render :index, status: :unprocessable_entity
|
||||
render :index, status: :unprocessable_content
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@@ -10,8 +10,10 @@ class SubmissionEventsController < ApplicationController
|
||||
'api_complete_form' => 'check',
|
||||
'send_reminder_email' => 'mail_forward',
|
||||
'send_2fa_sms' => '2fa',
|
||||
'send_2fa_email' => '2fa',
|
||||
'send_sms' => 'send',
|
||||
'phone_verified' => 'phone_check',
|
||||
'email_verified' => 'email_check',
|
||||
'click_sms' => 'hand_click',
|
||||
'decline_form' => 'x',
|
||||
'start_verification' => 'player_play',
|
||||
|
||||
@@ -12,6 +12,14 @@ class SubmissionsController < ApplicationController
|
||||
authorize!(:create, Submission)
|
||||
end
|
||||
|
||||
FIELD_ICONS = {
|
||||
'text' => 'text_size', 'signature' => 'writing_sign', 'date' => 'calendar_event',
|
||||
'number' => 'square_number_1', 'image' => 'photo', 'initials' => 'letter_case_upper',
|
||||
'file' => 'paperclip', 'select' => 'select', 'checkbox' => 'checkbox', 'radio' => 'circle_dot',
|
||||
'stamp' => 'rubber_stamp', 'cells' => 'columns_3', 'multiple' => 'checks', 'phone' => 'phone_check',
|
||||
'payment' => 'credit_card', 'verification' => 'id', 'kba' => 'user_scan'
|
||||
}.freeze
|
||||
|
||||
def show
|
||||
@submission = Submissions.preload_with_pages(@submission)
|
||||
|
||||
@@ -32,10 +40,7 @@ class SubmissionsController < ApplicationController
|
||||
def create
|
||||
save_template_message(@template, params) if params[:save_message] == '1'
|
||||
|
||||
if params[:is_custom_message] != '1'
|
||||
params.delete(:subject)
|
||||
params.delete(:body)
|
||||
end
|
||||
[params.delete(:subject), params.delete(:body)] if params[:is_custom_message] != '1'
|
||||
|
||||
submissions =
|
||||
if params[:emails].present?
|
||||
@@ -46,15 +51,21 @@ class SubmissionsController < ApplicationController
|
||||
emails: params[:emails],
|
||||
params: params.merge('send_completed_email' => true))
|
||||
else
|
||||
submissions_attrs = submissions_params[:submission].to_h.values
|
||||
|
||||
submissions_attrs, _, new_fields =
|
||||
Submissions::NormalizeParamUtils.normalize_submissions_params!(submissions_attrs, @template, add_fields: true)
|
||||
|
||||
Submissions.create_from_submitters(template: @template,
|
||||
user: current_user,
|
||||
source: :invite,
|
||||
submitters_order: params[:preserve_order] == '1' ? 'preserved' : 'random',
|
||||
submissions_attrs: submissions_params[:submission].to_h.values,
|
||||
submissions_attrs:,
|
||||
new_fields:,
|
||||
params: params.merge('send_completed_email' => true))
|
||||
end
|
||||
|
||||
enqueue_submission_created_webhooks(@template, submissions)
|
||||
WebhookUrls.enqueue_events(submissions, 'submission.created')
|
||||
|
||||
Submissions.send_signature_requests(submissions)
|
||||
|
||||
@@ -62,10 +73,9 @@ class SubmissionsController < ApplicationController
|
||||
|
||||
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
|
||||
render turbo_stream: turbo_stream.replace(:submitters_error, partial: 'submissions/error',
|
||||
locals: { error: e.message }),
|
||||
status: :unprocessable_content
|
||||
end
|
||||
|
||||
def destroy
|
||||
@@ -77,10 +87,7 @@ class SubmissionsController < ApplicationController
|
||||
else
|
||||
@submission.update!(archived_at: Time.current)
|
||||
|
||||
WebhookUrls.for_account_id(@submission.account_id, 'submission.archived').each do |webhook_url|
|
||||
SendSubmissionArchivedWebhookRequestJob.perform_async('submission_id' => @submission.id,
|
||||
'webhook_url_id' => webhook_url.id)
|
||||
end
|
||||
WebhookUrls.enqueue_events(@submission, 'submission.archived')
|
||||
|
||||
I18n.t('submission_has_been_archived')
|
||||
end
|
||||
@@ -97,17 +104,8 @@ class SubmissionsController < ApplicationController
|
||||
template.save!
|
||||
end
|
||||
|
||||
def enqueue_submission_created_webhooks(template, submissions)
|
||||
WebhookUrls.for_account_id(template.account_id, 'submission.created').each do |webhook_url|
|
||||
submissions.each do |submission|
|
||||
SendSubmissionCreatedWebhookRequestJob.perform_async('submission_id' => submission.id,
|
||||
'webhook_url_id' => webhook_url.id)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def submissions_params
|
||||
params.permit(submission: { submitters: [%i[uuid email phone name]] })
|
||||
params.permit(submission: { submitters: [:uuid, :email, :phone, :name, { values: {} }] })
|
||||
end
|
||||
|
||||
def load_template
|
||||
|
||||
@@ -70,7 +70,7 @@ class SubmissionsDownloadController < ApplicationController
|
||||
return if submitter.submission.submitters.order(:completed_at).last != submitter
|
||||
|
||||
attachment = submitter.submission.combined_document_attachment
|
||||
attachment ||= Submissions::GenerateCombinedAttachment.call(submitter)
|
||||
attachment ||= Submissions::EnsureCombinedGenerated.call(submitter)
|
||||
|
||||
filename_format = AccountConfig.find_or_initialize_by(account_id: submitter.account_id,
|
||||
key: AccountConfig::DOCUMENT_FILENAME_FORMAT_KEY)&.value
|
||||
|
||||
@@ -10,11 +10,16 @@ class SubmissionsExportController < ApplicationController
|
||||
attachments_attachments: :blob })
|
||||
.order(id: :asc)
|
||||
|
||||
submissions = Submissions.search(current_user, submissions, params[:q], search_values: true)
|
||||
submissions = Submissions::Filter.call(submissions, current_user, params)
|
||||
|
||||
expires_at = Accounts.link_expires_at(current_account)
|
||||
|
||||
if params[:format] == 'csv'
|
||||
send_data Submissions::GenerateExportFiles.call(submissions, format: params[:format]),
|
||||
send_data Submissions::GenerateExportFiles.call(submissions, format: params[:format], expires_at:),
|
||||
filename: "#{@template.name}.csv"
|
||||
elsif params[:format] == 'xlsx'
|
||||
send_data Submissions::GenerateExportFiles.call(submissions, format: params[:format]),
|
||||
send_data Submissions::GenerateExportFiles.call(submissions, format: params[:format], expires_at:),
|
||||
filename: "#{@template.name}.xlsx"
|
||||
end
|
||||
end
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
class SubmissionsFiltersController < ApplicationController
|
||||
ALLOWED_NAMES = %w[
|
||||
author
|
||||
folder
|
||||
completed_at
|
||||
status
|
||||
created_at
|
||||
|
||||
@@ -28,7 +28,7 @@ class SubmissionsPreviewController < ApplicationController
|
||||
raise ActionController::RoutingError, I18n.t('not_found')
|
||||
end
|
||||
|
||||
if !submission_valid_ttl?(@submission) && !signature_valid
|
||||
if use_signature?(@submission) && !signature_valid
|
||||
Rollbar.info("TTL: #{@submission.id}") if defined?(Rollbar)
|
||||
|
||||
return redirect_to submissions_preview_completed_path(@submission.slug)
|
||||
@@ -41,6 +41,9 @@ class SubmissionsPreviewController < ApplicationController
|
||||
|
||||
def completed
|
||||
@submission = Submission.find_by!(slug: params[:submissions_preview_slug])
|
||||
|
||||
raise ActionController::RoutingError, I18n.t('not_found') if @submission.account.archived_at?
|
||||
|
||||
@template = @submission.template
|
||||
|
||||
render :completed, layout: 'form'
|
||||
@@ -48,9 +51,18 @@ class SubmissionsPreviewController < ApplicationController
|
||||
|
||||
private
|
||||
|
||||
def submission_valid_ttl?(submission)
|
||||
return true if current_user && current_user.account.submissions.exists?(id: submission.id)
|
||||
def use_signature?(submission)
|
||||
return false if current_user && can?(:read, submission)
|
||||
return true if submission.submitters.any? do |e|
|
||||
e.preferences['require_phone_2fa'] || e.preferences['require_email_2fa']
|
||||
end
|
||||
return true if submission.template&.preferences&.dig('require_phone_2fa')
|
||||
return true if submission.template&.preferences&.dig('require_email_2fa')
|
||||
|
||||
!submission_valid_ttl?(submission)
|
||||
end
|
||||
|
||||
def submission_valid_ttl?(submission)
|
||||
last_submitter = submission.submitters.select(&:completed_at?).max_by(&:completed_at)
|
||||
|
||||
last_submitter && last_submitter.completed_at > TTL.ago
|
||||
|
||||
@@ -9,6 +9,7 @@ class SubmitFormController < ApplicationController
|
||||
|
||||
before_action :load_submitter, only: %i[show update completed]
|
||||
before_action :maybe_render_locked_page, only: :show
|
||||
before_action :maybe_require_link_2fa, only: %i[show update]
|
||||
|
||||
CONFIG_KEYS = [].freeze
|
||||
|
||||
@@ -16,6 +17,7 @@ class SubmitFormController < ApplicationController
|
||||
submission = @submitter.submission
|
||||
|
||||
return redirect_to submit_form_completed_path(@submitter.slug) if @submitter.completed_at?
|
||||
return render :email_2fa if require_email_2fa?(@submitter)
|
||||
|
||||
@form_configs = Submitters::FormConfigs.call(@submitter, CONFIG_KEYS)
|
||||
|
||||
@@ -46,21 +48,26 @@ class SubmitFormController < ApplicationController
|
||||
end
|
||||
|
||||
def update
|
||||
if @submitter.completed_at?
|
||||
return render json: { error: I18n.t('form_has_been_completed_already') }, status: :unprocessable_entity
|
||||
if require_email_2fa?(@submitter)
|
||||
return render json: { error: I18n.t('verification_required_refresh_the_page_and_pass_2fa') },
|
||||
status: :unprocessable_content
|
||||
end
|
||||
|
||||
if @submitter.template&.archived_at? || @submitter.submission.archived_at?
|
||||
return render json: { error: I18n.t('form_has_been_archived') }, status: :unprocessable_entity
|
||||
if @submitter.completed_at?
|
||||
return render json: { error: I18n.t('form_has_been_completed_already') }, status: :unprocessable_content
|
||||
end
|
||||
|
||||
if @submitter.submission.template&.archived_at? || @submitter.submission.archived_at?
|
||||
return render json: { error: I18n.t('form_has_been_archived') }, status: :unprocessable_content
|
||||
end
|
||||
|
||||
if @submitter.submission.expired?
|
||||
return render json: { error: I18n.t('form_has_been_expired') }, status: :unprocessable_entity
|
||||
return render json: { error: I18n.t('form_has_been_expired') }, status: :unprocessable_content
|
||||
end
|
||||
|
||||
if @submitter.declined_at?
|
||||
return render json: { error: I18n.t('form_has_been_declined') },
|
||||
status: :unprocessable_entity
|
||||
status: :unprocessable_content
|
||||
end
|
||||
|
||||
Submitters::SubmitValues.call(@submitter, params, request)
|
||||
@@ -69,17 +76,30 @@ class SubmitFormController < ApplicationController
|
||||
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
|
||||
render json: { field_uuid: e.message }, status: :unprocessable_content
|
||||
rescue Submitters::SubmitValues::ValidationError => e
|
||||
render json: { error: e.message }, status: :unprocessable_entity
|
||||
render json: { error: e.message }, status: :unprocessable_content
|
||||
end
|
||||
|
||||
def completed; end
|
||||
def completed
|
||||
raise ActionController::RoutingError, I18n.t('not_found') if @submitter.account.archived_at?
|
||||
|
||||
redirect_to submit_form_path(params[:submit_form_slug]) if require_email_2fa?(@submitter)
|
||||
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? ||
|
||||
@@ -97,4 +117,12 @@ class SubmitFormController < ApplicationController
|
||||
ActiveStorage::Attachment.where(record: submission.submitters, name: :attachments)
|
||||
.preload(:blob).index_by(&:uuid)
|
||||
end
|
||||
|
||||
def require_email_2fa?(submitter)
|
||||
return false if submitter.submission.template&.preferences&.dig('require_email_2fa') != true &&
|
||||
submitter.preferences['require_email_2fa'] != true
|
||||
return false if cookies.encrypted[:email_2fa_slug] == submitter.slug
|
||||
|
||||
true
|
||||
end
|
||||
end
|
||||
|
||||
@@ -25,10 +25,7 @@ class SubmitFormDeclineController < ApplicationController
|
||||
SubmitterMailer.declined_email(submitter, user).deliver_later!
|
||||
end
|
||||
|
||||
WebhookUrls.for_account_id(submitter.account_id, 'form.declined').each do |webhook_url|
|
||||
SendFormDeclinedWebhookRequestJob.perform_async('submitter_id' => submitter.id,
|
||||
'webhook_url_id' => webhook_url.id)
|
||||
end
|
||||
WebhookUrls.enqueue_events(submitter, 'form.declined')
|
||||
|
||||
redirect_to submit_form_path(submitter.slug)
|
||||
end
|
||||
|
||||
@@ -11,13 +11,13 @@ class SubmitFormDownloadController < ApplicationController
|
||||
|
||||
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)
|
||||
return head :unprocessable_content 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)
|
||||
|
||||
@@ -12,7 +12,7 @@ class SubmitFormDrawSignatureController < ApplicationController
|
||||
|
||||
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)
|
||||
end
|
||||
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class SubmitFormEmail2fasController < ApplicationController
|
||||
around_action :with_browser_locale
|
||||
|
||||
skip_before_action :authenticate_user!
|
||||
skip_authorization_check
|
||||
|
||||
before_action :load_submitter
|
||||
|
||||
COOKIES_TTL = 12.hours
|
||||
COOKIES_DEFAULTS = { httponly: true, secure: Rails.env.production? }.freeze
|
||||
|
||||
def create
|
||||
RateLimit.call("verify-2fa-code-#{@submitter.id}", limit: 2, ttl: 45.seconds, enabled: true)
|
||||
|
||||
value = [@submitter.email.downcase.strip, @submitter.slug].join(':')
|
||||
|
||||
if EmailVerificationCodes.verify(params[:one_time_code].to_s.gsub(/\D/, ''), value)
|
||||
SubmissionEvents.create_with_tracking_data(@submitter, 'email_verified', request, { email: @submitter.email })
|
||||
|
||||
cookies.encrypted[:email_2fa_slug] =
|
||||
{ value: @submitter.slug, expires: COOKIES_TTL.from_now, **COOKIES_DEFAULTS }
|
||||
|
||||
redirect_to submit_form_path(@submitter.slug)
|
||||
else
|
||||
redirect_to submit_form_path(@submitter.slug, status: :error), alert: I18n.t(:invalid_code)
|
||||
end
|
||||
rescue RateLimit::LimitApproached
|
||||
redirect_to submit_form_path(@submitter.slug, status: :error), alert: I18n.t(:too_many_attempts)
|
||||
end
|
||||
|
||||
def update
|
||||
if @submitter.submission_events.where(event_type: 'send_2fa_email').exists?(created_at: 15.seconds.ago..)
|
||||
return redirect_to submit_form_path(@submitter.slug, status: :error), alert: I18n.t(:rate_limit_exceeded)
|
||||
end
|
||||
|
||||
RateLimit.call("send-email-code-#{@submitter.id}", limit: 2, ttl: 45.seconds, enabled: true)
|
||||
|
||||
SendSubmitterVerificationEmailJob.perform_async('submitter_id' => @submitter.id, 'locale' => I18n.locale.to_s)
|
||||
|
||||
redir_params = params[:resend] ? { alert: I18n.t(:code_has_been_resent) } : {}
|
||||
|
||||
redirect_to submit_form_path(@submitter.slug, status: :sent), **redir_params
|
||||
rescue RateLimit::LimitApproached
|
||||
redirect_to submit_form_path(@submitter.slug, status: :error), alert: I18n.t(:too_many_attempts)
|
||||
end
|
||||
|
||||
def load_submitter
|
||||
@submitter = Submitter.find_by!(slug: params[:submitter_slug])
|
||||
end
|
||||
end
|
||||
@@ -7,7 +7,7 @@ class SubmitFormInviteController < ApplicationController
|
||||
def create
|
||||
submitter = Submitter.find_by!(slug: params[:submit_form_slug])
|
||||
|
||||
return head :unprocessable_entity unless can_invite?(submitter)
|
||||
return head :unprocessable_content unless can_invite?(submitter)
|
||||
|
||||
invite_submitters = filter_invite_submitters(submitter, 'invite_by_uuid')
|
||||
optional_invite_submitters = filter_invite_submitters(submitter, 'optional_invite_by_uuid')
|
||||
@@ -34,7 +34,7 @@ class SubmitFormInviteController < ApplicationController
|
||||
|
||||
head :ok
|
||||
else
|
||||
head :unprocessable_entity
|
||||
head :unprocessable_content
|
||||
end
|
||||
end
|
||||
|
||||
@@ -45,7 +45,7 @@ class SubmitFormInviteController < ApplicationController
|
||||
!submitter.completed_at? &&
|
||||
!submitter.submission.archived_at? &&
|
||||
!submitter.submission.expired? &&
|
||||
!submitter.submission.template.archived_at?
|
||||
!submitter.submission.template&.archived_at?
|
||||
end
|
||||
|
||||
def filter_invite_submitters(submitter, key = 'invite_by_uuid')
|
||||
|
||||
@@ -8,7 +8,7 @@ class SubmitFormValuesController < ApplicationController
|
||||
submitter = Submitter.find_by!(slug: params[:submit_form_slug])
|
||||
|
||||
return render json: {} if submitter.completed_at? || submitter.declined_at?
|
||||
return render json: {} if submitter.submission.template.archived_at? ||
|
||||
return render json: {} if submitter.submission.template&.archived_at? ||
|
||||
submitter.submission.archived_at? ||
|
||||
submitter.submission.expired?
|
||||
|
||||
|
||||
@@ -22,7 +22,7 @@ class SubmittersAutocompleteController < ApplicationController
|
||||
|
||||
def search_submitters(submitters)
|
||||
if SELECT_COLUMNS.include?(params[:field])
|
||||
if Docuseal.fulltext_search?(current_user)
|
||||
if Docuseal.fulltext_search?
|
||||
Submitters.fulltext_search_field(current_user, submitters, params[:q], params[:field])
|
||||
else
|
||||
column = Submitter.arel_table[params[:field].to_sym]
|
||||
|
||||
@@ -3,9 +3,15 @@
|
||||
class TemplateDocumentsController < ApplicationController
|
||||
load_and_authorize_resource :template
|
||||
|
||||
FILES_TTL = 5.minutes
|
||||
|
||||
def index
|
||||
render json: @template.schema_documents.map { |d| ActiveStorage::Blob.proxy_url(d.blob, expires_at: FILES_TTL.from_now.to_i) }
|
||||
end
|
||||
|
||||
def create
|
||||
if params[:blobs].blank? && params[:files].blank?
|
||||
return render json: { error: I18n.t('file_is_missing') }, status: :unprocessable_entity
|
||||
return render json: { error: I18n.t('file_is_missing') }, status: :unprocessable_content
|
||||
end
|
||||
|
||||
old_fields_hash = @template.fields.hash
|
||||
@@ -28,6 +34,6 @@ class TemplateDocumentsController < ApplicationController
|
||||
)
|
||||
}
|
||||
rescue Templates::CreateAttachments::PdfEncrypted
|
||||
render json: { error: 'PDF encrypted', status: 'pdf_encrypted' }, status: :unprocessable_entity
|
||||
render json: { error: 'PDF encrypted', status: 'pdf_encrypted' }, status: :unprocessable_content
|
||||
end
|
||||
end
|
||||
|
||||
@@ -3,12 +3,31 @@
|
||||
class TemplateFoldersAutocompleteController < ApplicationController
|
||||
load_and_authorize_resource :template_folder, parent: false
|
||||
|
||||
LIMIT = 100
|
||||
LIMIT = 30
|
||||
|
||||
def index
|
||||
template_folders = @template_folders.joins(:templates).where(templates: { archived_at: nil }).distinct
|
||||
template_folders = TemplateFolders.search(template_folders, params[:q]).limit(LIMIT)
|
||||
parent_name, name =
|
||||
if params[:parent_name].present?
|
||||
[params[:parent_name], params[:q]]
|
||||
else
|
||||
params[:q].to_s.split(' /', 2).map(&:squish)
|
||||
end
|
||||
|
||||
render json: template_folders.as_json(only: %i[name archived_at])
|
||||
if name
|
||||
parent_folder = @template_folders.find_by(name: parent_name, parent_folder_id: nil)
|
||||
else
|
||||
name = parent_name
|
||||
end
|
||||
|
||||
template_folders = TemplateFolders.filter_active_folders(@template_folders.where(parent_folder:),
|
||||
Template.accessible_by(current_ability))
|
||||
|
||||
name = name.to_s.downcase
|
||||
|
||||
template_folders = TemplateFolders.search(template_folders, name).order(id: :desc).limit(LIMIT)
|
||||
|
||||
render json: template_folders.preload(:parent_folder)
|
||||
.sort_by { |e| e.name.downcase.index(name) || Float::MAX }
|
||||
.as_json(only: %i[name archived_at], methods: %i[full_name])
|
||||
end
|
||||
end
|
||||
|
||||
@@ -3,13 +3,41 @@
|
||||
class TemplateFoldersController < ApplicationController
|
||||
load_and_authorize_resource :template_folder
|
||||
|
||||
def show
|
||||
@templates = @template_folder.templates.active.accessible_by(current_ability)
|
||||
.preload(:author, :template_accesses)
|
||||
@templates = Templates.search(current_user, @templates, params[:q])
|
||||
@templates = Templates::Order.call(@templates, current_user, cookies.permanent[:dashboard_templates_order])
|
||||
helper_method :selected_order
|
||||
|
||||
@pagy, @templates = pagy_auto(@templates, limit: 12)
|
||||
TEMPLATES_PER_PAGE = 12
|
||||
FOLDERS_PER_PAGE = 18
|
||||
|
||||
def show
|
||||
@templates = Template.active.accessible_by(current_ability)
|
||||
.where(folder: [@template_folder, *(params[:q].present? ? @template_folder.subfolders : [])])
|
||||
.preload(:author, :template_accesses)
|
||||
|
||||
@template_folders =
|
||||
@template_folder.subfolders.where(id: Template.accessible_by(current_ability).active.select(:folder_id))
|
||||
|
||||
@template_folders = TemplateFolders.search(@template_folders, params[:q])
|
||||
@template_folders = TemplateFolders.sort(@template_folders, current_user, selected_order)
|
||||
|
||||
if @templates.exists?
|
||||
@templates = Templates.search(current_user, @templates, params[:q])
|
||||
@templates = Templates::Order.call(@templates, current_user, selected_order)
|
||||
|
||||
limit =
|
||||
if @template_folders.size < 4
|
||||
TEMPLATES_PER_PAGE
|
||||
else
|
||||
(@template_folders.size < 7 ? 9 : 6)
|
||||
end
|
||||
|
||||
@pagy, @templates = pagy_auto(@templates, limit:)
|
||||
|
||||
load_related_submissions if params[:q].present? && @templates.blank?
|
||||
else
|
||||
@pagy, @template_folders = pagy(@template_folders, limit: FOLDERS_PER_PAGE)
|
||||
|
||||
@templates = @templates.none
|
||||
end
|
||||
end
|
||||
|
||||
def edit; end
|
||||
@@ -25,7 +53,34 @@ class TemplateFoldersController < ApplicationController
|
||||
|
||||
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
|
||||
params.require(:template_folder).permit(:name)
|
||||
end
|
||||
|
||||
def load_related_submissions
|
||||
@related_submissions =
|
||||
Submission.accessible_by(current_ability)
|
||||
.where(archived_at: nil)
|
||||
.where(template_id: current_account.templates.active
|
||||
.where(folder: [@template_folder, *@template_folder.subfolders])
|
||||
.select(:id))
|
||||
.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
|
||||
|
||||
@@ -4,9 +4,27 @@ class TemplatesArchivedController < ApplicationController
|
||||
load_and_authorize_resource :template, parent: false
|
||||
|
||||
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, :template_accesses, folder: :parent_folder)
|
||||
.order(id: :desc)
|
||||
|
||||
@templates = Templates.search(current_user, @templates, params[:q])
|
||||
|
||||
@pagy, @templates = pagy_auto(@templates, limit: 12)
|
||||
|
||||
return unless params[:q].present? && @templates.blank?
|
||||
|
||||
@related_submissions =
|
||||
Submission.accessible_by(current_ability)
|
||||
.joins(:template)
|
||||
.where.not(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
|
||||
|
||||
@@ -4,7 +4,7 @@ class TemplatesCloneAndReplaceController < ApplicationController
|
||||
load_and_authorize_resource :template
|
||||
|
||||
def create
|
||||
return head :unprocessable_entity if params[:files].blank?
|
||||
return head :unprocessable_content if params[:files].blank?
|
||||
|
||||
ActiveRecord::Associations::Preloader.new(
|
||||
records: [@template],
|
||||
@@ -17,6 +17,8 @@ class TemplatesCloneAndReplaceController < ApplicationController
|
||||
|
||||
documents = Templates::ReplaceAttachments.call(cloned_template, params, extract_fields: true)
|
||||
|
||||
Templates.maybe_assign_access(cloned_template)
|
||||
|
||||
cloned_template.save!
|
||||
|
||||
Templates::CloneAttachments.call(template: cloned_template, original_template: @template,
|
||||
@@ -31,7 +33,7 @@ class TemplatesCloneAndReplaceController < ApplicationController
|
||||
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 }
|
||||
f.json { render json: { error: 'PDF encrypted', status: 'pdf_encrypted' }, status: :unprocessable_content }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class TemplatesCloneController < ApplicationController
|
||||
load_and_authorize_resource :template, instance_name: :base_template
|
||||
|
||||
def new
|
||||
authorize!(:create, Template)
|
||||
|
||||
@template = Template.new(name: "#{@base_template.name} (#{I18n.t('clone')})")
|
||||
end
|
||||
|
||||
def create
|
||||
ActiveRecord::Associations::Preloader.new(
|
||||
records: [@base_template],
|
||||
associations: [schema_documents: :preview_images_attachments]
|
||||
).call
|
||||
|
||||
@template = Templates::Clone.call(@base_template, author: current_user,
|
||||
name: params.dig(:template, :name),
|
||||
folder_name: params[:folder_name])
|
||||
|
||||
authorize!(:create, @template)
|
||||
|
||||
if params[:account_id].present? && true_ability.authorize!(:manage, Account.find(params[:account_id]))
|
||||
@template.account_id = params[:account_id]
|
||||
@template.author = true_user if true_user.account_id == @template.account_id
|
||||
@template.folder = @template.account.default_template_folder if @template.account_id != current_account.id
|
||||
else
|
||||
@template.account = current_account
|
||||
end
|
||||
|
||||
Templates.maybe_assign_access(@template)
|
||||
|
||||
if @template.save
|
||||
Templates::CloneAttachments.call(template: @template, original_template: @base_template)
|
||||
|
||||
SearchEntries.enqueue_reindex(@template)
|
||||
|
||||
WebhookUrls.enqueue_events(@template, 'template.created')
|
||||
|
||||
maybe_redirect_to_template(@template)
|
||||
else
|
||||
render turbo_stream: turbo_stream.replace(:modal, partial: 'templates_clone/form'), status: :unprocessable_content
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def maybe_redirect_to_template(template)
|
||||
if template.account == current_account
|
||||
redirect_to(edit_template_path(template))
|
||||
else
|
||||
redirect_back(fallback_location: root_path, notice: I18n.t('template_has_been_cloned'))
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -3,8 +3,6 @@
|
||||
class TemplatesController < ApplicationController
|
||||
load_and_authorize_resource :template
|
||||
|
||||
before_action :load_base_template, only: %i[new create]
|
||||
|
||||
def show
|
||||
submissions = @template.submissions.accessible_by(current_ability)
|
||||
submissions = submissions.active if @template.archived_at.blank?
|
||||
@@ -26,9 +24,7 @@ class TemplatesController < ApplicationController
|
||||
redirect_to root_path
|
||||
end
|
||||
|
||||
def new
|
||||
@template.name = "#{@base_template.name} (#{I18n.t('clone')})" if @base_template
|
||||
end
|
||||
def new; end
|
||||
|
||||
def edit
|
||||
ActiveRecord::Associations::Preloader.new(
|
||||
@@ -48,37 +44,20 @@ class TemplatesController < ApplicationController
|
||||
end
|
||||
|
||||
def create
|
||||
if @base_template
|
||||
ActiveRecord::Associations::Preloader.new(
|
||||
records: [@base_template],
|
||||
associations: [schema_documents: :preview_images_attachments]
|
||||
).call
|
||||
@template.author = current_user
|
||||
@template.folder = TemplateFolders.find_or_create_by_name(current_user, params[:folder_name])
|
||||
@template.account = current_account
|
||||
|
||||
@template = Templates::Clone.call(@base_template, author: current_user,
|
||||
name: params.dig(:template, :name),
|
||||
folder_name: params[:folder_name])
|
||||
else
|
||||
@template.author = current_user
|
||||
@template.folder = TemplateFolders.find_or_create_by_name(current_user, params[:folder_name])
|
||||
end
|
||||
|
||||
if params[:account_id].present? && authorized_clone_account_id?(params[:account_id])
|
||||
@template.account_id = params[:account_id]
|
||||
@template.folder = @template.account.default_template_folder if @template.account_id != current_account.id
|
||||
else
|
||||
@template.account = current_account
|
||||
end
|
||||
Templates.maybe_assign_access(@template)
|
||||
|
||||
if @template.save
|
||||
Templates::CloneAttachments.call(template: @template, original_template: @base_template) if @base_template
|
||||
|
||||
SearchEntries.enqueue_reindex(@template)
|
||||
|
||||
enqueue_template_created_webhooks(@template)
|
||||
WebhookUrls.enqueue_events(@template, 'template.created')
|
||||
|
||||
maybe_redirect_to_template(@template)
|
||||
redirect_to(edit_template_path(@template))
|
||||
else
|
||||
render turbo_stream: turbo_stream.replace(:modal, template: 'templates/new'), status: :unprocessable_entity
|
||||
render turbo_stream: turbo_stream.replace(:modal, template: 'templates/new'), status: :unprocessable_content
|
||||
end
|
||||
end
|
||||
|
||||
@@ -91,7 +70,7 @@ class TemplatesController < ApplicationController
|
||||
|
||||
SearchEntries.enqueue_reindex(@template) if is_name_changed
|
||||
|
||||
enqueue_template_updated_webhooks(@template)
|
||||
WebhookUrls.enqueue_events(@template, 'template.updated')
|
||||
|
||||
head :ok
|
||||
end
|
||||
@@ -116,49 +95,18 @@ class TemplatesController < ApplicationController
|
||||
def template_params
|
||||
params.require(:template).permit(
|
||||
:name,
|
||||
{ schema: [[:attachment_uuid, :name, { conditions: [%i[field_uuid value action operation]] }]],
|
||||
submitters: [%i[name uuid is_requester linked_to_uuid invite_by_uuid optional_invite_by_uuid email]],
|
||||
{ schema: [[:attachment_uuid, :google_drive_file_id, :name,
|
||||
{ conditions: [%i[field_uuid value action operation]] }]],
|
||||
submitters: [%i[name uuid is_requester linked_to_uuid invite_by_uuid optional_invite_by_uuid email order]],
|
||||
fields: [[:uuid, :submitter_uuid, :name, :type,
|
||||
:required, :readonly, :default_value,
|
||||
:title, :description,
|
||||
:title, :description, :prefillable,
|
||||
{ preferences: {},
|
||||
default_value: [],
|
||||
conditions: [%i[field_uuid value action operation]],
|
||||
options: [%i[value uuid]],
|
||||
validation: %i[message pattern],
|
||||
validation: %i[message pattern min max step],
|
||||
areas: [%i[x y w h cell_w attachment_uuid option_uuid page]] }]] }
|
||||
)
|
||||
end
|
||||
|
||||
def authorized_clone_account_id?(account_id)
|
||||
true_user.account_id.to_s == account_id.to_s ||
|
||||
true_user.account.linked_accounts.accessible_by(current_ability).exists?(id: account_id)
|
||||
end
|
||||
|
||||
def maybe_redirect_to_template(template)
|
||||
if template.account == current_account
|
||||
redirect_to(edit_template_path(@template))
|
||||
else
|
||||
redirect_back(fallback_location: root_path, notice: I18n.t('template_has_been_cloned'))
|
||||
end
|
||||
end
|
||||
|
||||
def enqueue_template_created_webhooks(template)
|
||||
WebhookUrls.for_account_id(template.account_id, 'template.created').each do |webhook_url|
|
||||
SendTemplateCreatedWebhookRequestJob.perform_async('template_id' => template.id,
|
||||
'webhook_url_id' => webhook_url.id)
|
||||
end
|
||||
end
|
||||
|
||||
def enqueue_template_updated_webhooks(template)
|
||||
WebhookUrls.for_account_id(template.account_id, 'template.updated').each do |webhook_url|
|
||||
SendTemplateUpdatedWebhookRequestJob.perform_async('template_id' => template.id,
|
||||
'webhook_url_id' => webhook_url.id)
|
||||
end
|
||||
end
|
||||
|
||||
def load_base_template
|
||||
return if params[:base_template_id].blank?
|
||||
|
||||
@base_template = Template.accessible_by(current_ability).find_by(id: params[:base_template_id])
|
||||
end
|
||||
end
|
||||
|
||||
@@ -8,12 +8,14 @@ class TemplatesDashboardController < ApplicationController
|
||||
TEMPLATES_PER_PAGE = 12
|
||||
FOLDERS_PER_PAGE = 18
|
||||
|
||||
helper_method :selected_order
|
||||
|
||||
def index
|
||||
@template_folders = @template_folders.where(id: @templates.active.select(:folder_id))
|
||||
@template_folders =
|
||||
TemplateFolders.filter_active_folders(@template_folders.where(parent_folder_id: nil), @templates)
|
||||
|
||||
@template_folders = TemplateFolders.search(@template_folders, params[:q])
|
||||
@template_folders = sort_template_folders(@template_folders, current_user,
|
||||
cookies.permanent[:dashboard_templates_order])
|
||||
@template_folders = TemplateFolders.sort(@template_folders, current_user, selected_order)
|
||||
|
||||
@pagy, @template_folders = pagy(
|
||||
@template_folders,
|
||||
@@ -26,7 +28,7 @@ class TemplatesDashboardController < ApplicationController
|
||||
else
|
||||
@template_folders = @template_folders.reject { |e| e.name == TemplateFolder::DEFAULT_NAME }
|
||||
@templates = filter_templates(@templates).preload(:author, :template_accesses)
|
||||
@templates = Templates::Order.call(@templates, current_user, cookies.permanent[:dashboard_templates_order])
|
||||
@templates = Templates::Order.call(@templates, current_user, selected_order)
|
||||
|
||||
limit =
|
||||
if @template_folders.size < 4
|
||||
@@ -36,6 +38,8 @@ class TemplatesDashboardController < ApplicationController
|
||||
end
|
||||
|
||||
@pagy, @templates = pagy_auto(@templates, limit:)
|
||||
|
||||
load_related_submissions if params[:q].present? && @templates.blank?
|
||||
end
|
||||
end
|
||||
|
||||
@@ -53,10 +57,8 @@ class TemplatesDashboardController < ApplicationController
|
||||
|
||||
rel = Template.where(
|
||||
Template.arel_table[:id].in(
|
||||
Arel::Nodes::Union.new(
|
||||
rel.where(folder_id: current_account.default_template_folder.id).select(:id).arel,
|
||||
shared_template_ids.arel
|
||||
)
|
||||
rel.where(folder_id: current_account.default_template_folder.id).select(:id).arel
|
||||
.union(:all, shared_template_ids.arel)
|
||||
)
|
||||
)
|
||||
else
|
||||
@@ -67,37 +69,28 @@ class TemplatesDashboardController < ApplicationController
|
||||
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')
|
||||
)
|
||||
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
|
||||
|
||||
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
|
||||
)
|
||||
def load_related_submissions
|
||||
@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)
|
||||
|
||||
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
|
||||
@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
|
||||
|
||||
@@ -3,19 +3,29 @@
|
||||
class TemplatesDebugController < ApplicationController
|
||||
load_and_authorize_resource :template
|
||||
|
||||
DEBUG_FILE = ''
|
||||
|
||||
def show
|
||||
attachment = @template.documents.first
|
||||
schema_uuids = @template.schema.index_by { |e| e['attachment_uuid'] }
|
||||
attachment = @template.documents.find { |a| schema_uuids[a.uuid] }
|
||||
|
||||
data = attachment.download
|
||||
pdf = HexaPDF::Document.new(io: StringIO.new(data))
|
||||
|
||||
fields = Templates::FindAcroFields.call(pdf, attachment, data)
|
||||
unless attachment.image?
|
||||
pdf = HexaPDF::Document.new(io: StringIO.new(data))
|
||||
|
||||
fields = Templates::FindAcroFields.call(pdf, attachment, data)
|
||||
end
|
||||
|
||||
fields, = Templates::DetectFields.call(StringIO.new(data), attachment:) if fields.blank?
|
||||
|
||||
attachment.metadata['pdf'] ||= {}
|
||||
attachment.metadata['pdf']['fields'] = fields
|
||||
|
||||
@template.update!(fields: Templates::ProcessDocument.normalize_attachment_fields(@template, [attachment]))
|
||||
|
||||
debug_file if DEBUG_FILE.present?
|
||||
|
||||
ActiveRecord::Associations::Preloader.new(
|
||||
records: [@template],
|
||||
associations: [schema_documents: { preview_images_attachments: :blob }]
|
||||
@@ -31,4 +41,27 @@ class TemplatesDebugController < ApplicationController
|
||||
|
||||
render 'templates/edit', layout: 'plain'
|
||||
end
|
||||
|
||||
def debug_file
|
||||
tempfile = Tempfile.new
|
||||
tempfile.binmode
|
||||
tempfile.write(File.read(DEBUG_FILE))
|
||||
tempfile.rewind
|
||||
|
||||
filename = File.basename(DEBUG_FILE)
|
||||
|
||||
file = ActionDispatch::Http::UploadedFile.new(
|
||||
tempfile:,
|
||||
filename:,
|
||||
type: Marcel::MimeType.for(tempfile)
|
||||
)
|
||||
|
||||
params = { files: [file] }
|
||||
|
||||
documents = Templates::CreateAttachments.call(@template, params)
|
||||
|
||||
schema = documents.map { |doc| { attachment_uuid: doc.uuid, name: doc.filename.base } }
|
||||
|
||||
@template.update!(schema:)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class TemplatesDetectFieldsController < ApplicationController
|
||||
include ActionController::Live
|
||||
|
||||
load_and_authorize_resource :template
|
||||
|
||||
def create
|
||||
response.headers['Content-Type'] = 'text/event-stream'
|
||||
|
||||
sse = SSE.new(response.stream)
|
||||
|
||||
documents = @template.schema_documents.preload(:blob)
|
||||
documents = documents.where(uuid: params[:attachment_uuid]) if params[:attachment_uuid].present?
|
||||
|
||||
page_number = params[:page].presence&.to_i
|
||||
|
||||
documents.each do |document|
|
||||
io = StringIO.new(document.download)
|
||||
|
||||
Templates::DetectFields.call(io, attachment: document, page_number:) do |(attachment_uuid, page, fields)|
|
||||
sse.write({ attachment_uuid:, page:, fields: })
|
||||
end
|
||||
end
|
||||
|
||||
sse.write({ completed: true })
|
||||
ensure
|
||||
response.stream.close
|
||||
end
|
||||
end
|
||||
@@ -6,7 +6,9 @@ class TemplatesFoldersController < ApplicationController
|
||||
def edit; end
|
||||
|
||||
def update
|
||||
@template.folder = TemplateFolders.find_or_create_by_name(current_user, params[:name])
|
||||
name = [params[:parent_name], params[:name]].compact_blank.join(' / ')
|
||||
|
||||
@template.folder = TemplateFolders.find_or_create_by_name(current_user, name)
|
||||
|
||||
if @template.save
|
||||
redirect_back(fallback_location: template_path(@template), notice: I18n.t('document_template_has_been_moved'))
|
||||
|
||||
@@ -3,6 +3,15 @@
|
||||
class TemplatesPreferencesController < ApplicationController
|
||||
load_and_authorize_resource :template
|
||||
|
||||
RESETTABLE_PREFERENCE_KEYS = {
|
||||
AccountConfig::SUBMITTER_INVITATION_EMAIL_KEY => %w[request_email_subject request_email_body submitters],
|
||||
AccountConfig::SUBMITTER_INVITATION_REMINDER_EMAIL_KEY => %w[invitation_reminder_email_subject
|
||||
invitation_reminder_email_body],
|
||||
AccountConfig::SUBMITTER_DOCUMENTS_COPY_EMAIL_KEY => %w[documents_copy_email_subject documents_copy_email_body],
|
||||
AccountConfig::SUBMITTER_COMPLETED_EMAIL_KEY => %w[completed_notification_email_subject
|
||||
completed_notification_email_body]
|
||||
}.freeze
|
||||
|
||||
def show; end
|
||||
|
||||
def create
|
||||
@@ -15,23 +24,42 @@ class TemplatesPreferencesController < ApplicationController
|
||||
head :ok
|
||||
end
|
||||
|
||||
def destroy
|
||||
authorize!(:update, @template)
|
||||
|
||||
config_key = params[:config_key]
|
||||
preferences_to_delete = RESETTABLE_PREFERENCE_KEYS[config_key]
|
||||
|
||||
return head :ok if preferences_to_delete.blank?
|
||||
|
||||
preferences_to_delete.each do |key|
|
||||
@template.preferences.delete(key)
|
||||
end
|
||||
|
||||
@template.save!
|
||||
|
||||
render turbo_stream: turbo_stream.replace("#{config_key}_form",
|
||||
partial: "templates_preferences/#{config_key}_form"),
|
||||
status: :ok
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def template_params
|
||||
params.require(:template).permit(
|
||||
preferences: %i[bcc_completed request_email_subject request_email_body
|
||||
invitation_reminder_email_subject invitation_reminder_email_body
|
||||
documents_copy_email_subject documents_copy_email_body
|
||||
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_redirect_url validate_unique_submitters
|
||||
submitters_order require_phone_2fa
|
||||
default_expire_at_duration
|
||||
default_expire_at
|
||||
require_all_submitters submitters_order require_phone_2fa require_email_2fa
|
||||
default_expire_at_duration shared_link_2fa default_expire_at request_email_enabled
|
||||
completed_notification_email_subject completed_notification_email_body
|
||||
completed_notification_email_enabled completed_notification_email_attach_audit] +
|
||||
[completed_message: %i[title body],
|
||||
submitters: [%i[uuid request_email_subject request_email_body]]]
|
||||
submitters: [%i[uuid request_email_subject request_email_body]], link_form_fields: []]
|
||||
).tap do |attrs|
|
||||
attrs[:preferences].delete(:submitters) if params[:request_email_per_submitter] != '1'
|
||||
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class TemplatesPrefillableFieldsController < ApplicationController
|
||||
PREFILLABLE_FIELD_TYPES = %w[text number cells date checkbox select radio phone].freeze
|
||||
|
||||
load_and_authorize_resource :template
|
||||
|
||||
def create
|
||||
authorize!(:update, @template)
|
||||
|
||||
field = @template.fields.find { |f| f['uuid'] == params[:field_uuid] }
|
||||
|
||||
if params[:prefillable] == 'false'
|
||||
field.delete('prefillable')
|
||||
field.delete('readonly')
|
||||
elsif params[:prefillable] == 'true'
|
||||
field['prefillable'] = true
|
||||
field['readonly'] = true
|
||||
end
|
||||
|
||||
@template.save!
|
||||
|
||||
render turbo_stream: turbo_stream.replace(:prefillable_fields_list, partial: 'list',
|
||||
locals: { template: @template })
|
||||
end
|
||||
end
|
||||
@@ -9,6 +9,10 @@ class TemplatesRecipientsController < ApplicationController
|
||||
@template.submitters =
|
||||
submitters_params.map { |s| s.reject { |_, v| v.is_a?(String) && v.blank? } }
|
||||
|
||||
if @template.submitters.each_with_index.all? { |s, index| s['order'] == index }
|
||||
@template.submitters.each { |s| s.delete('order') }
|
||||
end
|
||||
|
||||
@template.save!
|
||||
|
||||
render json: { submitters: @template.submitters }
|
||||
@@ -18,7 +22,7 @@ class TemplatesRecipientsController < ApplicationController
|
||||
|
||||
def submitters_params
|
||||
permit_params = { submitters: [%i[name uuid is_requester optional_invite_by_uuid
|
||||
invite_by_uuid linked_to_uuid email option]] }
|
||||
invite_by_uuid linked_to_uuid email option order]] }
|
||||
|
||||
params.require(:template).permit(permit_params).fetch(:submitters, {}).values.filter_map do |s|
|
||||
next if s[:uuid].blank?
|
||||
@@ -29,6 +33,7 @@ class TemplatesRecipientsController < ApplicationController
|
||||
s.delete(:is_requester)
|
||||
end
|
||||
|
||||
s[:order] = s[:order].to_i if s[:order].present?
|
||||
s.delete(:invite_by_uuid) if s[:invite_by_uuid].blank?
|
||||
s.delete(:optional_invite_by_uuid) if s[:optional_invite_by_uuid].blank?
|
||||
|
||||
|
||||
@@ -10,7 +10,11 @@ class TemplatesShareLinkController < ApplicationController
|
||||
|
||||
@template.update!(template_params)
|
||||
|
||||
head :ok
|
||||
if params[:redir].present?
|
||||
redirect_to params[:redir]
|
||||
else
|
||||
head :ok
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
@@ -23,7 +23,7 @@ class TemplatesUploadsController < ApplicationController
|
||||
|
||||
@template.update!(schema:)
|
||||
|
||||
enqueue_template_created_webhooks(@template)
|
||||
WebhookUrls.enqueue_events(@template, 'template.created')
|
||||
|
||||
SearchEntries.enqueue_reindex(@template)
|
||||
|
||||
@@ -46,6 +46,8 @@ class TemplatesUploadsController < ApplicationController
|
||||
template.folder = TemplateFolders.find_or_create_by_name(current_user, params[:folder_name])
|
||||
template.name = File.basename((url_params || params)[:files].first.original_filename, '.*')
|
||||
|
||||
Templates.maybe_assign_access(template)
|
||||
|
||||
template.save!
|
||||
|
||||
template
|
||||
@@ -68,11 +70,4 @@ class TemplatesUploadsController < ApplicationController
|
||||
|
||||
{ files: [file] }
|
||||
end
|
||||
|
||||
def enqueue_template_created_webhooks(template)
|
||||
WebhookUrls.for_account_id(template.account_id, 'template.created').each do |webhook_url|
|
||||
SendTemplateCreatedWebhookRequestJob.perform_async('template_id' => template.id,
|
||||
'webhook_url_id' => webhook_url.id)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -32,7 +32,7 @@ class TimestampServerController < ApplicationController
|
||||
uri = Addressable::URI.parse(url)
|
||||
|
||||
conn = Faraday.new(uri.origin) do |c|
|
||||
c.basic_auth(uri.user, uri.password) if uri.password.present?
|
||||
c.request :authorization, :basic, uri.user, uri.password if uri.password.present?
|
||||
end
|
||||
|
||||
response = conn.post(uri.path, req.to_der,
|
||||
|
||||
@@ -24,12 +24,22 @@ class UsersController < ApplicationController
|
||||
def edit; end
|
||||
|
||||
def create
|
||||
if User.accessible_by(current_ability).exists?(email: @user.email)
|
||||
@user.errors.add(:email, I18n.t('already_exists'))
|
||||
existing_user = User.accessible_by(current_ability).find_by(email: @user.email)
|
||||
|
||||
return render turbo_stream: turbo_stream.replace(:modal, template: 'users/new'), status: :unprocessable_entity
|
||||
if existing_user
|
||||
if existing_user.archived_at? &&
|
||||
current_ability.can?(:manage, existing_user) && current_ability.can?(:manage, @user.account)
|
||||
existing_user.assign_attributes(@user.slice(:first_name, :last_name, :role, :account_id))
|
||||
existing_user.archived_at = nil
|
||||
@user = existing_user
|
||||
else
|
||||
@user.errors.add(:email, I18n.t('already_exists'))
|
||||
|
||||
return render turbo_stream: turbo_stream.replace(:modal, template: 'users/new'), status: :unprocessable_content
|
||||
end
|
||||
end
|
||||
|
||||
@user.password = SecureRandom.hex if @user.password.blank?
|
||||
@user.role = User::ADMIN_ROLE unless role_valid?(@user.role)
|
||||
|
||||
if @user.save
|
||||
@@ -37,14 +47,15 @@ class UsersController < ApplicationController
|
||||
|
||||
redirect_back fallback_location: settings_users_path, notice: I18n.t('user_has_been_invited')
|
||||
else
|
||||
render turbo_stream: turbo_stream.replace(:modal, template: 'users/new'), status: :unprocessable_entity
|
||||
render turbo_stream: turbo_stream.replace(:modal, template: 'users/new'), status: :unprocessable_content
|
||||
end
|
||||
end
|
||||
|
||||
def update
|
||||
return redirect_to settings_users_path, notice: I18n.t('unable_to_update_user') if Docuseal.demo?
|
||||
|
||||
attrs = user_params.compact_blank.merge(user_params.slice(:archived_at))
|
||||
attrs = user_params.compact_blank
|
||||
attrs = attrs.merge(user_params.slice(:archived_at)) if current_ability.can?(:create, @user)
|
||||
|
||||
if params.dig(:user, :account_id).present?
|
||||
account = Account.accessible_by(current_ability).find(params.dig(:user, :account_id))
|
||||
@@ -54,10 +65,17 @@ class UsersController < ApplicationController
|
||||
@user.account = account
|
||||
end
|
||||
|
||||
if @user.update(attrs.except(current_user == @user ? :role : nil))
|
||||
redirect_back fallback_location: settings_users_path, notice: I18n.t('user_has_been_updated')
|
||||
if @user.update(attrs.except(*(current_user == @user ? %i[password otp_required_for_login role] : %i[password])))
|
||||
if @user.try(:pending_reconfirmation?) && @user.previous_changes.key?(:unconfirmed_email)
|
||||
SendConfirmationInstructionsJob.perform_async('user_id' => @user.id)
|
||||
|
||||
redirect_back fallback_location: settings_users_path,
|
||||
notice: I18n.t('a_confirmation_email_has_been_sent_to_the_new_email_address')
|
||||
else
|
||||
redirect_back fallback_location: settings_users_path, notice: I18n.t('user_has_been_updated')
|
||||
end
|
||||
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_content
|
||||
end
|
||||
end
|
||||
|
||||
@@ -83,7 +101,7 @@ class UsersController < ApplicationController
|
||||
|
||||
def user_params
|
||||
if params.key?(:user)
|
||||
permitted_params = %i[email first_name last_name password archived_at]
|
||||
permitted_params = %i[email first_name last_name password archived_at otp_required_for_login]
|
||||
|
||||
permitted_params << :role if role_valid?(params.dig(:user, :role))
|
||||
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class UsersSendResetPasswordController < ApplicationController
|
||||
load_and_authorize_resource :user
|
||||
|
||||
LIMIT_DURATION = 10.minutes
|
||||
|
||||
def update
|
||||
authorize!(:manage, @user)
|
||||
|
||||
if @user.reset_password_sent_at && @user.reset_password_sent_at > LIMIT_DURATION.ago
|
||||
redirect_back fallback_location: settings_users_path, notice: I18n.t('email_has_been_sent_already')
|
||||
else
|
||||
@user.send_reset_password_instructions
|
||||
|
||||
redirect_back fallback_location: settings_users_path,
|
||||
notice: I18n.t('an_email_with_password_reset_instructions_has_been_sent')
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,66 @@
|
||||
# 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
|
||||
@@ -8,10 +8,26 @@ class WebhookSettingsController < ApplicationController
|
||||
@webhook_urls = @webhook_urls.order(id: :desc)
|
||||
@webhook_url = @webhook_urls.first_or_initialize
|
||||
|
||||
render @webhook_urls.size > 1 ? 'index' : 'show'
|
||||
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; 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
|
||||
|
||||
@@ -37,13 +53,18 @@ class WebhookSettingsController < ApplicationController
|
||||
def resend
|
||||
submitter = current_account.submitters.where.not(completed_at: nil).order(:id).last
|
||||
|
||||
authorize!(:read, submitter)
|
||||
|
||||
if submitter.blank? || @webhook_url.blank?
|
||||
return redirect_back(fallback_location: settings_webhooks_path,
|
||||
alert: I18n.t('unable_to_resend_webhook_request'))
|
||||
end
|
||||
|
||||
SendFormCompletedWebhookRequestJob.perform_async('submitter_id' => submitter.id,
|
||||
'webhook_url_id' => @webhook_url.id)
|
||||
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
|
||||
|
||||
@@ -23,6 +23,7 @@ import SignatureForm from './elements/signature_form'
|
||||
import SubmitForm from './elements/submit_form'
|
||||
import PromptPassword from './elements/prompt_password'
|
||||
import EmailsTextarea from './elements/emails_textarea'
|
||||
import ToggleSubmit from './elements/toggle_submit'
|
||||
import ToggleOnSubmit from './elements/toggle_on_submit'
|
||||
import CheckOnClick from './elements/check_on_click'
|
||||
import PasswordInput from './elements/password_input'
|
||||
@@ -34,7 +35,24 @@ 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 AppTourStart from './elements/app_tour_start'
|
||||
import DashboardDropzone from './elements/dashboard_dropzone'
|
||||
import RequiredCheckboxGroup from './elements/required_checkbox_group'
|
||||
import PageContainer from './elements/page_container'
|
||||
import EmailEditor from './elements/email_editor'
|
||||
import MountOnClick from './elements/mount_on_click'
|
||||
import RemoveOnEvent from './elements/remove_on_event'
|
||||
import ScrollTo from './elements/scroll_to'
|
||||
import SetValue from './elements/set_value'
|
||||
import ReviewForm from './elements/review_form'
|
||||
import ShowOnValue from './elements/show_on_value'
|
||||
import CustomValidation from './elements/custom_validation'
|
||||
import ToggleClasses from './elements/toggle_classes'
|
||||
import AutosizeField from './elements/autosize_field'
|
||||
import GoogleDriveFilePicker from './elements/google_drive_file_picker'
|
||||
import OpenModal from './elements/open_modal'
|
||||
import BarChart from './elements/bar_chart'
|
||||
import FieldCondition from './elements/field_condition'
|
||||
|
||||
import * as TurboInstantClick from './lib/turbo_instant_click'
|
||||
|
||||
@@ -51,6 +69,9 @@ document.addEventListener('keyup', (e) => {
|
||||
})
|
||||
|
||||
document.addEventListener('turbo:before-fetch-request', encodeMethodIntoRequestBody)
|
||||
document.addEventListener('turbo:before-fetch-request', (event) => {
|
||||
event.detail.fetchOptions.headers['X-Turbo'] = 'true'
|
||||
})
|
||||
document.addEventListener('turbo:submit-end', async (event) => {
|
||||
const resp = event.detail?.formSubmission?.result?.fetchResponse?.response
|
||||
|
||||
@@ -93,6 +114,7 @@ safeRegisterElement('submit-form', SubmitForm)
|
||||
safeRegisterElement('prompt-password', PromptPassword)
|
||||
safeRegisterElement('emails-textarea', EmailsTextarea)
|
||||
safeRegisterElement('toggle-cookies', ToggleCookies)
|
||||
safeRegisterElement('toggle-submit', ToggleSubmit)
|
||||
safeRegisterElement('toggle-on-submit', ToggleOnSubmit)
|
||||
safeRegisterElement('password-input', PasswordInput)
|
||||
safeRegisterElement('search-input', SearchInput)
|
||||
@@ -103,8 +125,25 @@ safeRegisterElement('masked-input', MaskedInput)
|
||||
safeRegisterElement('set-date-button', SetDateButton)
|
||||
safeRegisterElement('indeterminate-checkbox', IndeterminateCheckbox)
|
||||
safeRegisterElement('app-tour', AppTour)
|
||||
safeRegisterElement('app-tour-start', AppTourStart)
|
||||
safeRegisterElement('dashboard-dropzone', DashboardDropzone)
|
||||
safeRegisterElement('check-on-click', CheckOnClick)
|
||||
safeRegisterElement('required-checkbox-group', RequiredCheckboxGroup)
|
||||
safeRegisterElement('page-container', PageContainer)
|
||||
safeRegisterElement('email-editor', EmailEditor)
|
||||
safeRegisterElement('mount-on-click', MountOnClick)
|
||||
safeRegisterElement('remove-on-event', RemoveOnEvent)
|
||||
safeRegisterElement('scroll-to', ScrollTo)
|
||||
safeRegisterElement('set-value', SetValue)
|
||||
safeRegisterElement('review-form', ReviewForm)
|
||||
safeRegisterElement('show-on-value', ShowOnValue)
|
||||
safeRegisterElement('custom-validation', CustomValidation)
|
||||
safeRegisterElement('toggle-classes', ToggleClasses)
|
||||
safeRegisterElement('autosize-field', AutosizeField)
|
||||
safeRegisterElement('google-drive-file-picker', GoogleDriveFilePicker)
|
||||
safeRegisterElement('open-modal', OpenModal)
|
||||
safeRegisterElement('bar-chart', BarChart)
|
||||
safeRegisterElement('field-condition', FieldCondition)
|
||||
|
||||
safeRegisterElement('template-builder', class extends HTMLElement {
|
||||
connectedCallback () {
|
||||
@@ -116,20 +155,26 @@ safeRegisterElement('template-builder', class extends HTMLElement {
|
||||
|
||||
this.app = createApp(TemplateBuilder, {
|
||||
template: reactive(JSON.parse(this.dataset.template)),
|
||||
customFields: reactive(JSON.parse(this.dataset.customFields || '[]')),
|
||||
backgroundColor: '#faf7f5',
|
||||
locale: this.dataset.locale,
|
||||
withPhone: this.dataset.withPhone === 'true',
|
||||
withVerification: ['true', 'false'].includes(this.dataset.withVerification) ? this.dataset.withVerification === 'true' : null,
|
||||
withKba: ['true', 'false'].includes(this.dataset.withKba) ? this.dataset.withKba === 'true' : null,
|
||||
withLogo: this.dataset.withLogo !== 'false',
|
||||
withFieldsDetection: this.dataset.withFieldsDetection === 'true',
|
||||
editable: this.dataset.editable !== 'false',
|
||||
authenticityToken: document.querySelector('meta[name="csrf-token"]')?.content,
|
||||
withCustomFields: true,
|
||||
withPayment: this.dataset.withPayment === 'true',
|
||||
isPaymentConnected: this.dataset.isPaymentConnected === 'true',
|
||||
withFormula: this.dataset.withFormula === 'true',
|
||||
withSendButton: this.dataset.withSendButton !== 'false',
|
||||
withSignYourselfButton: this.dataset.withSignYourselfButton !== 'false',
|
||||
withConditions: this.dataset.withConditions === 'true',
|
||||
withGoogleDrive: this.dataset.withGoogleDrive === 'true',
|
||||
withReplaceAndCloneUpload: true,
|
||||
withDownload: true,
|
||||
currencies: (this.dataset.currencies || '').split(',').filter(Boolean),
|
||||
acceptFileTypes: this.dataset.acceptFileTypes,
|
||||
showTourStartForm: this.dataset.showTourStartForm === 'true'
|
||||
|
||||
@@ -19,7 +19,7 @@ button .disabled {
|
||||
display: none;
|
||||
}
|
||||
|
||||
button[disabled] .disabled {
|
||||
button[disabled] .disabled, button.btn-disabled .disabled {
|
||||
display: initial;
|
||||
}
|
||||
|
||||
@@ -27,7 +27,7 @@ button .enabled {
|
||||
display: initial;
|
||||
}
|
||||
|
||||
button[disabled] .enabled {
|
||||
button[disabled] .enabled, button.btn-disabled .enabled {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@@ -147,3 +147,11 @@ button[disabled] .enabled {
|
||||
outline-offset: 3px;
|
||||
outline-color: hsl(var(--bc) / 0.2);
|
||||
}
|
||||
|
||||
.font-times {
|
||||
font-family: "Times New Roman", Times, ui-serif, serif, Cambria, Georgia;
|
||||
}
|
||||
|
||||
.font-courier {
|
||||
font-family: "Courier New", Consolas, "Liberation Mono", monospace, ui-monospace, SFMono-Regular, Menlo, Monaco;
|
||||
}
|
||||
|
||||
+51
-6
@@ -4,15 +4,26 @@ import { isValidSignatureCanvas } from './submission_form/validate_signature'
|
||||
|
||||
window.customElements.define('draw-signature', class extends HTMLElement {
|
||||
connectedCallback () {
|
||||
const scale = 3
|
||||
|
||||
this.canvas.width = this.canvas.parentNode.clientWidth * scale
|
||||
this.canvas.height = this.canvas.parentNode.clientHeight * scale
|
||||
|
||||
this.canvas.getContext('2d').scale(scale, scale)
|
||||
this.setCanvasSize()
|
||||
|
||||
this.pad = new SignaturePad(this.canvas)
|
||||
|
||||
this.resizeObserver = new ResizeObserver(() => {
|
||||
requestAnimationFrame(() => {
|
||||
if (!this.canvas) return
|
||||
|
||||
const { width, height } = this.canvas
|
||||
|
||||
this.setCanvasSize()
|
||||
|
||||
if (this.canvas.width !== width || this.canvas.height !== height) {
|
||||
this.redrawCanvas(width, height)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
this.resizeObserver.observe(this.canvas.parentNode)
|
||||
|
||||
if (this.dataset.color) {
|
||||
this.pad.penColor = this.dataset.color
|
||||
}
|
||||
@@ -57,6 +68,40 @@ window.customElements.define('draw-signature', class extends HTMLElement {
|
||||
this.updateSubmitButtonVisibility()
|
||||
}
|
||||
|
||||
disconnectedCallback () {
|
||||
if (this.resizeObserver) {
|
||||
this.resizeObserver.disconnect()
|
||||
}
|
||||
}
|
||||
|
||||
setCanvasSize () {
|
||||
const scale = 3
|
||||
|
||||
const width = this.canvas.parentNode.clientWidth
|
||||
const height = this.canvas.parentNode.clientHeight
|
||||
|
||||
if (this.canvas.width !== width * scale || this.canvas.height !== height * scale) {
|
||||
this.canvas.width = width * scale
|
||||
this.canvas.height = height * scale
|
||||
|
||||
this.canvas.getContext('2d').scale(scale, scale)
|
||||
}
|
||||
}
|
||||
|
||||
redrawCanvas (oldWidth, oldHeight) {
|
||||
if (this.pad && !this.pad.isEmpty() && oldWidth > 0 && oldHeight > 0) {
|
||||
const sx = this.canvas.width / oldWidth
|
||||
const sy = this.canvas.height / oldHeight
|
||||
|
||||
const scaledData = this.pad.toData().map((stroke) => ({
|
||||
...stroke,
|
||||
points: stroke.points.map((p) => ({ ...p, x: p.x * sx, y: p.y * sy }))
|
||||
}))
|
||||
|
||||
this.pad.fromData(scaledData)
|
||||
}
|
||||
}
|
||||
|
||||
updateSubmitButtonVisibility () {
|
||||
if (this.pad.isEmpty()) {
|
||||
this.submitButton.style.display = 'none'
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
export default class extends HTMLElement {
|
||||
connectedCallback () {
|
||||
this.querySelector('form').addEventListener('submit', () => {
|
||||
window.app_tour.start()
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
export default class extends HTMLElement {
|
||||
connectedCallback () {
|
||||
const originalFontValue = this.field.style.fontSize
|
||||
|
||||
if (this.field.scrollHeight > this.field.clientHeight) {
|
||||
this.field.style.fontSize = `calc(${originalFontValue} / 1.5)`
|
||||
this.field.style.lineHeight = `calc(${this.field.style.fontSize} * 1.3)`
|
||||
|
||||
if (this.field.scrollHeight > this.field.clientHeight) {
|
||||
this.field.style.fontSize = `calc(${originalFontValue} / 2.0)`
|
||||
this.field.style.lineHeight = `calc(${this.field.style.fontSize} * 1.3)`
|
||||
}
|
||||
}
|
||||
|
||||
this.field.classList.remove('hidden')
|
||||
}
|
||||
|
||||
get field () {
|
||||
return this.closest('field-value')
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
export default class extends HTMLElement {
|
||||
connectedCallback () {
|
||||
this.chartLabels = JSON.parse(this.dataset.labels || '[]')
|
||||
this.chartDatasets = JSON.parse(this.dataset.datasets || '[]')
|
||||
|
||||
this.initChart()
|
||||
}
|
||||
|
||||
disconnectedCallback () {
|
||||
if (this.chartInstance) {
|
||||
this.chartInstance.destroy()
|
||||
this.chartInstance = null
|
||||
}
|
||||
}
|
||||
|
||||
async initChart () {
|
||||
const { default: Chart } = await import(/* webpackChunkName: "chartjs" */ 'chart.js/auto')
|
||||
|
||||
const canvas = this.querySelector('canvas')
|
||||
|
||||
const ctx = canvas.getContext('2d')
|
||||
|
||||
this.chartInstance = new Chart(ctx, {
|
||||
type: 'bar',
|
||||
data: {
|
||||
labels: this.chartLabels,
|
||||
datasets: this.chartDatasets
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
maintainAspectRatio: true,
|
||||
animation: false,
|
||||
scales: {
|
||||
y: {
|
||||
beginAtZero: true,
|
||||
grace: '20%',
|
||||
ticks: {
|
||||
precision: 0
|
||||
}
|
||||
}
|
||||
},
|
||||
plugins: {
|
||||
legend: {
|
||||
display: false
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
export default class extends HTMLElement {
|
||||
connectedCallback () {
|
||||
this.addEventListener('click', () => {
|
||||
if (!this.element.checked) {
|
||||
if (this.element && !this.element.disabled && !this.element.checked) {
|
||||
this.element.checked = true
|
||||
this.element.dispatchEvent(new Event('change', { bubbles: true }))
|
||||
}
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
export default class extends HTMLElement {
|
||||
connectedCallback () {
|
||||
const input = this.querySelector('input')
|
||||
const invalidMessage = this.dataset.invalidMessage || ''
|
||||
|
||||
input.addEventListener('invalid', () => {
|
||||
input.setCustomValidity(input.value ? invalidMessage : '')
|
||||
})
|
||||
|
||||
input.addEventListener('input', () => {
|
||||
input.setCustomValidity('')
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -15,6 +15,7 @@ export default targetable(class extends HTMLElement {
|
||||
static [target.static] = [
|
||||
'form',
|
||||
'fileDropzone',
|
||||
'folderDropzone',
|
||||
'fileDropzoneLoading'
|
||||
]
|
||||
|
||||
@@ -25,12 +26,13 @@ export default targetable(class extends HTMLElement {
|
||||
window.addEventListener('dragleave', this.onWindowDragleave)
|
||||
|
||||
this.fileDropzone?.addEventListener('drop', this.onDropFile)
|
||||
this.folderDropzone?.addEventListener('drop', this.onDropNewFolder)
|
||||
|
||||
this.folderCards.forEach((el) => el.addEventListener('drop', (e) => this.onDropFolder(e, el)))
|
||||
this.templateCards.forEach((el) => el.addEventListener('drop', this.onDropTemplate))
|
||||
this.templateCards.forEach((el) => el.addEventListener('dragstart', this.onTemplateDragStart))
|
||||
|
||||
return [this.fileDropzone, ...this.folderCards, ...this.templateCards].forEach((el) => {
|
||||
return [this.fileDropzone, this.folderDropzone, ...this.folderCards, ...this.templateCards].forEach((el) => {
|
||||
el?.addEventListener('dragover', this.onDragover)
|
||||
el?.addEventListener('dragleave', this.onDragleave)
|
||||
})
|
||||
@@ -46,6 +48,10 @@ export default targetable(class extends HTMLElement {
|
||||
onTemplateDragStart = (e) => {
|
||||
const id = e.target.href.split('/').pop()
|
||||
|
||||
this.folderCards.forEach((el) => el.classList.remove('bg-base-200', 'before:hidden'))
|
||||
this.folderDropzone?.classList?.remove('hidden')
|
||||
window.flash?.remove()
|
||||
|
||||
e.dataTransfer.effectAllowed = 'move'
|
||||
|
||||
if (id) {
|
||||
@@ -98,13 +104,13 @@ export default targetable(class extends HTMLElement {
|
||||
el.classList.add('opacity-50')
|
||||
|
||||
if (e.dataTransfer.files.length) {
|
||||
const params = new URLSearchParams({ folder_name: el.innerText }).toString()
|
||||
const params = new URLSearchParams({ folder_name: el.innerText.trim() }).toString()
|
||||
|
||||
this.uploadFiles(e.dataTransfer.files, `/templates_upload?${params}`)
|
||||
} else {
|
||||
const formData = new FormData()
|
||||
|
||||
formData.append('name', el.innerText)
|
||||
formData.append('name', el.dataset.fullName)
|
||||
|
||||
fetch(`/templates/${templateId}/folder`, {
|
||||
method: 'PUT',
|
||||
@@ -176,6 +182,24 @@ export default targetable(class extends HTMLElement {
|
||||
}
|
||||
}
|
||||
|
||||
onDropNewFolder (e) {
|
||||
e.preventDefault()
|
||||
|
||||
const templateId = e.dataTransfer.getData('template_id')
|
||||
|
||||
const a = document.createElement('a')
|
||||
|
||||
a.href = `/templates/${templateId}/folder/edit?autocomplete=false`
|
||||
a.dataset.turboFrame = 'modal'
|
||||
a.classList.add('hidden')
|
||||
|
||||
document.body.append(a)
|
||||
|
||||
a.click()
|
||||
|
||||
a.remove()
|
||||
}
|
||||
|
||||
onDragleave () {
|
||||
this.style.backgroundColor = null
|
||||
|
||||
@@ -199,6 +223,7 @@ export default targetable(class extends HTMLElement {
|
||||
|
||||
this.isDrag = true
|
||||
|
||||
window.flash?.remove()
|
||||
this.fileDropzone?.classList?.remove('hidden')
|
||||
|
||||
this.hiddenOnDrag.forEach((el) => { el.style.display = 'none' })
|
||||
@@ -212,6 +237,7 @@ export default targetable(class extends HTMLElement {
|
||||
this.isDrag = false
|
||||
|
||||
this.fileDropzone?.classList?.add('hidden')
|
||||
this.folderDropzone?.classList?.add('hidden')
|
||||
|
||||
this.hiddenOnDrag.forEach((el) => { el.style.display = null })
|
||||
|
||||
|
||||
@@ -0,0 +1,131 @@
|
||||
import { target, targetable } from '@github/catalyst/lib/targetable'
|
||||
|
||||
let loaderPromise = null
|
||||
|
||||
function loadCodeMirror () {
|
||||
if (!loaderPromise) {
|
||||
loaderPromise = Promise.all([
|
||||
import(/* webpackChunkName: "email-editor" */ '@codemirror/view'),
|
||||
import(/* webpackChunkName: "email-editor" */ '@codemirror/commands'),
|
||||
import(/* webpackChunkName: "email-editor" */ '@codemirror/language'),
|
||||
import(/* webpackChunkName: "email-editor" */ '@codemirror/lang-html'),
|
||||
import(/* webpackChunkName: "email-editor" */ '@specious/htmlflow')
|
||||
]).then(([view, commands, language, html, htmlflow]) => {
|
||||
return {
|
||||
minimalSetup: [
|
||||
commands.history(),
|
||||
language.syntaxHighlighting(language.defaultHighlightStyle, { fallback: true }),
|
||||
view.keymap.of([...commands.defaultKeymap, ...commands.historyKeymap])
|
||||
],
|
||||
EditorView: view.EditorView,
|
||||
html: html.html,
|
||||
htmlflow: htmlflow.default || htmlflow
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
return loaderPromise
|
||||
}
|
||||
|
||||
export default targetable(class extends HTMLElement {
|
||||
static [target.static] = [
|
||||
'codeViewTab',
|
||||
'previewViewTab',
|
||||
'editorContainer',
|
||||
'previewIframe'
|
||||
]
|
||||
|
||||
connectedCallback () {
|
||||
this.mount()
|
||||
|
||||
if (this.input.value) {
|
||||
this.showPreviewView()
|
||||
} else {
|
||||
this.showCodeView()
|
||||
}
|
||||
|
||||
this.previewViewTab.addEventListener('click', this.showPreviewView)
|
||||
this.codeViewTab.addEventListener('click', this.showCodeView)
|
||||
}
|
||||
|
||||
showCodeView = () => {
|
||||
this.editorView.dispatch({
|
||||
changes: { from: 0, to: this.editorView.state.doc.length, insert: this.input.value }
|
||||
})
|
||||
|
||||
this.previewViewTab.classList.remove('tab-active', 'tab-bordered')
|
||||
this.previewViewTab.classList.add('pb-[3px]')
|
||||
this.codeViewTab.classList.remove('pb-[3px]')
|
||||
this.codeViewTab.classList.add('tab-active', 'tab-bordered')
|
||||
this.editorContainer.classList.remove('hidden')
|
||||
this.previewIframe.classList.add('hidden')
|
||||
}
|
||||
|
||||
showPreviewView = () => {
|
||||
this.previewIframe.srcdoc = this.input.value
|
||||
|
||||
this.codeViewTab.classList.remove('tab-active', 'tab-bordered')
|
||||
this.codeViewTab.classList.add('pb-[3px]')
|
||||
this.previewViewTab.classList.remove('pb-[3px]')
|
||||
this.previewViewTab.classList.add('tab-active', 'tab-bordered')
|
||||
this.editorContainer.classList.add('hidden')
|
||||
this.previewIframe.classList.remove('hidden')
|
||||
}
|
||||
|
||||
async mount () {
|
||||
this.input = this.querySelector('input[type="hidden"]')
|
||||
this.input.style.display = 'none'
|
||||
|
||||
const { EditorView, minimalSetup, html, htmlflow } = await loadCodeMirror()
|
||||
|
||||
this.editorView = new EditorView({
|
||||
doc: this.input.value,
|
||||
parent: this.editorContainer,
|
||||
extensions: [
|
||||
html(),
|
||||
minimalSetup,
|
||||
EditorView.lineWrapping,
|
||||
EditorView.updateListener.of(update => {
|
||||
if (update.docChanged) this.input.value = update.state.doc.toString()
|
||||
}),
|
||||
EditorView.theme({
|
||||
'&': {
|
||||
backgroundColor: 'white',
|
||||
color: 'black',
|
||||
fontSize: '14px',
|
||||
fontFamily: 'monospace'
|
||||
},
|
||||
'&.cm-focused': {
|
||||
outline: 'none'
|
||||
},
|
||||
'&.cm-editor': {
|
||||
borderRadius: '0.375rem',
|
||||
border: 'none'
|
||||
},
|
||||
'.cm-gutters': {
|
||||
display: 'none'
|
||||
}
|
||||
})
|
||||
]
|
||||
})
|
||||
|
||||
this.previewIframe.srcdoc = this.editorView.state.doc.toString()
|
||||
|
||||
this.previewIframe.onload = () => {
|
||||
const previewIframeDoc = this.previewIframe.contentDocument
|
||||
|
||||
if (previewIframeDoc.body) {
|
||||
previewIframeDoc.body.contentEditable = true
|
||||
}
|
||||
|
||||
const contentDocument = this.previewIframe.contentDocument || this.previewIframe.contentWindow.document
|
||||
|
||||
contentDocument.body.addEventListener('input', async () => {
|
||||
const html = contentDocument.documentElement.outerHTML.replace(' contenteditable="true"', '')
|
||||
const prettifiedHtml = await htmlflow(html)
|
||||
|
||||
this.input.value = prettifiedHtml
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,167 @@
|
||||
export default class extends HTMLElement {
|
||||
connectedCallback () {
|
||||
this.targetId = this.dataset.targetId
|
||||
this.fieldId = this.dataset.fieldId
|
||||
this.action = (this.dataset.action || '').trim()
|
||||
this.expectedValue = this.dataset.value
|
||||
|
||||
this.targetEl = document.getElementById(this.targetId)
|
||||
this.sourceEl = document.getElementById(this.fieldId)
|
||||
|
||||
this.bindListeners()
|
||||
|
||||
this.evaluateAndApply()
|
||||
}
|
||||
|
||||
disconnectedCallback () {
|
||||
this.unbindListeners()
|
||||
}
|
||||
|
||||
bindListeners () {
|
||||
this.eventsFor(this.sourceEl).forEach((ev) => {
|
||||
this.sourceEl.addEventListener(ev, this.evaluateAndApply)
|
||||
})
|
||||
}
|
||||
|
||||
unbindListeners () {
|
||||
this.eventsFor(this.sourceEl).forEach((ev) => {
|
||||
this.sourceEl.removeEventListener(ev, this.evaluateAndApply)
|
||||
})
|
||||
}
|
||||
|
||||
eventsFor (el) {
|
||||
if (!el) return []
|
||||
|
||||
const tag = el.tagName.toLowerCase()
|
||||
|
||||
if (tag === 'textarea') return ['input']
|
||||
if (tag === 'input') return ['input', 'change']
|
||||
|
||||
return ['change']
|
||||
}
|
||||
|
||||
evaluateAndApply = () => {
|
||||
const fieldConditions = document.querySelectorAll(`field-condition[data-target-id="${this.targetId}"]`)
|
||||
|
||||
const result = [...fieldConditions].reduce((acc, cond) => {
|
||||
if (cond.dataset.operation === 'or') {
|
||||
acc.push(acc.pop() || cond.checkCondition())
|
||||
} else {
|
||||
acc.push(cond.checkCondition())
|
||||
}
|
||||
|
||||
return acc
|
||||
}, [])
|
||||
|
||||
this.apply(!result.includes(false))
|
||||
}
|
||||
|
||||
checkCondition () {
|
||||
const action = this.action
|
||||
const actual = this.getSourceValue()
|
||||
const expected = this.expectedValue
|
||||
|
||||
if (action === 'empty' || action === 'unchecked') return this.isEmpty(actual)
|
||||
if (action === 'not_empty' || action === 'checked') return !this.isEmpty(actual)
|
||||
|
||||
if (['equal', 'not_equal', 'greater_than', 'less_than'].includes(action) && this.sourceEl?.getAttribute('type') === 'number') {
|
||||
if (this.isEmpty(actual) || this.isEmpty(expected)) return false
|
||||
|
||||
const actualNumber = parseFloat(actual)
|
||||
const expectedNumber = parseFloat(expected)
|
||||
|
||||
if (Number.isNaN(actualNumber) || Number.isNaN(expectedNumber)) return false
|
||||
|
||||
if (action === 'equal') return Math.abs(actualNumber - expectedNumber) < Number.EPSILON
|
||||
if (action === 'not_equal') return Math.abs(actualNumber - expectedNumber) > Number.EPSILON
|
||||
if (action === 'greater_than') return actualNumber > expectedNumber
|
||||
if (action === 'less_than') return actualNumber < expectedNumber
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
if (action === 'equal') {
|
||||
const list = Array.isArray(actual) ? actual : [actual]
|
||||
return list.filter((v) => v !== null && v !== undefined).map(String).includes(String(expected))
|
||||
}
|
||||
|
||||
if (action === 'contains') return this.contains(actual, expected)
|
||||
|
||||
if (action === 'not_equal') {
|
||||
const list = Array.isArray(actual) ? actual : [actual]
|
||||
return !list.filter((v) => v !== null && v !== undefined).map(String).includes(String(expected))
|
||||
}
|
||||
|
||||
if (action === 'does_not_contain') return !this.contains(actual, expected)
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
getSourceValue () {
|
||||
const el = this.sourceEl
|
||||
|
||||
if (!el) return
|
||||
|
||||
const tag = el.tagName.toLowerCase()
|
||||
const type = (el.getAttribute('type') || '').toLowerCase()
|
||||
|
||||
if (tag === 'select') return el.value
|
||||
if (tag === 'textarea') return el.value
|
||||
if (tag === 'input' && type === 'checkbox') return el.checked ? (el.value || '1') : null
|
||||
if (tag === 'input') return el.value
|
||||
|
||||
return el.value ?? null
|
||||
}
|
||||
|
||||
isEmpty (obj) {
|
||||
if (obj == null) return true
|
||||
|
||||
if (Array.isArray(obj)) {
|
||||
return obj.length === 0
|
||||
}
|
||||
|
||||
if (typeof obj === 'string') {
|
||||
return obj.trim().length === 0
|
||||
}
|
||||
|
||||
if (typeof obj === 'object') {
|
||||
return Object.keys(obj).length === 0
|
||||
}
|
||||
|
||||
if (obj === false) {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
contains (actual, expected) {
|
||||
if (expected === null || expected === undefined) return false
|
||||
|
||||
const exp = String(expected)
|
||||
|
||||
if (Array.isArray(actual)) return actual.filter((v) => v !== null && v !== undefined).map(String).includes(exp)
|
||||
|
||||
if (typeof actual === 'string') return actual.includes(exp)
|
||||
|
||||
return actual !== null && actual !== undefined && String(actual) === exp
|
||||
}
|
||||
|
||||
apply (passed) {
|
||||
const controls = this.targetEl.matches('input, select, textarea, button')
|
||||
? [this.targetEl]
|
||||
: Array.from(this.targetEl.querySelectorAll('input, select, textarea, button'))
|
||||
|
||||
if (passed) {
|
||||
this.targetEl.style.display = ''
|
||||
this.targetEl.labels.forEach((label) => { label.style.display = '' })
|
||||
|
||||
controls.forEach((c) => (c.disabled = false))
|
||||
} else {
|
||||
this.targetEl.style.display = 'none'
|
||||
this.targetEl.labels.forEach((label) => { label.style.display = 'none' })
|
||||
|
||||
controls.forEach((c) => (c.disabled = true))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,8 @@ import autocomplete from 'autocompleter'
|
||||
|
||||
export default class extends HTMLElement {
|
||||
connectedCallback () {
|
||||
if (this.dataset.enabled === 'false') return
|
||||
|
||||
autocomplete({
|
||||
input: this.input,
|
||||
preventSubmit: this.dataset.submitOnSelect === 'true' ? 0 : 1,
|
||||
@@ -14,12 +16,16 @@ export default class extends HTMLElement {
|
||||
}
|
||||
|
||||
onSelect = (item) => {
|
||||
this.input.value = item.name
|
||||
this.input.value = this.dataset.parentName ? item.name : item.full_name
|
||||
}
|
||||
|
||||
fetch = (text, resolve) => {
|
||||
const queryParams = new URLSearchParams({ q: text })
|
||||
|
||||
if (this.dataset.parentName) {
|
||||
queryParams.append('parent_name', this.dataset.parentName)
|
||||
}
|
||||
|
||||
fetch('/template_folders_autocomplete?' + queryParams).then(async (resp) => {
|
||||
const items = await resp.json()
|
||||
|
||||
@@ -34,7 +40,7 @@ export default class extends HTMLElement {
|
||||
|
||||
div.setAttribute('dir', 'auto')
|
||||
|
||||
div.textContent = item.name
|
||||
div.textContent = this.dataset.parentName ? item.name : item.full_name
|
||||
|
||||
return div
|
||||
}
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
export default class extends HTMLElement {
|
||||
connectedCallback () {
|
||||
const iframeTemplate = this.querySelector('template')
|
||||
|
||||
this.observer = new IntersectionObserver((entries) => {
|
||||
if (entries.some(e => e.isIntersecting)) {
|
||||
iframeTemplate.parentElement.prepend(iframeTemplate.content)
|
||||
|
||||
this.observer.disconnect()
|
||||
}
|
||||
})
|
||||
|
||||
this.observer.observe(this)
|
||||
|
||||
window.addEventListener('message', this.messageHandler)
|
||||
}
|
||||
|
||||
messageHandler = (event) => {
|
||||
if (event.data.type === 'google-drive-files-picked') {
|
||||
this.form.querySelectorAll('input[name="google_drive_file_ids[]"]').forEach(el => el.remove())
|
||||
|
||||
const files = event.data.files || []
|
||||
|
||||
files.forEach((file) => {
|
||||
const input = document.createElement('input')
|
||||
input.type = 'hidden'
|
||||
input.name = 'google_drive_file_ids[]'
|
||||
input.value = file.id
|
||||
this.form.appendChild(input)
|
||||
})
|
||||
|
||||
this.form.querySelector('button[type="submit"]').click()
|
||||
this.loader.classList.remove('hidden')
|
||||
} else if (event.data.type === 'google-drive-picker-loaded') {
|
||||
this.loader.classList.add('hidden')
|
||||
this.form.classList.remove('hidden')
|
||||
} else if (event.data.type === 'google-drive-picker-request-oauth') {
|
||||
document.getElementById(this.dataset.oauthButtonId).classList.remove('hidden')
|
||||
this.classList.add('hidden')
|
||||
}
|
||||
}
|
||||
|
||||
disconnectedCallback () {
|
||||
this.observer?.unobserve(this)
|
||||
window.removeEventListener('message', this.messageHandler)
|
||||
}
|
||||
|
||||
get form () {
|
||||
return this.querySelector('form')
|
||||
}
|
||||
|
||||
get loader () {
|
||||
return document.getElementById('google_drive_loader')
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
export default class extends HTMLElement {
|
||||
connectedCallback () {
|
||||
this.addEventListener('click', () => {
|
||||
document.body.append(this.template.content)
|
||||
})
|
||||
}
|
||||
|
||||
get template () {
|
||||
return document.getElementById(this.dataset.templateId)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
export default class extends HTMLElement {
|
||||
connectedCallback () {
|
||||
const src = this.getAttribute('src')
|
||||
const link = document.createElement('a')
|
||||
|
||||
link.href = src
|
||||
link.setAttribute('data-turbo-frame', 'modal')
|
||||
link.style.display = 'none'
|
||||
|
||||
this.appendChild(link)
|
||||
|
||||
link.click()
|
||||
|
||||
window.history.replaceState({}, document.title, window.location.pathname)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
export default class extends HTMLElement {
|
||||
connectedCallback () {
|
||||
const image = this.querySelector('img')
|
||||
|
||||
image.addEventListener('load', (e) => {
|
||||
image.setAttribute('width', e.target.naturalWidth)
|
||||
image.setAttribute('height', e.target.naturalHeight)
|
||||
|
||||
this.style.aspectRatio = `${e.target.naturalWidth} / ${e.target.naturalHeight}`
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
export default class extends HTMLElement {
|
||||
connectedCallback () {
|
||||
const eventType = this.dataset.on || 'click'
|
||||
const selector = document.getElementById(this.dataset.selectorId) || this
|
||||
const eventElement = eventType === 'submit' ? this.querySelector('form') : this
|
||||
|
||||
eventElement.addEventListener(eventType, (event) => {
|
||||
if (eventType === 'click') {
|
||||
event.preventDefault()
|
||||
}
|
||||
|
||||
selector.remove()
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
export default class extends HTMLElement {
|
||||
connectedCallback () {
|
||||
this.querySelectorAll('input[type="checkbox"]').forEach(checkbox => {
|
||||
checkbox.addEventListener('change', this.handleChange)
|
||||
})
|
||||
}
|
||||
|
||||
handleChange = () => {
|
||||
if (this.checkedCount !== 0) {
|
||||
this.closest('form')?.requestSubmit()
|
||||
}
|
||||
}
|
||||
|
||||
get checkedCount () {
|
||||
return this.querySelectorAll('input[type="checkbox"]:checked').length
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
export default class extends HTMLElement {
|
||||
connectedCallback () {
|
||||
this.querySelectorAll('input[type="radio"]').forEach(radio => {
|
||||
radio.addEventListener('change', (event) => {
|
||||
const rating = parseInt(event.target.value)
|
||||
|
||||
if (rating === 10) {
|
||||
window.review_comment.value = ''
|
||||
window.review_comment.classList.add('hidden')
|
||||
window.review_submit.classList.add('hidden')
|
||||
event.target.form.submit()
|
||||
} else {
|
||||
window.review_comment.classList.remove('hidden')
|
||||
window.review_submit.classList.remove('hidden')
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
export default class extends HTMLElement {
|
||||
connectedCallback () {
|
||||
this.selector = document.getElementById(this.dataset.selectorId)
|
||||
|
||||
this.addEventListener('click', () => {
|
||||
this.selector.scrollIntoView({ behavior: 'smooth', block: 'start' })
|
||||
history.replaceState(null, null, `#${this.dataset.selectorId}`)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -13,6 +13,14 @@ export default class extends HTMLElement {
|
||||
this.input.classList.remove('w-60')
|
||||
}
|
||||
})
|
||||
|
||||
this.button.addEventListener('click', (event) => {
|
||||
if (!this.input.value && document.activeElement !== this.input) {
|
||||
event.preventDefault()
|
||||
|
||||
this.input.focus()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
get input () {
|
||||
@@ -22,4 +30,8 @@ export default class extends HTMLElement {
|
||||
get title () {
|
||||
return document.querySelector(this.dataset.title)
|
||||
}
|
||||
|
||||
get button () {
|
||||
return this.querySelector('button')
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
export default class extends HTMLElement {
|
||||
connectedCallback () {
|
||||
const input = this.dataset.inputId ? document.getElementById(this.dataset.inputId) : this.querySelector('input')
|
||||
|
||||
this.firstElementChild.addEventListener(this.dataset.on || 'click', () => {
|
||||
if (this.dataset.emptyOnly !== 'true' || !input.value) {
|
||||
input.value = this.dataset.value
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
export default class extends HTMLElement {
|
||||
connectedCallback () {
|
||||
this.addEventListener('change', (event) => {
|
||||
const targetValue = this.dataset.value
|
||||
const selectorId = this.dataset.selectorId
|
||||
const targetElement = document.getElementById(selectorId)
|
||||
|
||||
if (event.target.value === targetValue) {
|
||||
targetElement.classList.remove('hidden')
|
||||
} else {
|
||||
targetElement.classList.add('hidden')
|
||||
targetElement.value = ''
|
||||
event.target.form.requestSubmit()
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -5,17 +5,28 @@ export default targetable(class extends HTMLElement {
|
||||
static [target.static] = ['canvas', 'input', 'clear', 'button']
|
||||
|
||||
async connectedCallback () {
|
||||
const scale = 3
|
||||
|
||||
this.canvas.width = this.canvas.parentNode.clientWidth * scale
|
||||
this.canvas.height = this.canvas.parentNode.clientHeight * scale
|
||||
|
||||
this.canvas.getContext('2d').scale(scale, scale)
|
||||
|
||||
const { default: SignaturePad } = await import('signature_pad')
|
||||
|
||||
this.setCanvasSize()
|
||||
|
||||
this.pad = new SignaturePad(this.canvas)
|
||||
|
||||
this.resizeObserver = new ResizeObserver(() => {
|
||||
requestAnimationFrame(() => {
|
||||
if (!this.canvas) return
|
||||
|
||||
const { width, height } = this.canvas
|
||||
|
||||
this.setCanvasSize()
|
||||
|
||||
if (this.canvas.width !== width || this.canvas.height !== height) {
|
||||
this.redrawCanvas(width, height)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
this.resizeObserver.observe(this.canvas.parentNode)
|
||||
|
||||
this.clear.addEventListener('click', (e) => {
|
||||
e.preventDefault()
|
||||
|
||||
@@ -47,4 +58,38 @@ export default targetable(class extends HTMLElement {
|
||||
|
||||
this.closest('form').requestSubmit()
|
||||
}
|
||||
|
||||
disconnectedCallback () {
|
||||
if (this.resizeObserver) {
|
||||
this.resizeObserver.disconnect()
|
||||
}
|
||||
}
|
||||
|
||||
setCanvasSize () {
|
||||
const scale = 3
|
||||
|
||||
const width = this.canvas.parentNode.clientWidth
|
||||
const height = this.canvas.parentNode.clientWidth / 2.5
|
||||
|
||||
if (this.canvas.width !== width * scale || this.canvas.height !== height * scale) {
|
||||
this.canvas.width = width * scale
|
||||
this.canvas.height = height * scale
|
||||
|
||||
this.canvas.getContext('2d').scale(scale, scale)
|
||||
}
|
||||
}
|
||||
|
||||
redrawCanvas (oldWidth, oldHeight) {
|
||||
if (this.pad && !this.pad.isEmpty() && oldWidth > 0 && oldHeight > 0) {
|
||||
const sx = this.canvas.width / oldWidth
|
||||
const sy = this.canvas.height / oldHeight
|
||||
|
||||
const scaledData = this.pad.toData().map((stroke) => ({
|
||||
...stroke,
|
||||
points: stroke.points.map((p) => ({ ...p, x: p.x * sx, y: p.y * sy }))
|
||||
}))
|
||||
|
||||
this.pad.fromData(scaledData)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user