Compare commits

..

52 Commits

Author SHA1 Message Date
Alex Turchyn 077c55f7e8 fix rubocop 2023-08-17 21:30:50 +03:00
Alex Turchyn e5c5620b20 collect ua and ip on form completion 2023-08-17 21:30:49 +03:00
Alex Turchyn 187b354605 add personalization settings 2023-08-16 23:51:55 +03:00
Alex Turchyn 545b18086a rescue all email smtp errors 2023-08-15 21:51:46 +03:00
Alex Turchyn 130c91178f fix templates api page when new installation 2023-08-15 21:39:25 +03:00
Alex Turchyn 47c57158f4 refactor and fixes 2023-08-14 00:48:42 +03:00
Alex Turchyn 3b745a650d optimize dockerfile 2023-08-12 15:48:50 +03:00
Alex Turchyn 6871a3f840 send emails when configured smtp configured from env 2023-08-11 18:32:43 +03:00
Alex Turchyn c57148eada allow to set default field values via api 2023-08-11 18:32:33 +03:00
Alex Turchyn d875477ee6 improve SMTP configs form 2023-08-10 23:44:49 +03:00
Alex Turchyn 1daee8214c fix anonymous checkboxes form 2023-08-09 21:10:07 +03:00
Alex Turchyn 9cc85595e3 fix font size on large pdfs 2023-08-09 10:09:36 +03:00
Alex Turchyn f6879d7f94 fix GitHub CI 2023-08-08 23:38:53 +03:00
Alex Turchyn f6247e68b5 fix typo 2023-08-08 23:28:26 +03:00
Alex Turchyn 454acb63db update readme 2023-08-08 23:28:26 +03:00
Alex Turchyn 6d78d5430c add access tokens 2023-08-08 23:28:26 +03:00
Alex Turchyn e75e7b18b4 developer console fixes 2023-08-08 23:28:26 +03:00
Alex Turchyn 644b142798 add source to submissions 2023-08-08 23:28:25 +03:00
Alex Turchyn e4bb5466f5 add webhook settings 2023-08-08 23:28:25 +03:00
Alex Turchyn 8929adbe83 add settings api page 2023-08-08 23:28:25 +03:00
Alex Turchyn 896b149b25 add console redirect 2023-08-08 23:28:25 +03:00
Alex Turchyn 7697b94f73 show alert when upload non pdf or image 2023-08-08 23:28:25 +03:00
Alex Turchyn 7912c9a1ee add submitters api 2023-08-08 23:28:25 +03:00
Alex Turchyn 63e1246a11 adjust readme 2023-08-08 23:27:44 +03:00
Alex Turchyn cbca34a7d7 add reply to into submission invitation email 2023-08-02 23:22:31 +03:00
Alex Turchyn 9471a510c9 adjust email footer 2023-08-02 23:22:31 +03:00
Alex Turchyn e754c5d414 update README 2023-08-02 14:25:23 +03:00
Alex Turchyn 182f593d86 fix form previous day date bug 2023-08-01 00:26:37 +03:00
Alex Turchyn 7f4548e5c7 use company name for email friendly from name 2023-07-31 22:53:49 +03:00
Alex Turchyn 5e4f207844 improve checkboxes form 2023-07-31 00:48:51 +03:00
Alex Turchyn 1f29e624e7 remember signed in user session 2023-07-27 23:33:54 +03:00
Alex Turchyn 658e8a7bd6 fix missing field areas 2023-07-27 09:59:52 +03:00
Alex Turchyn 08c3b04511 fix builder when field areas key is undefined 2023-07-26 23:47:35 +03:00
Alex Turchyn 7779eb880d adjust preview buttons style 2023-07-26 23:33:05 +03:00
Alex Turchyn 21b9ca8ea5 detach areas on remove docuemnt 2023-07-26 23:17:24 +03:00
Alex Turchyn db7e3794d9 fix auto migration 2023-07-26 22:17:13 +03:00
Alex Turchyn df1d78897a add ability to replace documents 2023-07-26 22:01:16 +03:00
Alex Turchyn e1eb2da6f9 preserve template fields per submission 2023-07-26 09:58:56 +03:00
Alex Turchyn a92d1d82c1 #23 allow smtp configure without requiring smtp user 2023-07-24 00:53:12 +03:00
Alex Turchyn 2dcfc41365 update readme 2023-07-24 00:52:21 +03:00
Alex Turchyn 15cbdfb20c fix completed page 2023-07-22 20:44:20 +03:00
Alex Turchyn a2cf8fc5b5 fix checkboxes 2023-07-22 20:20:58 +03:00
Alex Turchyn f94ef3afdc adjust self-host landing 2023-07-22 16:06:43 +03:00
Alex Turchyn 9e3a9f0eeb fix submitter form when minimized 2023-07-22 14:31:13 +03:00
Alex Turchyn 1280572e6f update readme 2023-07-22 11:34:00 +03:00
Alex Turchyn cba2e84338 add signup link into demo 2023-07-22 01:30:51 +03:00
Alex Turchyn 17ca8f013a add 'Try in Cloud' to README 2023-07-19 23:41:55 +03:00
Alex Turchyn 96ef7ffaf7 add Render deployment button to README 2023-07-19 01:40:41 +03:00
Alex Turchyn 1bbcdb8073 fix modal scroll 2023-07-19 01:38:00 +03:00
Alex Turchyn da130c3f4f fix recipients added alert 2023-07-19 01:31:27 +03:00
Alex Turchyn c15f3ecd83 update Daisyui to v3.2.1 2023-07-18 01:24:05 +03:00
Alex Turchyn df0e0e6a1b remove internal scrolling in modals 2023-07-18 01:19:29 +03:00
112 changed files with 1538 additions and 321 deletions
+2
View File
@@ -1,5 +1,6 @@
/db/*.sqlite3
**/node_modules
/node_modules
/coverage
/doc
@@ -27,3 +28,4 @@
/attachments
/docuseal
.DS_Store
.env
+1
View File
@@ -127,6 +127,7 @@ jobs:
bundle config path vendor/bundle
bundle install --jobs 4 --retry 4
yarn install
sudo apt-get update
sudo apt-get install libvips
- name: Run
env:
+1
View File
@@ -35,3 +35,4 @@ yarn-debug.log*
/coverage
/attachments
/docuseal
/ee
+4 -1
View File
@@ -27,7 +27,7 @@ Lint/MissingSuper:
Enabled: false
Metrics/MethodLength:
Max: 20
Max: 25
Exclude:
- 'db/migrate/**'
@@ -61,5 +61,8 @@ RSpec/ExampleLength:
Rails/I18nLocaleTexts:
Enabled: false
Rails/SkipsModelValidations:
Enabled: false
Rails/ApplicationController:
Enabled: false
+9 -1
View File
@@ -37,7 +37,15 @@ COPY ./Gemfile ./Gemfile.lock ./
RUN bundle update --bundler && bundle install && rm -rf ~/.bundle
COPY . ./
COPY ./bin ./bin
COPY ./app ./app
COPY ./config ./config
COPY ./db ./db
COPY ./log ./log
COPY ./lib ./lib
COPY ./public ./public
COPY ./tmp ./tmp
COPY LICENSE README.md Rakefile config.ru ./
COPY --from=webpack /app/public/packs ./public/packs
+2
View File
@@ -13,6 +13,7 @@ gem 'faraday'
gem 'google-cloud-storage', require: false
gem 'hexapdf'
gem 'image_processing'
gem 'jwt'
gem 'lograge'
gem 'mysql2', require: false
gem 'oj'
@@ -23,6 +24,7 @@ gem 'pg', require: false
gem 'premailer-rails'
gem 'puma'
gem 'rails'
gem 'rails_autolink'
gem 'rails-i18n'
gem 'rollbar', require: ENV.key?('ROLLBAR_ACCESS_TOKEN')
gem 'ruby-vips'
+6
View File
@@ -379,6 +379,10 @@ GEM
rails-i18n (7.0.7)
i18n (>= 0.7, < 2)
railties (>= 6.0.0, < 8)
rails_autolink (1.1.8)
actionview (> 3.1)
activesupport (> 3.1)
railties (> 3.1)
railties (7.0.5)
actionpack (= 7.0.5)
activesupport (= 7.0.5)
@@ -540,6 +544,7 @@ DEPENDENCIES
google-cloud-storage
hexapdf
image_processing
jwt
letter_opener_web
lograge
mysql2
@@ -553,6 +558,7 @@ DEPENDENCIES
puma
rails
rails-i18n
rails_autolink
rollbar
rspec-rails
rubocop
+17 -4
View File
@@ -26,6 +26,8 @@ DocuSeal is an open source platform that provides secure and efficient digital d
</p>
<h2 align="center">
<a href="https://demo.docuseal.co">✨ Live Demo</a>
<span>|</span>
<a href="https://docuseal.co/sign_up">☁️ Try in Cloud</a>
</h2>
[![Demo](https://github.com/docusealco/docuseal/assets/5418788/d8703ea3-361a-423f-8bfe-eff1bd9dbe14)](https://demo.docuseal.co)
@@ -40,13 +42,16 @@ DocuSeal is an open source platform that provides secure and efficient digital d
- [x] PDF signature verification
- [x] Users management
- [x] Mobile-optimized
- [x] API and Webhooks for integrations
- [x] Easy to deploy in minutes
## Deploy
| Heroku | Railway | DigitalOcean |
| ---------------------------------------------------------------------------- | --------------------------------------------------------------------------- | --------------------------------------------------------------------------- |
| [<img alt="Deploy on Heroku" src="https://www.herokucdn.com/deploy/button.svg" height="40">](https://heroku.com/deploy?template=https://github.com/docusealco/docuseal-heroku) | [<img alt="Deploy on Railway" src="https://railway.app/button.svg" height="40">](https://railway.app/template/IGoDnc?referralCode=ruU7JR) | [<img alt="Deploy on DigitalOcean" src="https://www.deploytodo.com/do-btn-blue.svg" height="40">](https://cloud.digitalocean.com/apps/new?repo=https://github.com/docusealco/docuseal-digitalocean/tree/master&refcode=421d50f53990) |
|Heroku|Railway|
|:--:|:---:|
| [<img alt="Deploy on Heroku" src="https://www.herokucdn.com/deploy/button.svg" height="40">](https://heroku.com/deploy?template=https://github.com/docusealco/docuseal-heroku) | [<img alt="Deploy on Railway" src="https://railway.app/button.svg" height="40">](https://railway.app/template/IGoDnc?referralCode=ruU7JR)|
|**DigitalOcean**|**Render**|
| [<img alt="Deploy on DigitalOcean" src="https://www.deploytodo.com/do-btn-blue.svg" height="40">](https://cloud.digitalocean.com/apps/new?repo=https://github.com/docusealco/docuseal-digitalocean/tree/master&refcode=421d50f53990) | [<img alt="Deploy to Render" src="https://render.com/images/deploy-to-render-button.svg" height="40">](https://render.com/deploy?repo=https://github.com/docusealco/docuseal-render)
#### Docker
@@ -69,6 +74,14 @@ Run the app under a custom domain over https using docker compose (make sure you
HOST=your-domain-name.com docker-compose up
```
## For Companies
### Integrate seamless document signing into your web or mobile apps with DocuSeal!
At DocuSeal we have expertise and technologies to make documents creation, filling, signing and processing seamlessly integrated with your product. We specialize in working with various industries, including **Banking, Healthcare, Transport, Real Estate, eCommerce, KYC, CRM, and other software products** that require bulk document signing. By leveraging DocuSeal, we can assist in reducing the overall cost of developing and processing electronic documents while ensuring security and compliance with local electronic document laws.
[![Book an Integration Demo](https://cal.com/book-with-cal-dark.svg)](https://cal.com/docuseal)
## License
DocuSeal is released under the GNU Affero General Public License v3.0.
Distributed under the AGPLv3 License. See [LICENSE](https://github.com/docusealco/docuseal/blob/master/LICENSE) for more information.
Unless otherwise noted, all files © 2023 Oleksandr Turchyn.
+1 -14
View File
@@ -7,20 +7,7 @@ module Api
def create
submitter = Submitter.find_by!(slug: params[:submitter_slug])
blob =
if (file = params[:file])
ActiveStorage::Blob.create_and_upload!(io: file.open,
filename: file.original_filename,
content_type: file.content_type)
else
ActiveStorage::Blob.find_signed(params[:blob_signed_id])
end
attachment = ActiveStorage::Attachment.create!(
blob:,
name: params[:name],
record: submitter
)
attachment = Submitters.create_attachment!(submitter, params)
render json: attachment.as_json(only: %i[uuid], methods: %i[url filename content_type])
end
@@ -0,0 +1,80 @@
# frozen_string_literal: true
module Api
class SubmissionsController < ApiBaseController
UnknownFieldName = Class.new(StandardError)
UnknownSubmitterName = Class.new(StandardError)
def create
template = current_account.templates.find(params[:template_id])
submissions =
if params[:emails].present?
Submissions.create_from_emails(template:,
user: current_user,
source: :api,
send_email: params[:send_email] != 'false',
emails: params[:emails] || params[:email])
else
submissions_attrs = normalize_submissions_params!(submissions_params[:submission], template)
Submissions.create_from_submitters(template:,
user: current_user,
source: :api,
send_email: params[:send_email] != 'false',
submissions_attrs:)
end
submitters = submissions.flat_map(&:submitters)
send_invitation_emails(submitters) if params[:send_email] != 'false'
render json: submitters
rescue UnknownFieldName, UnknownSubmitterName => e
render json: { error: e.message }, status: :unprocessable_entity
end
private
def send_invitation_emails(submitters)
submitters.each do |submitter|
SubmitterMailer.invitation_email(submitter, message: params[:message]).deliver_later!
end
end
def submissions_params
params.permit(submission: [{ submitters: [[:uuid, :name, :email, { values: {} }]] }])
end
def normalize_submissions_params!(submissions_params, template)
submissions_params.each do |submission|
submission[:submitters].each_with_index do |submitter, index|
next if submitter[:values].blank?
submitter[:values] =
normalize_submitter_values(template,
submitter[:values], submitter[:name] || template.submitters[index]['name'])
end
end
submissions_params
end
def normalize_submitter_values(template, values, submitter_name)
submitter =
template.submitters.find { |e| e['name'] == submitter_name } ||
raise(UnknownSubmitterName, "Unknown submitter: #{submitter_name}")
fields = template.fields.select { |e| e['submitter_uuid'] == submitter['uuid'] }
fields_uuid_index = fields.index_by { |e| e['uuid'] }
fields_name_index = fields.index_by { |e| e['name'] }
values.transform_keys do |key|
next key if fields_uuid_index[key].present?
fields_name_index[key]&.dig('uuid') || raise(UnknownFieldName, "Unknown field: #{key}")
end
end
end
end
+15 -2
View File
@@ -2,9 +2,18 @@
module Api
class TemplatesController < ApiBaseController
def update
@template = current_account.templates.find(params[:id])
before_action :load_template, only: %i[show update]
def index
render json: current_account.templates
end
def show
render json: @template.as_json(include: { author: { only: %i[id email first_name last_name] },
documents: { only: %i[id uuid], methods: %i[url filename] } })
end
def update
@template.update!(template_params)
render :ok
@@ -19,5 +28,9 @@ module Api
fields: [[:uuid, :submitter_uuid, :name, :type, :required,
{ options: [], areas: [%i[x y w h cell_w attachment_uuid page]] }]])
end
def load_template
@template = current_account.templates.find(params[:id])
end
end
end
@@ -0,0 +1,5 @@
# frozen_string_literal: true
class ApiSettingsController < ApplicationController
def index; end
end
+2 -2
View File
@@ -34,8 +34,8 @@ class ApplicationController < ActionController::Base
redirect_to setup_index_path unless User.exists?
end
def button_title(title: 'Submit', disabled_with: 'Submitting', icon: nil)
render_to_string(partial: 'shared/button_title', locals: { title:, disabled_with:, icon: })
def button_title(title: 'Submit', disabled_with: 'Submitting', icon: nil, icon_disabled: nil)
render_to_string(partial: 'shared/button_title', locals: { title:, disabled_with:, icon:, icon_disabled: })
end
def svg_icon(icon_name, class: '')
@@ -0,0 +1,11 @@
# frozen_string_literal: true
class ConsoleRedirectController < ApplicationController
def index
auth = JsonWebToken.encode(uuid: current_user.uuid,
scope: :console,
exp: 1.minute.from_now.to_i)
redirect_to("#{Docuseal::CONSOLE_URL}?#{{ auth: }.to_query}", allow_other_host: true)
end
end
@@ -7,10 +7,16 @@ class EmailSettingsController < ApplicationController
def create
if @encrypted_config.update(storage_configs)
SettingsMailer.smtp_successful_setup(@encrypted_config.value['from_email']).deliver_now!
redirect_to settings_email_index_path, notice: 'Changes have been saved'
else
render :index, status: :unprocessable_entity
end
rescue StandardError => e
flash[:alert] = e.message
render :index, status: :unprocessable_entity
end
private
@@ -0,0 +1,20 @@
# frozen_string_literal: true
class PersonalizationSettingsController < ApplicationController
def show; end
def create
account_config =
current_account.account_configs.find_or_initialize_by(key: encrypted_config_params[:key])
account_config.update!(encrypted_config_params)
redirect_back(fallback_location: settings_personalization_path, notice: 'Settings have been saved.')
end
private
def encrypted_config_params
params.require(:account_config).permit!
end
end
+4 -2
View File
@@ -25,7 +25,7 @@ class StartFormController < ApplicationController
ua: request.user_agent
)
@submitter.submission ||= Submission.new(template: @template)
@submitter.submission ||= Submission.new(template: @template, source: :link)
if @submitter.save
redirect_to submit_form_path(@submitter.slug)
@@ -36,7 +36,9 @@ class StartFormController < ApplicationController
end
def completed
@submitter = Submitter.where(submission: @template.submissions).find_by!(email: params[:email])
@submitter = Submitter.where(submission: @template.submissions)
.where.not(completed_at: nil)
.find_by!(email: params[:email])
end
private
+14 -28
View File
@@ -17,19 +17,29 @@ class SubmissionsController < ApplicationController
def create
submissions =
if params[:emails].present?
create_submissions_from_emails
Submissions.create_from_emails(template: @template,
user: current_user,
source: :invite,
send_email: params[:send_email] == '1',
emails: params[:emails])
else
create_submissions_from_submitters
Submissions.create_from_submitters(template: @template,
user: current_user,
source: :invite,
send_email: params[:send_email] == '1',
submissions_attrs: submissions_params[:submission].to_h.values)
end
submitters = submissions.flat_map(&:submitters)
if params[:send_email] == '1'
submissions.flat_map(&:submitters).each do |submitter|
submitters.each do |submitter|
SubmitterMailer.invitation_email(submitter, message: params[:message]).deliver_later!
end
end
redirect_to template_path(@template),
notice: "#{submissions.size} #{'recipient'.pluralize(submissions.size)} added"
notice: "#{submitters.size} #{'recipient'.pluralize(submitters.size)} added"
end
def destroy
@@ -43,30 +53,6 @@ class SubmissionsController < ApplicationController
private
def create_submissions_from_emails
emails = params[:emails].to_s.scan(User::EMAIL_REGEXP)
emails.map do |email|
submission = @template.submissions.new(created_by_user: current_user)
submission.submitters.new(email:, uuid: @template.submitters.first['uuid'],
sent_at: params[:send_email] == '1' ? Time.current : nil)
submission.tap(&:save!)
end
end
def create_submissions_from_submitters
submissions_params[:submission].to_h.map do |_, attrs|
submission = @template.submissions.new(created_by_user: current_user)
attrs[:submitters].each do |submitter_attrs|
submission.submitters.new(**submitter_attrs, sent_at: params[:send_email] == '1' ? Time.current : nil)
end
submission.tap(&:save!)
end
end
def submissions_params
params.permit(submission: { submitters: [%i[uuid email]] })
end
+1 -24
View File
@@ -17,19 +17,8 @@ class SubmitFormController < ApplicationController
def update
submitter = Submitter.find_by!(slug: params[:slug])
submitter.values.merge!(normalized_values)
submitter.completed_at = Time.current if params[:completed] == 'true'
submitter.opened_at ||= Time.current
submitter.save!
if submitter.completed_at?
GenerateSubmitterResultAttachmentsJob.perform_later(submitter)
submitter.submission.template.account.users.active.each do |user|
SubmitterMailer.completed_email(submitter, user).deliver_later!
end
end
Submitters::SubmitValues.call(submitter, params, request)
head :ok
end
@@ -37,16 +26,4 @@ class SubmitFormController < ApplicationController
def completed
@submitter = Submitter.find_by!(slug: params[:submit_form_slug])
end
private
def normalized_values
params.fetch(:values, {}).to_unsafe_h.transform_values do |v|
if params[:cast_boolean] == 'true'
v == 'true'
else
v.is_a?(Array) ? v.compact_blank : v
end
end
end
end
@@ -0,0 +1,31 @@
# frozen_string_literal: true
class WebhookSettingsController < ApplicationController
def show
@encrypted_config =
current_account.encrypted_configs.find_or_initialize_by(key: EncryptedConfig::WEBHOOK_URL_KEY)
end
def create
@encrypted_config =
current_account.encrypted_configs.find_or_initialize_by(key: EncryptedConfig::WEBHOOK_URL_KEY)
@encrypted_config.update!(encrypted_config_params)
redirect_back(fallback_location: settings_webhooks_path, notice: 'Webhook URL has been saved.')
end
def update
submitter = current_account.submitters.where.not(completed_at: nil).order(:id).last
SendWebhookRequestJob.perform_later(submitter)
redirect_back(fallback_location: settings_webhooks_path, notice: 'Webhook request has been sent.')
end
private
def encrypted_config_params
params.require(:encrypted_config).permit(:value)
end
end
+4
View File
@@ -70,3 +70,7 @@ button[disabled] .enabled {
.base-radio {
@apply radio bg-white radio-sm no-animation;
}
.base-select {
@apply select base-input w-full font-normal;
}
+1 -1
View File
@@ -1,7 +1,7 @@
export default class extends HTMLElement {
connectedCallback () {
this.querySelectorAll('a').forEach((link) => {
if (document.location.pathname.startsWith(link.pathname)) {
if (document.location.pathname.startsWith(link.pathname) && !link.getAttribute('href').startsWith('http')) {
link.classList.add('bg-base-300')
}
})
+1 -1
View File
@@ -13,7 +13,7 @@ export default actionable(class extends HTMLElement {
document.body.classList.remove('overflow-hidden')
document.removeEventListener('keyup', this.onEscKey)
document.removeEventListener('turbo:submit-end', this.handleSubmit)
document.removeEventListener('turbo:submit-end', this.onSubmit)
document.removeEventListener('turbo:before-cache', this.close)
}
+1
View File
@@ -15,6 +15,7 @@ window.customElements.define('submission-form', class extends HTMLElement {
canSendEmail: this.dataset.canSendEmail === 'true',
isDirectUpload: this.dataset.isDirectUpload === 'true',
isDemo: this.dataset.isDemo === 'true',
withConfetti: true,
values: reactive(JSON.parse(this.dataset.values)),
attachments: reactive(JSON.parse(this.dataset.attachments)),
fields: JSON.parse(this.dataset.fields)
+9 -4
View File
@@ -21,14 +21,14 @@
</span>
</div>
<div
v-if="isActive"
v-if="isActive && withLabel"
class="absolute -top-7 rounded bg-base-content text-base-100 px-2 text-sm whitespace-nowrap"
>
{{ field.name || fieldNames[field.type] }}
<template v-if="field.type === 'checkbox'">
<template v-if="field.type === 'checkbox' && !field.name">
{{ fieldIndex + 1 }}
</template>
<template v-else-if="!field.required">
<template v-else-if="!field.required && field.type !== 'checkbox'">
(optional)
</template>
</div>
@@ -146,6 +146,11 @@ export default {
required: false,
default: false
},
withLabel: {
type: Boolean,
required: false,
default: true
},
fieldIndex: {
type: Number,
required: false,
@@ -207,7 +212,7 @@ export default {
},
formattedDate () {
if (this.field.type === 'date' && this.modelValue) {
return new Intl.DateTimeFormat([], { year: 'numeric', month: 'long', day: 'numeric' }).format(new Date(this.modelValue))
return new Intl.DateTimeFormat([], { year: 'numeric', month: 'long', day: 'numeric', timeZone: 'UTC' }).format(new Date(this.modelValue))
} else {
return ''
}
+24 -4
View File
@@ -11,7 +11,9 @@
v-for="(area, areaIndex) in field.areas"
:key="areaIndex"
>
<Teleport :to="`#page-${area.attachment_uuid}-${area.page}`">
<Teleport
:to="findPageElementForArea(area)"
>
<FieldArea
:ref="setAreaRef"
v-model="values[field.uuid]"
@@ -20,6 +22,7 @@
:submittable="true"
:field-index="fieldIndex"
:is-active="currentStep === step"
:with-label="withLabel"
:is-value-set="step.some((f) => f.uuid in values)"
:attachments-index="attachmentsIndex"
@click="$emit('focus-step', step)"
@@ -54,6 +57,11 @@ export default {
required: false,
default: () => ({})
},
withLabel: {
type: Boolean,
required: false,
default: true
},
currentStep: {
type: Array,
required: false,
@@ -70,10 +78,16 @@ export default {
this.areaRefs = []
},
methods: {
findPageElementForArea (area) {
return (this.$root.$el?.parentNode?.getRootNode() || document).getElementById(`page-${area.attachment_uuid}-${area.page}`)
},
scrollIntoField (field) {
this.areaRefs.find((area) => {
if (area.field === field) {
if (document.body.style.overflow === 'hidden') {
const root = this.$root.$el.parentNode.getRootNode()
const container = root.body || root.querySelector('div')
if (container.style.overflow === 'hidden') {
this.scrollInContainer(area.$el)
} else {
area.$refs.scrollToElem.scrollIntoView({ behavior: 'smooth', block: 'start' })
@@ -86,13 +100,19 @@ export default {
})
},
scrollInContainer (target) {
const root = this.$root.$el.parentNode.getRootNode()
const scrollbox = root.getElementById('scrollbox')
const formContainer = root.getElementById('form_container')
const container = root.body || root.querySelector('div')
const padding = 64
const boxRect = window.scrollbox.children[0].getBoundingClientRect()
const boxRect = scrollbox.children[0].getBoundingClientRect()
const targetRect = target.getBoundingClientRect()
const targetTopRelativeToBox = targetRect.top - boxRect.top
window.scrollbox.scrollTop = targetTopRelativeToBox - document.body.offsetHeight + window.form_container.offsetHeight + target.offsetHeight + padding
scrollbox.scrollTop = targetTopRelativeToBox - container.offsetHeight + formContainer.offsetHeight + target.offsetHeight + padding
},
setAreaRef (el) {
if (el) {
+28 -9
View File
@@ -51,6 +51,16 @@
Star on Github
</span>
</a>
<a
v-if="isDemo"
href="https://docuseal.co/sign_up"
class="white-button flex items-center space-x-1 w-full"
>
<IconLogin />
<span>
Create a Free Account
</span>
</a>
</div>
<div class="text-center mt-4">
Signed with
@@ -64,7 +74,7 @@
</template>
<script>
import { IconCircleCheck, IconBrandGithub, IconMail, IconDownload, IconInnerShadowTop } from '@tabler/icons-vue'
import { IconCircleCheck, IconBrandGithub, IconMail, IconDownload, IconInnerShadowTop, IconLogin } from '@tabler/icons-vue'
export default {
name: 'FormCompleted',
@@ -73,8 +83,10 @@ export default {
IconInnerShadowTop,
IconBrandGithub,
IconMail,
IconLogin,
IconDownload
},
inject: ['baseUrl'],
props: {
submitterSlug: {
type: String,
@@ -85,6 +97,11 @@ export default {
required: false,
default: false
},
withConfetti: {
type: Boolean,
required: false,
default: false
},
canSendEmail: {
type: Boolean,
required: false,
@@ -98,19 +115,21 @@ export default {
}
},
async mounted () {
const { default: confetti } = await import('canvas-confetti')
if (this.withConfetti) {
const { default: confetti } = await import('canvas-confetti')
confetti({
particleCount: 50,
startVelocity: 30,
spread: 140
})
confetti({
particleCount: 50,
startVelocity: 30,
spread: 140
})
}
},
methods: {
sendCopyToEmail () {
this.isSendingCopy = true
fetch(`/send_submission_email.json?submitter_slug=${this.submitterSlug}`, {
fetch(this.baseUrl + `/send_submission_email.json?submitter_slug=${this.submitterSlug}`, {
method: 'POST'
}).then(() => {
alert('Email has been sent')
@@ -121,7 +140,7 @@ export default {
download () {
this.isDownloading = true
fetch(`/submitters/${this.submitterSlug}/download`).then((response) => response.json()).then((urls) => {
fetch(this.baseUrl + `/submitters/${this.submitterSlug}/download`).then((response) => response.json()).then((urls) => {
const fileRequests = urls.map((url) => {
return () => {
return fetch(url).then(async (resp) => {
+3 -2
View File
@@ -55,6 +55,7 @@ export default {
IconCloudUpload,
IconInnerShadowTop
},
inject: ['baseUrl'],
props: {
message: {
type: String,
@@ -141,7 +142,7 @@ export default {
return await Promise.all(
blobs.map((blob) => {
return fetch('/api/attachments', {
return fetch(this.baseUrl + '/api/attachments', {
method: 'POST',
body: JSON.stringify({
name: 'attachments',
@@ -166,7 +167,7 @@ export default {
formData.append('submitter_slug', this.submitterSlug)
formData.append('name', 'attachments')
return fetch('/api/attachments', {
return fetch(this.baseUrl + '/api/attachments', {
method: 'POST',
body: formData
}).then(resp => resp.json()).then((data) => {
+77 -35
View File
@@ -4,12 +4,13 @@
:steps="stepFields"
:values="values"
:attachments-index="attachmentsIndex"
:with-label="!isAnonymousChecboxes"
:current-step="currentStepFields"
@focus-step="[saveStep(), goToStep($event, false, true)]"
@focus-step="[saveStep(), goToStep($event, false, true), currentField.type !== 'checkbox' ? isFormVisible = true : '']"
/>
<button
v-if="!isFormVisible"
class="btn btn-neutral text-white absolute rounded-none border-x-0 md:border md:rounded-full bottom-0 w-full md:mb-4 text-base"
class="btn btn-neutral flex text-white absolute rounded-none border-x-0 md:border md:rounded-full bottom-0 w-full md:mb-4 text-base"
@click.prevent="isFormVisible = true"
>
Submit Form
@@ -20,7 +21,7 @@
/>
</button>
<div
v-else
v-show="isFormVisible"
id="form_container"
class="shadow-md bg-base-100 absolute bottom-0 md:bottom-4 w-full border-base-200 border p-4 rounded"
>
@@ -182,7 +183,7 @@
/>
<div
v-else-if="currentField.type === 'checkbox'"
class="flex w-full"
class="flex w-full max-h-44 overflow-y-auto"
>
<input
type="hidden"
@@ -192,31 +193,45 @@
<div
class="space-y-3.5 mx-auto"
>
<div
v-for="(field, index) in currentStepFields"
:key="field.uuid"
>
<label
:for="field.uuid"
class="flex items-center space-x-3"
<template v-if="isAnonymousChecboxes">
<span class="text-xl">
Complete hightlighted checkboxes and click <span class="font-semibold">{{ stepFields.length === currentStep + 1 ? 'submit' : 'next' }}</span>.
</span>
<input
v-for="field in currentStepFields"
:key="field.uuid"
type="hidden"
:name="`values[${field.uuid}]`"
:value="!!values[field.uuid]"
>
<input
type="hidden"
:name="`values[${field.uuid}]`"
:value="!!values[field.uuid]"
</template>
<template v-else>
<div
v-for="(field, index) in currentStepFields"
:key="field.uuid"
>
<label
:for="field.uuid"
class="flex items-center space-x-3"
>
<input
:id="field.uuid"
type="checkbox"
class="base-checkbox !h-7 !w-7"
:checked="!!values[field.uuid]"
@click="[$refs.areas.scrollIntoField(field), values[field.uuid] = !values[field.uuid]]"
>
<span class="text-xl">
{{ currentField.name || currentField.type + ' ' + (index + 1) }}
</span>
</label>
</div>
<input
type="hidden"
:name="`values[${field.uuid}]`"
:value="!!values[field.uuid]"
>
<input
:id="field.uuid"
type="checkbox"
class="base-checkbox !h-7 !w-7"
:checked="!!values[field.uuid]"
@click="[$refs.areas.scrollIntoField(field), values[field.uuid] = !values[field.uuid]]"
>
<span class="text-xl">
{{ field.name || field.type + ' ' + (index + 1) }}
</span>
</label>
</div>
</template>
</div>
</div>
<ImageStep
@@ -277,6 +292,7 @@
<FormCompleted
v-else
:is-demo="isDemo"
:with-confetti="withConfetti"
:can-send-email="canSendEmail"
:submitter-slug="submitterSlug"
/>
@@ -318,6 +334,11 @@ export default {
IconArrowsDiagonalMinimize2,
FormCompleted
},
provide () {
return {
baseUrl: this.baseUrl
}
},
props: {
submitterSlug: {
type: String,
@@ -337,6 +358,16 @@ export default {
required: false,
default: () => []
},
withConfetti: {
type: Boolean,
required: false,
default: false
},
baseUrl: {
type: String,
required: false,
default: ''
},
fields: {
type: Array,
required: false,
@@ -344,16 +375,17 @@ export default {
},
authenticityToken: {
type: String,
required: true
required: false,
default: ''
},
isDirectUpload: {
type: Boolean,
required: true,
required: false,
default: false
},
isDemo: {
type: Boolean,
required: true,
required: false,
default: false
},
values: {
@@ -374,6 +406,9 @@ export default {
currentStepFields () {
return this.stepFields[this.currentStep]
},
isAnonymousChecboxes () {
return this.currentField.type === 'checkbox' && this.currentStepFields.every((e) => !e.name) && this.currentStepFields.length > 4
},
isButtonDisabled () {
return this.isSubmitting ||
(this.currentField.required && ['image', 'file'].includes(this.currentField.type) && !this.values[this.currentField.uuid]?.length) ||
@@ -413,10 +448,17 @@ export default {
)
if (/iPhone|iPad|iPod/i.test(navigator.userAgent)) {
document.body.style.overflow = 'hidden'
this.$nextTick(() => {
const root = this.$root.$el.parentNode.getRootNode()
const scrollbox = root.getElementById('scrollbox')
const parent = root.body || root.querySelector('div')
window.scrollbox.classList.add('h-full', 'overflow-y-auto')
window.scrollbox.parentNode.classList.add('h-screen', 'overflow-y-auto')
parent.style.overflow = 'hidden'
scrollbox.classList.add('h-full', 'overflow-y-auto')
scrollbox.parentNode.classList.add('h-screen', 'overflow-y-auto')
scrollbox.parentNode.style.maxHeight = '-webkit-fill-available'
})
}
},
methods: {
@@ -430,7 +472,7 @@ export default {
this.$refs.form.querySelector('input[type="date"], input[type="text"], select')?.focus()
if (clickUpload && !this.values[this.currentField.uuid]) {
if (clickUpload && !this.values[this.currentField.uuid] && ['file', 'image'].includes(this.currentField.type)) {
this.$refs.form.querySelector('input[type="file"]')?.click()
}
})
@@ -439,7 +481,7 @@ export default {
if (this.isCompleted) {
return Promise.resolve({})
} else {
return fetch(this.submitPath, {
return fetch(this.baseUrl + this.submitPath, {
method: 'POST',
body: formData || new FormData(this.$refs.form)
})
@@ -99,6 +99,7 @@ export default {
IconTextSize,
IconArrowsDiagonalMinimize2
},
inject: ['baseUrl'],
props: {
field: {
type: Object,
@@ -304,7 +305,7 @@ export default {
file,
'/direct_uploads'
).create((_error, data) => {
fetch('/api/attachments', {
fetch(this.baseUrl + '/api/attachments', {
method: 'POST',
body: JSON.stringify({
submitter_slug: this.submitterSlug,
@@ -326,12 +327,12 @@ export default {
formData.append('submitter_slug', this.submitterSlug)
formData.append('name', 'attachments')
return fetch('/api/attachments', {
return fetch(this.baseUrl + '/api/attachments', {
method: 'POST',
body: formData
}).then((resp) => resp.json()).then((attachment) => {
this.$emit('update:model-value', attachment.uuid)
this.$emit('attached', attachment)
this.$emit('update:model-value', attachment.uuid)
return resolve(attachment)
})
+1 -1
View File
@@ -65,7 +65,7 @@
@blur="onNameBlur"
>{{ field.name || defaultName }}</span>
<div
v-if="isNameFocus"
v-if="isNameFocus && field.type !== 'checkbox'"
class="flex items-center ml-1.5"
>
<input
+28 -4
View File
@@ -64,8 +64,11 @@
:with-arrows="template.schema.length > 1"
:item="item"
:document="sortedDocuments[index]"
:template="template"
:is-direct-upload="isDirectUpload"
@scroll-to="scrollIntoDocument(item)"
@remove="onDocumentRemove"
@replace="onDocumentReplace"
@up="moveDocument(item, -1)"
@down="moveDocument(item, 1)"
@change="save"
@@ -133,7 +136,7 @@
<p>
<button
class="base-button"
@click="drawField = false"
@click="drawField = null"
>
Cancel
</button>
@@ -314,7 +317,7 @@ export default {
const field = {
name: '',
uuid: v4(),
required: true,
required: type !== 'checkbox',
type,
submitter_uuid: this.selectedSubmitter.uuid,
areas: [area]
@@ -333,7 +336,7 @@ export default {
type: this.dragFieldType,
uuid: v4(),
submitter_uuid: this.selectedSubmitter.uuid,
required: true
required: this.dragFieldType !== 'checkbox'
}
if (['select', 'multiple', 'radio'].includes(this.dragFieldType)) {
@@ -353,7 +356,7 @@ export default {
if (this.selectedField?.type === this.dragFieldType) {
baseArea = this.selectedAreaRef.value
} else if (previousField?.areas) {
} else if (previousField?.areas?.length) {
baseArea = previousField.areas[previousField.areas.length - 1]
} else {
if (['checkbox'].includes(this.dragFieldType)) {
@@ -417,6 +420,27 @@ export default {
this.template.schema.splice(this.template.schema.indexOf(item), 1)
}
this.template.fields.forEach((field) => {
[...(field.areas || [])].forEach((area) => {
if (area.attachment_uuid === item.attachment_uuid) {
field.areas.splice(field.areas.indexOf(area), 1)
}
})
})
this.save()
},
onDocumentReplace ({ replaceSchemaItem, schema, documents }) {
this.template.schema.splice(this.template.schema.indexOf(replaceSchemaItem), 1, schema[0])
this.template.documents.push(...documents)
this.template.fields.forEach((field) => {
(field.areas || []).forEach((area) => {
if (area.attachment_uuid === replaceSchemaItem.attachment_uuid) {
area.attachment_uuid = schema[0].attachment_uuid
}
})
})
this.save()
},
moveDocument (item, direction) {
+11 -2
View File
@@ -93,12 +93,21 @@ export default {
}
}
},
mounted () {
if (this.isDirectUpload) {
import('@rails/activestorage')
}
},
methods: {
upload: Upload.methods.upload,
onDropFiles (e) {
this.$refs.input.files = e.dataTransfer.files
if ([...e.dataTransfer.files].every((f) => f.type.match(/(?:image\/)|(?:application\/pdf)/))) {
this.$refs.input.files = e.dataTransfer.files
this.upload()
this.upload()
} else {
alert('Only PDF and images are supported.')
}
}
}
}
+15 -13
View File
@@ -31,19 +31,21 @@
v-if="isNameFocus"
class="flex items-center relative"
>
<input
:id="`required-checkbox-${field.uuid}`"
v-model="field.required"
type="checkbox"
class="checkbox checkbox-xs no-animation rounded"
@mousedown.prevent
>
<label
:for="`required-checkbox-${field.uuid}`"
class="label text-xs"
@click.prevent="field.required = !field.required"
@mousedown.prevent
>Required</label>
<template v-if="field.type !== 'checkbox'">
<input
:id="`required-checkbox-${field.uuid}`"
v-model="field.required"
type="checkbox"
class="checkbox checkbox-xs no-animation rounded"
@mousedown.prevent
>
<label
:for="`required-checkbox-${field.uuid}`"
class="label text-xs"
@click.prevent="field.required = !field.required"
@mousedown.prevent
>Required</label>
</template>
</div>
<div
v-else
+3 -2
View File
@@ -76,7 +76,7 @@
Draw a text field on the page with a mouse
</li>
<li>
Single click of the page to add a checkbox
Single click on the page to add a checkbox
</li>
<li>
Drag &amp; drop any other field type on the page
@@ -171,7 +171,8 @@ export default {
const field = {
name: '',
uuid: v4(),
required: true,
required: type !== 'checkbox',
areas: [],
submitter_uuid: this.selectedSubmitter.uuid,
type
}
+59 -29
View File
@@ -9,39 +9,51 @@
loading="lazy"
>
<div
class="group flex justify-end cursor-pointer top-0 bottom-0 left-0 right-0 absolute"
class="group flex justify-end cursor-pointer top-0 bottom-0 left-0 right-0 absolute p-1"
@click="$emit('scroll-to', item)"
>
<div
class="flex flex-col justify-between opacity-0 group-hover:opacity-100"
>
<div class="rounded-bl rounded-tr bg-white border">
<button
class="rounded-bl rounded-tr hover:text-base-100 hover:bg-base-content w-full transition-colors"
style="width: 24px"
@click.stop="$emit('remove', item)"
>
&times;
</button>
<div class="flex justify-between w-full">
<div style="width: 26px" />
<div class="">
<ReplaceButton
:is-direct-upload="isDirectUpload"
:template-id="template.id"
class="opacity-0 group-hover:opacity-100"
@click.stop
@success="$emit('replace', { replaceSchemaItem: item, ...$event })"
/>
</div>
<div
v-if="withArrows"
class="flex flex-col border rounded-br rounded-tl bg-white divide-y"
class="flex flex-col justify-between opacity-0 group-hover:opacity-100"
>
<button
class="rounded-tl hover:text-base-100 hover:bg-base-content w-full transition-colors"
style="width: 24px"
@click.stop="$emit('up', item)"
<div>
<button
class="btn border-base-200 bg-white text-base-content btn-xs rounded hover:text-base-100 hover:bg-base-content hover:border-base-content w-full transition-colors"
style="width: 24px; height: 24px"
@click.stop="$emit('remove', item)"
>
&times;
</button>
</div>
<div
v-if="withArrows"
class="flex flex-col space-y-1"
>
&uarr;
</button>
<button
class="rounded-br hover:text-base-100 hover:bg-base-content w-full transition-colors"
style="width: 24px"
@click.stop="$emit('down', item)"
>
&darr;
</button>
<button
class="btn border-base-200 bg-white text-base-content btn-xs rounded hover:text-base-100 hover:bg-base-content hover:border-base-content w-full transition-colors"
style="width: 24px; height: 24px"
@click.stop="$emit('up', item)"
>
&uarr;
</button>
<button
class="btn border-base-200 bg-white text-base-content btn-xs rounded hover:text-base-100 hover:bg-base-content hover:border-base-content w-full transition-colors"
style="width: 24px; height: 24px"
@click.stop="$emit('down', item)"
>
&darr;
</button>
</div>
</div>
</div>
</div>
@@ -60,34 +72,52 @@
<script>
import Contenteditable from './contenteditable'
import Upload from './upload'
import ReplaceButton from './replace'
export default {
name: 'DocumentPreview',
components: {
Contenteditable
Contenteditable,
ReplaceButton
},
props: {
item: {
type: Object,
required: true
},
template: {
type: Object,
required: true
},
document: {
type: Object,
required: true
},
isDirectUpload: {
type: Boolean,
required: true,
default: false
},
withArrows: {
type: Boolean,
required: false,
default: true
}
},
emits: ['scroll-to', 'change', 'remove', 'up', 'down'],
emits: ['scroll-to', 'change', 'remove', 'up', 'down', 'replace'],
computed: {
previewImage () {
return this.document.preview_images[0]
}
},
mounted () {
if (this.isDirectUpload) {
import('@rails/activestorage')
}
},
methods: {
upload: Upload.methods.upload,
onUpdateName (value) {
this.item.name = value
@@ -0,0 +1,70 @@
<template>
<label
:for="inputId"
class="btn btn-neutral btn-xs text-white transition-none"
:class="{ 'opacity-100': isLoading || isProcessing }"
>
{{ message }}
<form
ref="form"
class="hidden"
>
<input
:id="inputId"
ref="input"
name="files[]"
type="file"
accept="image/*, application/pdf"
@change="upload"
>
</form>
</label>
</template>
<script>
import Upload from './upload'
export default {
name: 'ReplaceDocument',
props: {
templateId: {
type: [Number, String],
required: true
},
isDirectUpload: {
type: Boolean,
required: true,
default: false
}
},
emits: ['success'],
data () {
return {
isLoading: false,
isProcessing: false
}
},
computed: {
inputId () {
return 'el' + Math.random().toString(32).split('.')[1]
},
message () {
if (this.isLoading) {
return 'Uploading...'
} else if (this.isProcessing) {
return 'Processing...'
} else {
return 'Replace'
}
}
},
mounted () {
if (this.isDirectUpload) {
import('@rails/activestorage')
}
},
methods: {
upload: Upload.methods.upload
}
}
</script>
+24
View File
@@ -0,0 +1,24 @@
# frozen_string_literal: true
class SendWebhookRequestJob < ApplicationJob
USER_AGENT = 'DocuSeal.co Webhook'
def perform(submitter)
config = submitter.submission.account.encrypted_configs.find_by(key: EncryptedConfig::WEBHOOK_URL_KEY)
return if config.blank? || config.value.blank?
Submissions::EnsureResultGenerated.call(submitter)
ActiveStorage::Current.url_options = Docuseal.default_url_options
Faraday.post(config.value,
{
event_type: 'form.submitted',
timestamp: Time.current.iso8601,
data: Submitters::SerializeForWebhook.call(submitter)
}.to_json,
'Content-Type' => 'application/json',
'User-Agent' => USER_AGENT)
end
end
+4
View File
@@ -6,6 +6,10 @@ class ApplicationMailer < ActionMailer::Base
register_interceptor ActionMailerConfigsInterceptor
before_action do
ActiveStorage::Current.url_options = Docuseal.default_url_options
end
def default_url_options
Docuseal.default_url_options
end
+7
View File
@@ -0,0 +1,7 @@
# frozen_string_literal: true
class SettingsMailer < ApplicationMailer
def smtp_successful_setup(email)
mail(to: email, subject: 'SMTP has been configured')
end
end
+16 -3
View File
@@ -3,15 +3,27 @@
class SubmitterMailer < ApplicationMailer
DEFAULT_MESSAGE = %(You have been invited to submit the "%<name>s" form:)
def invitation_email(submitter, message: format(DEFAULT_MESSAGE, name: submitter.submission.template.name))
def invitation_email(submitter, message: '')
@current_account = submitter.submission.template.account
@submitter = submitter
@message = message
@message = message.presence || format(DEFAULT_MESSAGE, name: submitter.submission.template.name)
@email_config = @current_account.account_configs.find_by(key: AccountConfig::SUBMITTER_INVITATION_EMAIL_KEY)
subject =
if @email_config
ReplaceEmailVariables.call(@email_config.value['subject'], submitter:)
else
'You have been invited to submit a form'
end
mail(to: @submitter.email,
subject: 'You have been invited to submit a form')
subject:,
reply_to: submitter.submission.created_by_user&.friendly_name)
end
def completed_email(submitter, user)
@current_account = submitter.submission.template.account
@submitter = submitter
@user = user
@@ -20,6 +32,7 @@ class SubmitterMailer < ApplicationMailer
end
def documents_copy_email(submitter)
@current_account = submitter.submission.template.account
@submitter = submitter
Submissions::EnsureResultGenerated.call(@submitter)
+1
View File
@@ -2,6 +2,7 @@
class UserMailer < ApplicationMailer
def invitation_email(user)
@current_account = user.account
@user = user
@token = @user.send(:set_reset_password_token)
+39
View File
@@ -0,0 +1,39 @@
# frozen_string_literal: true
# == Schema Information
#
# Table name: access_tokens
#
# id :bigint not null, primary key
# sha256 :text not null
# token :text not null
# created_at :datetime not null
# updated_at :datetime not null
# user_id :bigint not null
#
# Indexes
#
# index_access_tokens_on_sha256 (sha256) UNIQUE
# index_access_tokens_on_user_id (user_id)
#
# Foreign Keys
#
# fk_rails_... (user_id => users.id)
#
class AccessToken < ApplicationRecord
TOKEN_LENGTH = 22
belongs_to :user
before_validation :set_sha256, on: :create
attribute :token, :string, default: -> { SecureRandom.base58(TOKEN_LENGTH) }
encrypts :token
private
def set_sha256
self.sha256 = Digest::SHA256.hexdigest(token)
end
end
+3
View File
@@ -14,7 +14,10 @@
class Account < ApplicationRecord
has_many :users, dependent: :destroy
has_many :encrypted_configs, dependent: :destroy
has_many :account_configs, dependent: :destroy
has_many :templates, dependent: :destroy
has_many :submissions, through: :templates
has_many :submitters, through: :submissions
has_many :active_users, -> { active }, dependent: :destroy,
inverse_of: :account, class_name: 'User'
+41
View File
@@ -0,0 +1,41 @@
# frozen_string_literal: true
# == Schema Information
#
# Table name: account_configs
#
# id :bigint not null, primary key
# key :string not null
# value :text not null
# created_at :datetime not null
# updated_at :datetime not null
# account_id :bigint not null
#
# Indexes
#
# index_account_configs_on_account_id (account_id)
# index_account_configs_on_account_id_and_key (account_id,key) UNIQUE
#
# Foreign Keys
#
# fk_rails_... (account_id => accounts.id)
#
class AccountConfig < ApplicationRecord
SUBMITTER_INVITATION_EMAIL_KEY = 'submitter_invitation_email'
DEFAULT_VALUES = {
SUBMITTER_INVITATION_EMAIL_KEY => {
'subject' => 'You have been invited to submit a form',
'body' => "Hi there,\n\n" \
"You have been invited to submit the \"{{template.name}}\" form:\n\n" \
"{{submitter.link}}\n\n" \
"Please contact us by replying to this email if you didn't request this.\n\n" \
"Thanks,\n" \
'{{account.name}}'
}
}.freeze
belongs_to :account
serialize :value, JSON
end
+1
View File
@@ -25,6 +25,7 @@ class EncryptedConfig < ApplicationRecord
EMAIL_SMTP_KEY = 'action_mailer_smtp'
ESIGN_CERTS_KEY = 'esign_certs'
APP_URL_KEY = 'app_url'
WEBHOOK_URL_KEY = 'webhook_url'
belongs_to :account
+24 -6
View File
@@ -4,12 +4,16 @@
#
# Table name: submissions
#
# id :bigint not null, primary key
# deleted_at :datetime
# created_at :datetime not null
# updated_at :datetime not null
# created_by_user_id :bigint
# template_id :bigint not null
# id :bigint not null, primary key
# deleted_at :datetime
# source :text not null
# template_fields :text
# template_schema :text
# template_submitters :text
# created_at :datetime not null
# updated_at :datetime not null
# created_by_user_id :bigint
# template_id :bigint not null
#
# Indexes
#
@@ -23,9 +27,23 @@
#
class Submission < ApplicationRecord
belongs_to :template
has_one :account, through: :template
belongs_to :created_by_user, class_name: 'User', optional: true
has_many :submitters, dependent: :destroy
serialize :template_fields, JSON
serialize :template_schema, JSON
serialize :template_submitters, JSON
attribute :source, :string, default: 'link'
scope :active, -> { where(deleted_at: nil) }
enum :source, {
invite: 'invite',
api: 'api',
embed: 'embed',
link: 'link'
}, scope: false, prefix: true
end
+2
View File
@@ -30,6 +30,8 @@
#
class Submitter < ApplicationRecord
belongs_to :submission
has_one :template, through: :submission
has_one :account, through: :template
attribute :values, :string, default: -> { {} }
attribute :slug, :string, default: -> { SecureRandom.base58(14) }
+3
View File
@@ -44,6 +44,9 @@ class Template < ApplicationRecord
has_many_attached :documents
has_many :schema_documents, ->(e) { where(uuid: e.schema.pluck('attachment_uuid')) },
class_name: 'ActiveStorage::Attachment', dependent: :destroy, as: :record, inverse_of: :record
has_many :submissions, dependent: :destroy
scope :active, -> { where(deleted_at: nil) }
+13 -2
View File
@@ -22,6 +22,7 @@
# role :string not null
# sign_in_count :integer default(0), not null
# unlock_token :string
# uuid :text not null
# created_at :datetime not null
# updated_at :datetime not null
# account_id :bigint not null
@@ -32,6 +33,7 @@
# index_users_on_email (email) UNIQUE
# index_users_on_reset_password_token (reset_password_token) UNIQUE
# index_users_on_unlock_token (unlock_token) UNIQUE
# index_users_on_uuid (uuid) UNIQUE
#
# Foreign Keys
#
@@ -40,22 +42,31 @@
class User < ApplicationRecord
ROLES = %w[admin].freeze
EMAIL_REGEXP =
/[a-z0-9][.']?(?:(?:[a-z0-9_-]++[.'])*[a-z0-9_-]++)*@(?:[a-z0-9]++[.-])*[a-z0-9]++\.[a-z]{2,}/i
EMAIL_REGEXP = /[^@,\s]+@[^@,\s]+/
belongs_to :account
has_one :access_token, dependent: :destroy
devise :database_authenticatable, :recoverable, :rememberable, :validatable, :trackable
devise :registerable, :omniauthable, omniauth_providers: [:google_oauth2] if Docuseal.multitenant?
attribute :role, :string, default: 'admin'
attribute :uuid, :string, default: -> { SecureRandom.uuid }
scope :active, -> { where(deleted_at: nil) }
def access_token
super || build_access_token.tap(&:save!)
end
def active_for_authentication?
!deleted_at?
end
def remember_me
true
end
def initials
[first_name.first, last_name.first].join.upcase
end
+2 -2
View File
@@ -11,13 +11,13 @@
<div class="grid md:grid-cols-2 gap-4">
<div class="form-control">
<%= ff.label :timezone, class: 'label' %>
<%= ff.select :timezone, nil, {}, class: 'select base-input w-full font-normal' do %>
<%= ff.select :timezone, nil, {}, class: 'base-select' do %>
<%= time_zone_options_for_select(current_account.timezone) %>
<% end %>
</div>
<div class="form-control">
<%= ff.label :locale, 'Time format', class: 'label' %>
<%= ff.select :locale, options_for_select(controller.class::LOCALE_OPTIONS.invert, current_account.locale), {}, class: 'select base-input w-full font-normal' %>
<%= ff.select :locale, options_for_select(controller.class::LOCALE_OPTIONS.invert, current_account.locale), {}, class: 'base-select' %>
</div>
</div>
<% end %>
+104
View File
@@ -0,0 +1,104 @@
<div class="flex flex-wrap space-y-4 md:flex-nowrap md:space-y-0 md:space-x-10">
<%= render 'shared/settings_nav' %>
<div class="flex-grow">
<h1 class="text-4xl font-bold mb-4">API</h1>
<div class="card bg-base-200">
<div class="card-body p-6">
<label for="api_key" class="text-sm font-semibold">X-Auth-Token</label>
<div class="flex flex-row space-x-4">
<input id="api_key" type="text" value="<%= current_user.access_token.token %>" class="input font-mono input-bordered w-full" autocomplete="off" readonly>
<%= render 'shared/clipboard_copy', icon: 'copy', text: current_user.access_token.token, class: 'base-button', icon_class: 'w-6 h-6 text-white', copy_title: 'Copy', copied_title: 'Copied' %>
</div>
</div>
</div>
<div class="space-y-4 mt-4">
<div class="collapse collapse-plus bg-base-200 px-1">
<input type="checkbox">
<div class="collapse-title text-xl font-medium">
<div>
Request signature, single submitter
</div>
<div class="mt-1">
<div class="badge badge-warning badge-lg">POST</div>
<div class="badge badge-primary badge-lg"><%= api_submissions_path %></div>
</div>
</div>
<div class="collapse-content" style="display: inherit">
<div class="mockup-code overflow-hidden">
<% text = capture do %>curl --location '<%= api_submissions_url %>' \
--header 'X-Auth-Token: <%= current_user.access_token.token %>' \
--data-raw '{
"template_id": <%= current_account.templates.last&.id || 1 %>,
"emails": "<%= current_user.email.sub('@', '+test@') %>, <%= current_user.email.sub('@', '+test2@') %>"
}'<% end.to_str %>
<span class="top-0 right-0 absolute">
<%= render 'shared/clipboard_copy', icon: 'copy', text:, class: 'btn btn-ghost text-white', icon_class: 'w-6 h-6 text-white', copy_title: 'Copy', copied_title: 'Copied' %>
</span>
<pre data-prefix="$"><code class="overflow-hidden w-full"><%= text %></code></pre>
</div>
</div>
</div>
<div class="collapse collapse-plus bg-base-200 px-1">
<input type="checkbox">
<div class="collapse-title text-xl font-medium">
<div>
Request signature, multiple submitters with default values
</div>
<div class="mt-1">
<div class="badge badge-warning badge-lg">POST</div>
<div class="badge badge-primary badge-lg"><%= api_submissions_path %></div>
</div>
</div>
<div class="collapse-content" style="display: inherit">
<div class="mockup-code overflow-hidden">
<% text = capture do %>curl --location '<%= api_submissions_url %>' \
--header 'X-Auth-Token: <%= current_user.access_token.token %>' \
--data-raw '{
"template_id": <%= current_account.templates.last&.id || 1 %>,
"submission": [
{
"submitters": [
{
"name": "<%= current_account.templates.last ? current_account.templates.last.submitters.first['name'] : 'First Submitter' %>",
"email": "<%= current_user.email.sub('@', '+test@') %>",
"values": {
"Form Text Field Name": "Default Value"
}
},
{ "name": "Second Submitter", "email": "<%= current_user.email.sub('@', '+test2@') %>" }
]
}
]
}'<% end.to_str %>
<span class="top-0 right-0 absolute">
<%= render 'shared/clipboard_copy', icon: 'copy', text:, class: 'btn btn-ghost text-white', icon_class: 'w-6 h-6 text-white', copy_title: 'Copy', copied_title: 'Copied' %>
</span>
<pre data-prefix="$"><code class="overflow-hidden w-full"><%= text %></code></pre>
</div>
</div>
</div>
<div class="collapse collapse-plus bg-base-200 px-1">
<input type="checkbox">
<div class="collapse-title text-xl font-medium">
<div>
Template details
</div>
<div class="mt-1">
<div class="badge badge-info badge-lg">GET</div>
<div class="badge badge-primary badge-lg"><%= api_template_path(':id') %></div>
</div>
</div>
<div class="collapse-content" style="display: inherit">
<div class="mockup-code overflow-hidden">
<% text = capture do %>curl '<%= api_template_url(current_account.templates&.last || 1) %>' \
--header 'X-Auth-Token: <%= current_user.access_token.token %>'<% end.to_str %>
<span class="top-0 right-0 absolute">
<%= render 'shared/clipboard_copy', icon: 'copy', text:, class: 'btn btn-ghost text-white', icon_class: 'w-6 h-6 text-white', copy_title: 'Copy', copied_title: 'Copied' %>
</span>
<pre data-prefix="$"><code class="overflow-hidden w-full"><%= text %></code></pre>
</div>
</div>
</div>
</div>
</div>
</div>
-8
View File
@@ -10,14 +10,6 @@
<%= f.label :password, class: 'label' %>
<%= f.password_field :password, autocomplete: 'current-password', class: 'base-input' %>
</div>
<% if devise_mapping.rememberable? %>
<div class="form-control">
<%= f.label :remember_me, class: 'flex items-center cursor-pointer' do %>
<%= f.check_box :remember_me, checked: true, class: 'base-checkbox' %>
<span class="label">Remember me</span>
<% end %>
</div>
<% end %>
</div>
<div class="form-control">
<%= f.button button_title(title: 'Log In', disabled_with: 'Logging In'), class: 'base-button' %>
+21
View File
@@ -25,6 +25,27 @@
<%= ff.password_field :password, value: value['password'], required: true, class: 'base-input' %>
</div>
</div>
<div class="grid md:grid-cols-2 gap-4">
<div class="form-control">
<%= ff.label :domain, 'Domain (optional)', class: 'label' %>
<%= ff.text_field :domain, value: value['domain'], class: 'base-input' %>
</div>
<div class="form-control">
<%= ff.label :authentication, class: 'label' %>
<%= ff.select :authentication, options_for_select([%w[Plain plain], %w[Login login], %w[CRAM-MD5 cram_md5]], value.fetch('authentication', 'plain')), { prompt: true }, required: true, class: 'base-select' %>
</div>
</div>
<div class="form-control">
<%= ff.label :security_label, 'SMTP Security', class: 'label' %>
<div class="flex items-center space-x-6">
<% [%w[None none], %w[SSL ssl], %w[TLS tls]].each do |(label, val)| %>
<%= ff.label :security, value: val, for: "#{val}_radio", class: 'label' do %>
<%= ff.radio_button :security, val, checked: (value['security'].blank? && val == 'none') || value['security'] == val, id: "#{val}_radio", class: 'base-radio mr-2' %>
<%= label %>
<% end %>
<% end %>
</div>
</div>
<div class="form-control">
<%= ff.label :from_email, 'Send from Email', class: 'label' %>
<%= ff.email_field :from_email, value: value['from_email'], required: true, class: 'base-input' %>
+1 -6
View File
@@ -8,11 +8,6 @@
</head>
<body>
<%= yield %>
<p>
---
</p>
<p>
Sent using <a href="<%= Docuseal::PRODUCT_URL %>"><%= Docuseal::PRODUCT_NAME %></a> documents software.
</p>
<%= render partial: 'shared/mailer_attribution' %>
</body>
</html>
+3 -2
View File
@@ -8,7 +8,7 @@
DocuSeal
</h1>
</div>
<h2 class="mb-16 text-lg text-center text-gray-500">
<h2 class="mb-16 text-lg text-center text-gray-600">
A self-hosted and open-source web platform that provides secure and efficient digital document signing and processing.
</h2>
</div>
@@ -22,7 +22,7 @@
</div>
<h3 class="mb-4 text-2xl font-semibold">Easy to Start</h3>
<p class="text-base text-gray-500">
Run on your own host using <a href="https://hub.docker.com/r/docuseal/docuseal" class="link link-neutral font-bold" target="_blank">Docker</a> container, or deploy on your favorite managed PaaS with a single <a href="https:///www.docuseal.co/install" class="link link-neutral font-bold">click</a>.
Run on your own host using <a href="https://hub.docker.com/r/docuseal/docuseal" class="link link-neutral font-bold" target="_blank">Docker</a> container, or deploy on your favorite managed PaaS with a single <a href="https://www.docuseal.co/install" class="link link-neutral font-bold">click</a>.
</p>
</div>
</div>
@@ -70,3 +70,4 @@
</div>
</div>
</section>
<%= render 'shared/attribution' %>
@@ -0,0 +1 @@
<%= render 'logo_placeholder' %>
@@ -0,0 +1,11 @@
<div class="alert my-4">
<%= svg_icon('info_circle', class: 'w-6 h-6') %>
<div>
<p class="font-bold">Unlock with DocuSeal Enterprise</p>
<p>
Display your company name and logo when signing documents.
<br>
<a class="link font-medium" target="_blank" href="<%= "#{Docuseal::PRODUCT_URL}/pricing" %>">Learn More</a>
</p>
</div>
</div>
@@ -0,0 +1,27 @@
<div class="flex flex-wrap space-y-4 md:flex-nowrap md:space-y-0">
<%= render 'shared/settings_nav' %>
<div class="flex-grow max-w-xl mx-auto">
<p class="text-4xl font-bold mb-4">Signature Request Email</p>
<%= form_for AccountConfigs.find_or_initialize_for_key(current_account, AccountConfig::SUBMITTER_INVITATION_EMAIL_KEY), url: settings_personalization_path, method: :post, html: { autocomplete: 'off', class: 'space-y-4' } do |f| %>
<%= f.hidden_field :key %>
<%= f.fields_for :value, Struct.new(:subject, :body).new(*f.object.value.values_at('subject', 'body')) do |ff| %>
<div class="form-control">
<%= ff.label :subject, class: 'label' %>
<%= ff.text_field :subject, required: true, class: 'base-input' %>
</div>
<div class="form-control">
<%= ff.label :body, class: 'label' %>
<autoresize-textarea>
<%= ff.text_area :body, required: true, class: 'base-input w-full py-2' %>
</autoresize-textarea>
</div>
<% end %>
<div class="form-control pt-2">
<%= f.button button_title(title: 'Save', disabled_with: 'Saving'), class: 'base-button' %>
</div>
<% end %>
<p class="text-4xl font-bold mb-4 mt-8">Company Logo</p>
<%= render 'logo_form' %>
</div>
<div class="w-0 md:w-52"></div>
</div>
@@ -2,12 +2,7 @@
<div class="space-y-6 mx-auto">
<div class="space-y-6">
<div class="flex items-center justify-center">
<a href="/" class="flex items-center">
<div class="mr-3">
<%= render 'shared/logo', width: '50px', height: '50px' %>
</div>
<h1 class="text-5xl font-bold text-center">DocuSeal</h1>
</a>
<%= render 'start_form/docuseal_logo' %>
</div>
<div class="text-center text-4xl font-bold">
Email has been sent
@@ -0,0 +1 @@
<p>SMTP has been successfully configured!</p>
+1 -1
View File
@@ -8,7 +8,7 @@
</span>
<span class="disabled">
<span class="flex items-center justify-center space-x-2">
<%= svg_icon('loader', class: 'w-6 h-6 animate-spin') %>
<%= local_assigns[:icon_disabled] || svg_icon('loader', class: 'w-6 h-6 animate-spin') %>
<span><%= disabled_with %>...</span>
</span>
</span>
+2 -2
View File
@@ -2,13 +2,13 @@
<label class="<%= local_assigns[:class] %>">
<input type="radio" class="peer hidden">
<span class="peer-checked:hidden flex items-center space-x-2">
<%= svg_icon('link', class: local_assigns[:icon_class] || 'w-6 h-6 text-white') %>
<%= svg_icon(local_assigns[:icon] || 'link', class: local_assigns[:icon_class] || 'w-6 h-6 text-white') %>
<span class="hidden md:inline">
<%= local_assigns[:copy_title] || 'Copy' %>
</span>
</span>
<span class="hidden peer-checked:flex items-center space-x-2">
<%= svg_icon('clipboard_copy', class: local_assigns[:icon_class] || 'w-6 h-6 text-white') %>
<%= svg_icon(local_assigns[:copied_icon] || 'clipboard_copy', class: local_assigns[:icon_class] || 'w-6 h-6 text-white') %>
<span class="hidden md:inline">
<%= local_assigns[:copied_title] || 'Copied' %>
</span>
@@ -0,0 +1,6 @@
<p>
---
</p>
<p>
Sent using <a href="<%= Docuseal::PRODUCT_URL %>"><%= Docuseal::PRODUCT_NAME %></a> free document signing.
</p>
@@ -0,0 +1 @@
<%= render 'shared/email_attribution' %>
+19
View File
@@ -23,6 +23,25 @@
<li>
<%= link_to 'Team', settings_users_path, class: 'text-base hover:bg-base-300' %>
</li>
<% if Docuseal.demo? || !Docuseal.multitenant? %>
<li>
<%= link_to 'API', settings_api_index_path, class: 'text-base hover:bg-base-300' %>
</li>
<li>
<%= link_to 'Webhooks', settings_webhooks_path, class: 'text-base hover:bg-base-300' %>
</li>
<% end %>
<li>
<%= link_to 'Personalization', settings_personalization_path, class: 'text-base hover:bg-base-300' %>
</li>
<% unless Docuseal.demo? %>
<li>
<%= link_to Docuseal.multitenant? ? console_redirect_index_path : Docuseal::CONSOLE_URL, class: 'text-base hover:bg-base-300', data: { prefetch: false } do %>
Console
<span class="badge badge-warning">New</span>
<% end %>
</li>
<% end %>
</ul>
</menu-active>
<div class="mx-4 border-t border-base-300 hidden md:block">
+2 -2
View File
@@ -1,7 +1,7 @@
<turbo-frame id="modal">
<turbo-modal class="modal modal-open items-start !animate-none">
<turbo-modal class="modal modal-open items-start !animate-none overflow-y-auto">
<div class="absolute top-0 bottom-0 right-0 left-0" data-action="click:turbo-modal#close"></div>
<div class="modal-box pt-4 pb-6 px-6 mt-20">
<div class="modal-box pt-4 pb-6 px-6 mt-20 max-h-none">
<% if local_assigns[:title] %>
<div class="flex justify-between items-center border-b pb-2 mb-2 font-medium">
<span>
+1
View File
@@ -0,0 +1 @@
<%= render 'docuseal_logo' %>
@@ -0,0 +1,6 @@
<a href="/" class="flex justify-center items-center">
<span class="mr-3">
<%= render 'shared/logo', width: '50px', height: '50px' %>
</span>
<h1 class="text-5xl font-bold text-center">DocuSeal</h1>
</a>
+2 -7
View File
@@ -2,14 +2,9 @@
<div class="space-y-6 mx-auto">
<div class="space-y-6">
<div class="flex items-center justify-center">
<a href="/" class="flex items-center">
<div class="mr-3">
<%= render 'shared/logo', width: '50px', height: '50px' %>
</div>
<h1 class="text-5xl font-bold text-center">DocuSeal</h1>
</a>
<%= render 'banner' %>
</div>
<div class="flex items-center bg-base-300 rounded-xl p-4 mb-4">
<div class="flex items-center bg-base-200 rounded-xl p-4 mb-4">
<div class="flex items-center">
<div class="mr-3">
<%= svg_icon('writing_sign', class: 'w-10 h-10') %>
+3 -8
View File
@@ -2,22 +2,17 @@
<div class="space-y-6 mx-auto">
<div class="space-y-6">
<div class="text-center w-full space-y-6">
<a href="/" class="flex justify-center">
<span class="mr-3">
<%= render 'shared/logo', width: '50px', height: '50px' %>
</span>
<h1 class="text-5xl font-bold text-center">DocuSeal</h1>
</a>
<%= render 'banner' %>
<p class="text-xl font-semibold text-center">You have been invited to submit a form</p>
</div>
<div class="flex items-center bg-base-300 rounded-xl p-4 mb-4">
<div class="flex items-center bg-base-200 rounded-xl p-4 mb-4">
<div class="flex items-center">
<div class="mr-3">
<%= svg_icon('writing_sign', class: 'w-10 h-10') %>
</div>
<div>
<p class="text-lg font-bold mb-1"><%= @template.name %></p>
<p class="text-sm">Invited by <%= @template.account.name %></p>
<p class="text-sm">Invited by <span class="font-semibold"><%= @template.account.name %></span></p>
</div>
</div>
</div>
+16 -14
View File
@@ -40,7 +40,7 @@
<div class="form-control">
<% is_smtp_configured = Accounts.can_send_emails?(current_account) %>
<%= f.label :send_email, class: 'flex items-center cursor-pointer' do %>
<%= f.check_box :send_email, class: 'base-checkbox', disabled: !is_smtp_configured, onchange: "message_field.classList.toggle('hidden', !event.currentTarget.checked)" %>
<%= f.check_box :send_email, class: 'base-checkbox', disabled: !is_smtp_configured, onchange: "window.message_field && message_field.classList.toggle('hidden', !event.currentTarget.checked)" %>
<span class="label">Send Email</span>
<% end %>
<% unless is_smtp_configured %>
@@ -57,21 +57,23 @@
</div>
<% end %>
</div>
<div id="message_field" class="card card-compact bg-base-200 hidden">
<div class="card-body">
<div class="form-control space-y-2">
<span class="label-text">Hi there,</span>
<autoresize-textarea>
<%= f.text_area :message, value: format(SubmitterMailer::DEFAULT_MESSAGE, name: @template.name), required: true, class: 'base-textarea !rounded-lg w-full' %>
</autoresize-textarea>
<span class="label-text">
Thanks,
<br>
<%= current_account.name %>
</span>
<% unless AccountConfig.exists?(key: AccountConfig::SUBMITTER_INVITATION_EMAIL_KEY) %>
<div id="message_field" class="card card-compact bg-base-200 hidden">
<div class="card-body">
<div class="form-control space-y-2">
<span class="label-text">Hi there,</span>
<autoresize-textarea>
<%= f.text_area :message, value: format(SubmitterMailer::DEFAULT_MESSAGE, name: @template.name), required: true, class: 'base-textarea !rounded-lg w-full' %>
</autoresize-textarea>
<span class="label-text">
Thanks,
<br>
<%= current_account.name %>
</span>
</div>
</div>
</div>
</div>
<% end %>
<div class="form-control">
<%= f.button button_title(title: 'Add Recipients'), class: 'base-button' %>
</div>
+6 -6
View File
@@ -23,7 +23,7 @@
</div>
<div class="flex md:max-h-[calc(100vh-60px)]">
<div class="overflow-y-auto overflow-x-hidden hidden lg:block w-52 flex-none pr-3 mt-0.5 pt-0.5">
<% @submission.template.schema.each do |item| %>
<% (@submission.template_schema || @submission.template.schema).each do |item| %>
<% document = @submission.template.documents.find { |a| item['attachment_uuid'] == a.uuid } %>
<a href="#<%= "page-#{document.uuid}-0" %>" onclick="[event.preventDefault(), window[event.target.closest('a').href.split('#')[1]].scrollIntoView({ behavior: 'smooth', block: 'start' })]" class="block cursor-pointer">
<img src="<%= document.preview_images.first.url %>" width="<%= document.preview_images.first.metadata['width'] %>" height="<%= document.preview_images.first.metadata['height'] %>" class="rounded border" loading="lazy">
@@ -35,10 +35,10 @@
</div>
<div class="w-full overflow-y-auto overflow-x-hidden mt-0.5 pt-0.5">
<div class="pr-3.5 pl-0.5">
<% fields_index = Templates.build_field_areas_index(@submission.template) %>
<% fields_index = Templates.build_field_areas_index(@submission.template_fields || @submission.template.fields) %>
<% values = @submission.submitters.reduce({}) { |acc, sub| acc.merge(sub.values) } %>
<% attachments_index = ActiveStorage::Attachment.where(record: @submission.submitters, name: :attachments).preload(:blob).index_by(&:uuid) %>
<% @submission.template.schema.each do |item| %>
<% (@submission.template_schema || @submission.template.schema).each do |item| %>
<% document = @submission.template.documents.find { |e| e.uuid == item['attachment_uuid'] } %>
<% document.preview_images.sort_by { |a| a.filename.base.to_i }.each_with_index do |page, index| %>
<div id="<%= "page-#{document.uuid}-#{index}" %>" class="relative">
@@ -57,15 +57,15 @@
</div>
<div class="hidden md:block relative w-80 flex-none pt-0.5 pr-4 pl-0.5 overflow-auto space">
<% colors = %w[bg-red-500 bg-sky-500 bg-emerald-500 bg-yellow-300 bg-purple-600] %>
<% submitter_fields_index = @submission.template.fields.group_by { |f| f['submitter_uuid'] } %>
<% @submission.template.submitters.each_with_index do |item, index| %>
<% submitter_fields_index = (@submission.template_fields || @submission.template.fields).group_by { |f| f['submitter_uuid'] } %>
<% (@submission.template_submitters || @submission.template.submitters).each_with_index do |item, index| %>
<% submitter = @submission.submitters.find { |e| e.uuid == item['uuid'] } %>
<div class="sticky -top-1 bg-base-100 pt-1 -mt-1 z-10">
<div class="border border-base-300 rounded-md px-2 py-1 mb-1">
<div class="flex items-center space-x-1">
<span class="mx-1 w-3 h-3 rounded-full <%= colors[index] %>"></span>
<span class="text-lg">
<%= @submission.template.submitters.find { |e| e['uuid'] == submitter&.uuid }&.dig('name') || "#{(index + 1).ordinalize} Submitter" %>
<%= (@submission.template_submitters || @submission.template.submitters).find { |e| e['uuid'] == submitter&.uuid }&.dig('name') || "#{(index + 1).ordinalize} Submitter" %>
</span>
</div>
<div class="flex items-center space-x-1 mt-1">
+1
View File
@@ -0,0 +1 @@
<%= render 'docuseal_logo' %>
@@ -0,0 +1,4 @@
<a href="<%= root_path %>" class="mx-auto text-2xl md:text-3xl font-bold items-center flex space-x-3">
<%= render 'shared/logo', class: 'w-9 h-9 md:w-12 md:h-12' %>
<span><%= Docuseal::PRODUCT_NAME %></span>
</a>
+3 -8
View File
@@ -2,14 +2,9 @@
<div class="space-y-6 mx-auto">
<div class="space-y-6">
<div class="flex items-center justify-center">
<a href="/" class="flex items-center">
<div class="mr-3">
<%= render 'shared/logo', width: '50px', height: '50px' %>
</div>
<h1 class="text-5xl font-bold text-center">DocuSeal</h1>
</a>
<%= render 'start_form/banner' %>
</div>
<div class="flex items-center bg-base-300 rounded-xl p-4 mb-4">
<div class="flex items-center bg-base-200 rounded-xl p-4 mb-4">
<div class="flex items-center">
<div class="mr-3">
<%= svg_icon('writing_sign', class: 'w-10 h-10') %>
@@ -23,7 +18,7 @@
</div>
<div>
<% if Accounts.can_send_emails?(@submitter.submission.template.account) %>
<%= button_to button_title(title: 'Send copy to Email', disabled_with: 'Sending', icon: svg_icon('mail_forward', class: 'w-6 h-6')), send_submission_email_index_path, params: { submitter_slug: @submitter.slug }, form: { onsubmit: 'event.submitter.disabled = true' }, class: 'base-button w-full' %>
<%= button_to button_title(title: 'Send copy to Email', disabled_with: 'Sending', icon: svg_icon('mail_forward', class: 'w-6 h-6')), send_submission_email_index_path, params: { submitter_slug: @submitter.slug }, form: { onsubmit: 'event.submitter.disabled = true' }, class: 'white-button w-full' %>
<div class="divider">OR</div>
<% end %>
<download-button data-src="<%= submitter_download_index_path(@submitter.slug) %>" class="base-button w-full">
+4 -7
View File
@@ -1,16 +1,13 @@
<% fields_index = Templates.build_field_areas_index(@submitter.submission.template) %>
<% fields_index = Templates.build_field_areas_index(@submitter.submission.template_fields || @submitter.submission.template.fields) %>
<% values = @submitter.submission.submitters.where.not(id: @submitter.id).reduce({}) { |acc, sub| acc.merge(sub.values) } %>
<% attachments_index = ActiveStorage::Attachment.where(record: @submitter.submission.submitters, name: :attachments).preload(:blob).index_by(&:uuid) %>
<div style="max-height: -webkit-fill-available;">
<div id="scrollbox">
<div class="mx-auto block pb-72" style="max-width: 1000px">
<div class="mt-4 flex">
<a href="<%= root_path %>" class="mx-auto text-2xl md:text-3xl font-bold items-center flex space-x-3">
<%= render 'shared/logo', class: 'w-9 h-9 md:w-12 md:h-12' %>
<span>DocuSeal</span>
</a>
<%= render 'banner' %>
</div>
<% @submitter.submission.template.schema.each do |item| %>
<% (@submitter.submission.template_schema || @submitter.submission.template.schema).each do |item| %>
<% document = @submitter.submission.template.documents.find { |a| a.uuid == item['attachment_uuid'] } %>
<% document.preview_images.sort_by { |a| a.filename.base.to_i }.each_with_index do |page, index| %>
<div class="relative my-4 shadow-md">
@@ -32,7 +29,7 @@
<div class="fixed bottom-0 w-full h-0 z-20">
<div class="mx-auto" style="max-width: 1000px">
<div class="relative md:mx-32">
<submission-form data-is-demo="<%= Docuseal.demo? %>" data-is-direct-upload="<%= Docuseal.active_storage_public? %>" data-submitter-uuid="<%= @submitter.uuid %>" data-submitter-slug="<%= @submitter.slug %>" data-can-send-email="<%= Accounts.can_send_emails?(Struct.new(:id).new(@submitter.submission.template.account_id)) %>" data-attachments="<%= attachments_index.values.select { |e| e.record_id == @submitter.id }.to_json(only: %i[uuid], methods: %i[url filename content_type]) %>" data-fields="<%= @submitter.submission.template.fields.select { |f| f['submitter_uuid'] == @submitter.uuid }.to_json %>" data-values="<%= @submitter.values.to_json %>" data-authenticity-token="<%= form_authenticity_token %>"></submission-form>
<submission-form data-is-demo="<%= Docuseal.demo? %>" data-is-direct-upload="<%= Docuseal.active_storage_public? %>" data-submitter-uuid="<%= @submitter.uuid %>" data-submitter-slug="<%= @submitter.slug %>" data-can-send-email="<%= Accounts.can_send_emails?(Struct.new(:id).new(@submitter.submission.template.account_id)) %>" data-attachments="<%= attachments_index.values.select { |e| e.record_id == @submitter.id }.to_json(only: %i[uuid], methods: %i[url filename content_type]) %>" data-fields="<%= (@submitter.submission.template_fields || @submitter.submission.template.fields).select { |f| f['submitter_uuid'] == @submitter.uuid }.to_json %>" data-values="<%= @submitter.values.to_json %>" data-authenticity-token="<%= form_authenticity_token %>"></submission-form>
</div>
</div>
</div>
@@ -9,5 +9,5 @@
</ul>
<% end %>
<p>
Thanks,<br><%= @submitter.submission.template.account.name %>
Thanks,<br><%= @current_account.name %>
</p>
@@ -1,7 +1,11 @@
<p>Hi there,</p>
<%= simple_format(@message) %>
<p><%= link_to 'Submit Form', submit_form_url(slug: @submitter.slug) %></p>
<p>Please contact us by replying to this email if you didn't request this.</p>
<p>
Thanks,<br><%= @submitter.submission.template.account.name %>
</p>
<% if @email_config %>
<%= auto_link(simple_format(h(ReplaceEmailVariables.call(@email_config.value['body'], submitter: @submitter)))) %>
<% else %>
<p>Hi there,</p>
<%= simple_format(@message) %>
<p><%= link_to 'Submit Form', submit_form_url(slug: @submitter.slug) %></p>
<p>Please contact us by replying to this email if you didn't request this.</p>
<p>
Thanks,<br><%= @current_account.name %>
</p>
<% end %>
+1 -1
View File
@@ -1 +1 @@
<template-builder data-is-direct-upload="<%= Docuseal.active_storage_public? %>" data-template="<%= @template.to_json(include: { documents: { include: { preview_images: { methods: %i[url metadata filename] } } } }) %>"></template-builder>
<template-builder data-is-direct-upload="<%= Docuseal.active_storage_public? %>" data-template="<%= @template.as_json.merge(documents: @template.schema_documents.as_json(include: { preview_images: { methods: %i[url metadata filename] } })).to_json %>"></template-builder>
+1 -1
View File
@@ -37,7 +37,7 @@
<div class="space-y-4">
<% @submissions.each do |submission| %>
<a href="<%= submission_path(submission) %>" class="bg-base-200 w-full flex flex-col md:flex-row space-y-4 md:space-y-0 md:justify-between rounded-2xl px-6 py-5 md:items-center">
<% submitters = submission.template.submitters.filter_map { |item| submission.submitters.find { |e| e.uuid == item['uuid'] } } %>
<% submitters = (submission.template_submitters || submission.template.submitters).filter_map { |item| submission.submitters.find { |e| e.uuid == item['uuid'] } } %>
<% is_submission_complated = submitters.all?(&:completed_at?) %>
<% if submitters.size == 1 %>
<div>
@@ -3,5 +3,5 @@
<p><%= link_to 'Sign up', invitation_url(reset_password_token: @token) %></p>
<p>Please contact us by replying to this email if you didn't request this.</p>
<p>
Thanks,<br><%= @user.account.name %>
Thanks,<br><%= @current_account.name %>
</p>
+42
View File
@@ -0,0 +1,42 @@
<div class="flex flex-wrap space-y-4 md:flex-nowrap md:space-y-0 md:space-x-10">
<%= render 'shared/settings_nav' %>
<div class="flex-grow">
<h1 class="text-4xl font-bold mb-4">Webhooks</h1>
<div class="card bg-base-200">
<div class="card-body p-6">
<%= form_for @encrypted_config, url: settings_webhooks_path, method: :post, html: { autocomplete: 'off' } do |f| %>
<%= f.label :value, 'Webhook URL', class: 'text-sm font-semibold' %>
<div class="flex flex-row space-x-4 mt-2">
<%= f.text_field :value, required: true, class: 'input font-mono input-bordered w-full', placeholder: 'https://example.com/hook' %>
<%= f.button button_title(title: 'Save', disabled_with: 'Saving'), class: 'base-button w-32' %>
</div>
<% end %>
</div>
</div>
<% submitter = current_account.submitters.where.not(completed_at: nil).order(:id).last %>
<% if submitter %>
<div class="space-y-4 mt-4">
<div class="collapse collapse-open bg-base-200 px-1">
<div class="p-4 text-xl font-medium">
<div class="flex items-center justify-between">
<span>
Submission example payload
</span>
<% if @encrypted_config.value.present? %>
<%= button_to button_title(title: 'Test Webhook', disabled_with: 'Sending', icon_disabled: svg_icon('loader', class: 'w-4 h-4 animate-spin')), settings_webhooks_path, class: 'btn btn-neutral btn-outline btn-sm', method: :put %>
<% end %>
</div>
</div>
<div class="collapse-content" style="display: inherit">
<div class="mockup-code overflow-hidden relative">
<span class="top-0 right-0 absolute">
<%= render 'shared/clipboard_copy', icon: 'copy', text: code = JSON.pretty_generate({ event_type: 'form.submitted', timestamp: Time.current.iso8601, data: Submitters::SerializeForWebhook.call(submitter) }).gsub(/^/, ' ').sub(/^\s+/, ''), class: 'btn btn-ghost text-white', icon_class: 'w-6 h-6 text-white', copy_title: 'Copy', copied_title: 'Copied' %>
</span>
<pre><code class="overflow-hidden w-full"><%= code %></code></pre>
</div>
</div>
</div>
</div>
<% end %>
</div>
</div>
+5
View File
@@ -9,6 +9,8 @@ require 'action_view/railtie'
require 'action_mailer/railtie'
require 'active_job/railtie'
require_relative '../lib/api_path_consider_json_middleware'
Bundler.require(*Rails.groups)
module DocuSeal
@@ -26,5 +28,8 @@ module DocuSeal
config.action_view.frozen_string_literal = true
config.middleware.insert_before ActionDispatch::Static, Rack::Deflater
config.middleware.insert_before ActionDispatch::Static, ApiPathConsiderJsonMiddleware
ActiveSupport.run_load_hooks(:application_config, self)
end
end
+2 -2
View File
@@ -76,7 +76,7 @@ Rails.application.configure do
# Set this to true and configure the email server for immediate delivery to raise delivery errors.
config.action_mailer.raise_delivery_errors = false
if ENV['SMTP_USERNAME']
if ENV['SMTP_ADDRESS']
config.action_mailer.delivery_method = :smtp
config.action_mailer.smtp_settings = {
address: ENV.fetch('SMTP_ADDRESS', nil),
@@ -86,7 +86,7 @@ Rails.application.configure do
password: ENV.fetch('SMTP_PASSWORD', nil),
authentication: ENV.fetch('SMTP_AUTHENTICATION', 'plain'),
enable_starttls_auto: ENV['SMTP_ENABLE_STARTTLS_AUTO'] == 'true'
}
}.compact
end
# Enable locale fallbacks for I18n (makes lookups for any locale fall back to
+14
View File
@@ -13,6 +13,20 @@ Rails.configuration.to_prepare do
response.set_header('Cache-Control', 'public, max-age=31536000') if action_name == 'show'
end
ActiveStorage::Blobs::ProxyController.before_action do
response.set_header('Access-Control-Allow-Origin', '*')
response.set_header('Access-Control-Allow-Methods', 'GET')
response.set_header('Access-Control-Allow-Headers', '*')
response.set_header('Access-Control-Max-Age', '1728000')
end
ActiveStorage::Blobs::RedirectController.before_action do
response.set_header('Access-Control-Allow-Origin', '*')
response.set_header('Access-Control-Allow-Methods', 'GET')
response.set_header('Access-Control-Allow-Headers', '*')
response.set_header('Access-Control-Max-Age', '1728000')
end
ActiveStorage::DirectUploadsController.before_action do
next if current_user
next if Submitter.find_signed(cookies[:submitter_sid])
+9 -5
View File
@@ -1,5 +1,9 @@
# frozen_string_literal: true
require_relative '../../lib/auth_with_token_strategy'
Warden::Strategies.add(:auth_token, AuthWithTokenStrategy)
# Assuming you have not yet modified this file, each configuration option below
# is set to its default value. Note that some are commented out while others
# are not: uncommented lines are intended to protect your configuration from
@@ -159,7 +163,7 @@ Devise.setup do |config|
# ==> Configuration for :rememberable
# The time the user will be remembered without asking for credentials again.
# config.remember_for = 2.weeks
config.remember_for = 2.years
# Invalidates all the remember me tokens when the user signs out.
config.expire_all_remember_me_on_sign_out = true
@@ -272,10 +276,10 @@ Devise.setup do |config|
# If you want to use other strategies, that are not supported by Devise, or
# change the failure app, you can configure them inside the config.warden block.
#
# config.warden do |manager|
# manager.intercept_401 = false
# manager.default_strategies(scope: :user).unshift :some_external_strategy
# end
config.warden do |manager|
# manager.intercept_401 = false
manager.default_strategies(scope: :user).unshift(:auth_token)
end
# ==> Mountable engine configurations
# When using Devise inside an engine, let's call it `MyEngine`, and this engine
+3 -1
View File
@@ -1,3 +1,5 @@
# frozen_string_literal: true
ActiveRecord::Tasks::DatabaseTasks.migrate if ENV['RAILS_ENV'] == 'production'
Rails.configuration.to_prepare do
ActiveRecord::Tasks::DatabaseTasks.migrate if ENV['RAILS_ENV'] == 'production'
end
+12 -2
View File
@@ -27,9 +27,11 @@ Rails.application.routes.draw do
end
end
namespace :api do
namespace :api, defaults: { format: :json } do
resources :attachments, only: %i[create]
resources :templates, only: %i[update] do
resources :submissions, only: %i[create]
resources :templates, only: %i[update show index] do
resources :submissions, only: %i[create]
resources :documents, only: %i[create destroy], controller: 'templates_documents'
end
end
@@ -39,6 +41,7 @@ Rails.application.routes.draw do
resource :newsletter, only: %i[show update]
resources :users, only: %i[new create edit update destroy]
resources :submissions, only: %i[show destroy]
resources :console_redirect, only: %i[index]
resources :templates, only: %i[new create edit show destroy] do
resources :submissions, only: %i[new create]
end
@@ -67,6 +70,11 @@ Rails.application.routes.draw do
end
resources :esign, only: %i[index create], controller: 'esign_settings'
resources :users, only: %i[index]
resource :personalization, only: %i[show create], controller: 'personalization_settings'
if !Docuseal.multitenant? || Docuseal.demo?
resources :api, only: %i[index], controller: 'api_settings'
resource :webhooks, only: %i[show create update], controller: 'webhook_settings'
end
resource :account, only: %i[show update]
resources :profile, only: %i[index] do
collection do
@@ -76,4 +84,6 @@ Rails.application.routes.draw do
end
end
end
ActiveSupport.run_load_hooks(:routes, self)
end
@@ -0,0 +1,31 @@
# frozen_string_literal: true
class AddTemplateFieldsToSubmission < ActiveRecord::Migration[7.0]
class MigrationTemplate < ApplicationRecord
self.table_name = 'templates'
end
class MigrationSubmission < ApplicationRecord
self.table_name = 'submissions'
end
def up
add_column :submissions, :template_fields, :text
add_column :submissions, :template_schema, :text
add_column :submissions, :template_submitters, :text
MigrationTemplate.all.each do |template|
MigrationSubmission.where(template_id: template.id).each do |submission|
submission.update_columns(template_fields: template.fields,
template_schema: template.schema,
template_submitters: template.submitters)
end
end
end
def down
remove_column :submissions, :template_fields
remove_column :submissions, :template_schema
remove_column :submissions, :template_submitters
end
end
@@ -0,0 +1,22 @@
# frozen_string_literal: true
class AddUuidToUsers < ActiveRecord::Migration[7.0]
class MigrationUser < ApplicationRecord
self.table_name = 'users'
end
def up
add_column :users, :uuid, :text
add_index :users, :uuid, unique: true
MigrationUser.all.each do |user|
user.update_columns(uuid: SecureRandom.uuid)
end
change_column_null :users, :uuid, false
end
def down
drop_column :users, :uuid
end
end
@@ -0,0 +1,19 @@
# frozen_string_literal: true
class AddSourceToSubmissions < ActiveRecord::Migration[7.0]
class MigrationSubmission < ApplicationRecord
self.table_name = 'submissions'
end
def up
add_column :submissions, :source, :text
MigrationSubmission.where(source: nil).update_all(source: :invite)
change_column_null :submissions, :source, false
end
def down
drop_column :submissions, :source
end
end
@@ -0,0 +1,13 @@
# frozen_string_literal: true
class CreateAccessTokens < ActiveRecord::Migration[7.0]
def change
create_table :access_tokens do |t|
t.references :user, null: false, foreign_key: true, index: true
t.text :token, null: false
t.text :sha256, null: false, index: { unique: true }
t.timestamps
end
end
end
@@ -0,0 +1,15 @@
# frozen_string_literal: true
class CreateAccountConfigs < ActiveRecord::Migration[7.0]
def change
create_table :account_configs do |t|
t.references :account, null: false, foreign_key: true, index: true
t.string :key, null: false
t.text :value, null: false
t.index %i[account_id key], unique: true
t.timestamps
end
end
end
+29 -1
View File
@@ -10,10 +10,30 @@
#
# It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema[7.0].define(version: 2023_07_01_075115) do
ActiveRecord::Schema[7.0].define(version: 2023_08_15_190540) do
# These are extensions that must be enabled in order to support this database
enable_extension "plpgsql"
create_table "access_tokens", force: :cascade do |t|
t.bigint "user_id", null: false
t.text "token", null: false
t.text "sha256", null: false
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.index ["sha256"], name: "index_access_tokens_on_sha256", unique: true
t.index ["user_id"], name: "index_access_tokens_on_user_id"
end
create_table "account_configs", force: :cascade do |t|
t.bigint "account_id", null: false
t.string "key", null: false
t.text "value", null: false
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.index ["account_id", "key"], name: "index_account_configs_on_account_id_and_key", unique: true
t.index ["account_id"], name: "index_account_configs_on_account_id"
end
create_table "accounts", force: :cascade do |t|
t.string "name", null: false
t.string "timezone", null: false
@@ -77,6 +97,10 @@ ActiveRecord::Schema[7.0].define(version: 2023_07_01_075115) do
t.datetime "deleted_at"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.text "template_fields"
t.text "template_schema"
t.text "template_submitters"
t.text "source", null: false
t.index ["created_by_user_id"], name: "index_submissions_on_created_by_user_id"
t.index ["template_id"], name: "index_submissions_on_template_id"
end
@@ -136,12 +160,16 @@ ActiveRecord::Schema[7.0].define(version: 2023_07_01_075115) do
t.datetime "deleted_at"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.text "uuid", null: false
t.index ["account_id"], name: "index_users_on_account_id"
t.index ["email"], name: "index_users_on_email", unique: true
t.index ["reset_password_token"], name: "index_users_on_reset_password_token", unique: true
t.index ["unlock_token"], name: "index_users_on_unlock_token", unique: true
t.index ["uuid"], name: "index_users_on_uuid", unique: true
end
add_foreign_key "access_tokens", "users"
add_foreign_key "account_configs", "accounts"
add_foreign_key "active_storage_attachments", "active_storage_blobs", column: "blob_id"
add_foreign_key "active_storage_variant_records", "active_storage_blobs", column: "blob_id"
add_foreign_key "document_generation_events", "submitters"
+10
View File
@@ -0,0 +1,10 @@
# frozen_string_literal: true
module AccountConfigs
module_function
def find_or_initialize_for_key(account, key)
account.account_configs.find_by(key:) ||
account.account_configs.new(key:, value: AccountConfig::DEFAULT_VALUES[key])
end
end
+1
View File
@@ -62,6 +62,7 @@ module Accounts
def can_send_emails?(account)
return true if Docuseal.multitenant?
return true if ENV['SMTP_ADDRESS'].present?
EncryptedConfig.exists?(account_id: account.id, key: EncryptedConfig::EMAIL_SMTP_KEY)
end
+18 -6
View File
@@ -19,17 +19,29 @@ module ActionMailerConfigsInterceptor
email_configs = EncryptedConfig.find_by(key: EncryptedConfig::EMAIL_SMTP_KEY)
if email_configs
message.delivery_method(:smtp, user_name: email_configs.value['username'],
password: email_configs.value['password'],
address: email_configs.value['host'],
port: email_configs.value['port'],
tls: email_configs.value['port'].to_s == '465')
message.delivery_method(:smtp, build_smtp_configs_hash(email_configs))
message.from = email_configs.value['from_email']
message.from = "#{email_configs.account.name} <#{email_configs.value['from_email']}>"
else
message.delivery_method(:test)
end
message
end
def build_smtp_configs_hash(email_configs)
value = email_configs.value
{
user_name: value['username'],
password: value['password'],
address: value['host'],
port: value['port'],
domain: value['domain'],
authentication: value.fetch('authentication', 'plain'),
enable_starttls_auto: true,
ssl: value['security'] == 'ssl',
tls: value['security'] == 'tls' || (value['security'].blank? && value['port'].to_s == '465')
}.compact_blank
end
end
+17
View File
@@ -0,0 +1,17 @@
# frozen_string_literal: true
class ApiPathConsiderJsonMiddleware
def initialize(app)
@app = app
end
def call(env)
if env['PATH_INFO'].starts_with?('/api') &&
!env['PATH_INFO'].ends_with?('/documents') &&
!env['PATH_INFO'].ends_with?('/attachments')
env['CONTENT_TYPE'] = 'application/json'
end
@app.call(env)
end
end
+21
View File
@@ -0,0 +1,21 @@
# frozen_string_literal: true
class AuthWithTokenStrategy < Devise::Strategies::Base
def valid?
request.headers['X-Auth-Token'].present?
end
def authenticate!
sha256 = Digest::SHA256.hexdigest(request.headers['X-Auth-Token'])
user = User.joins(:access_token).find_by(access_token: { sha256: })
if user
success!(user)
else
fail!('Invalid token')
end
rescue JWT::VerificationError
fail!('Invalid token')
end
end

Some files were not shown because too many files have changed in this diff Show More