Sync to upstream 3.0.2 — integrate 28 upstream commits with WaboSign rebrand

pull/687/head
Wabo 3 weeks ago
parent dca4a705ce
commit 977a98a5da

5
.gitattributes vendored

@ -1,6 +1 @@
*.html linguist-detectable=false *.html linguist-detectable=false
# Upstream-merge ergonomics. Lockfiles are regenerated post-merge rather
# than merged line-by-line — see REBRANDING.md "Sync workflow".
Gemfile.lock -merge
yarn.lock -merge

@ -1,22 +1,8 @@
--- ---
name: CI name: CI
on: [push] on: [push]
permissions: read-all
jobs: jobs:
rebrand_check:
name: Rebrand check
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- uses: actions/checkout@v4
- name: Install Ruby
uses: ruby/setup-ruby@v1
with:
ruby-version: 4.0.1
- name: Run rebrand-check
run: ruby bin/rebrand-check
rubocop: rubocop:
name: Rubocop name: Rubocop
runs-on: ubuntu-latest runs-on: ubuntu-latest
@ -74,12 +60,12 @@ jobs:
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- name: Install Node.js - name: Install Node.js
uses: actions/setup-node@v4 uses: actions/setup-node@v1
with: with:
node-version: 20.19.0 node-version: 20.19.0
- name: Cache directory path - name: Cache directory path
id: yarn-cache-dir-path id: yarn-cache-dir-path
run: echo "dir=$(yarn cache dir)" >> "$GITHUB_OUTPUT" run: echo "::set-output name=dir::$(yarn cache dir)"
- uses: actions/cache@v4 - uses: actions/cache@v4
id: yarn-cache id: yarn-cache
with: with:
@ -133,7 +119,7 @@ jobs:
env: env:
POSTGRES_USER: postgres POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres POSTGRES_PASSWORD: postgres
POSTGRES_DB: wabosign_test POSTGRES_DB: docuseal_test
ports: ["5432:5432"] ports: ["5432:5432"]
options: >- options: >-
--health-cmd pg_isready --health-cmd pg_isready
@ -148,7 +134,7 @@ jobs:
with: with:
ruby-version: 4.0.5 ruby-version: 4.0.5
- name: Set up Node - name: Set up Node
uses: actions/setup-node@v4 uses: actions/setup-node@v1
with: with:
node-version: 20.19.0 node-version: 20.19.0
- name: Install Chrome - name: Install Chrome
@ -185,7 +171,7 @@ jobs:
RAILS_ENV: test RAILS_ENV: test
NODE_ENV: test NODE_ENV: test
COVERAGE: true COVERAGE: true
DATABASE_URL: postgres://postgres:postgres@localhost:5432/wabosign_test DATABASE_URL: postgres://postgres:postgres@localhost:5432/docuseal_test
run: | run: |
bundle exec rake db:create bundle exec rake db:create
bundle exec rake db:migrate bundle exec rake db:migrate

@ -9,24 +9,19 @@ jobs:
build: build:
runs-on: ubuntu-24.04-arm runs-on: ubuntu-24.04-arm
timeout-minutes: 30 timeout-minutes: 30
permissions:
contents: read
packages: write
steps: steps:
- name: Checkout code - name: Checkout code
uses: actions/checkout@v4 uses: actions/checkout@v3
with: with:
submodules: recursive submodules: recursive
- name: Docker meta - name: Docker meta
id: meta id: meta
uses: docker/metadata-action@v5 uses: docker/metadata-action@v4
with: with:
images: ghcr.io/wabolabs/wabosign images: docuseal/docuseal
tags: | tags: type=semver,pattern={{version}}
type=semver,pattern={{version}}
type=raw,value=latest
- name: Set up QEMU - name: Set up QEMU
uses: docker/setup-qemu-action@v3 uses: docker/setup-qemu-action@v3
@ -37,12 +32,11 @@ jobs:
- name: Create .version file - name: Create .version file
run: echo ${{ github.ref_name }} > .version run: echo ${{ github.ref_name }} > .version
- name: Login to GitHub Container Registry - name: Login to Docker Hub
uses: docker/login-action@v3 uses: docker/login-action@v3
with: with:
registry: ghcr.io username: ${{ secrets.DOCKERHUB_USERNAME }}
username: ${{ github.actor }} password: ${{ secrets.DOCKERHUB_TOKEN }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push Docker image - name: Build and push Docker image
uses: docker/build-push-action@v6 uses: docker/build-push-action@v6
@ -51,7 +45,3 @@ jobs:
push: true push: true
platforms: linux/amd64,linux/arm64 platforms: linux/amd64,linux/arm64
tags: ${{ steps.meta.outputs.tags }} tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
build-args: |
VERSION=${{ github.ref_name }}
REVISION=${{ github.sha }}

1
.gitignore vendored

@ -38,4 +38,3 @@ yarn-debug.log*
/ee /ee
dump.rdb dump.rdb
*.onnx *.onnx
/vendor

@ -24,10 +24,6 @@ Metrics/BlockLength:
Style/Documentation: Style/Documentation:
Enabled: false Enabled: false
Rails/Exit:
Exclude:
- spec/rails_helper.rb
Lint/MissingSuper: Lint/MissingSuper:
Enabled: false Enabled: false

@ -1 +0,0 @@
1.3.0

@ -42,26 +42,14 @@ RUN echo "gem 'shakapacker'" > Gemfile && ./bin/shakapacker
FROM ruby:4.0.5-alpine AS app FROM ruby:4.0.5-alpine AS app
ARG VERSION="dev"
ARG REVISION="unknown"
LABEL org.opencontainers.image.title="WaboSign" \
org.opencontainers.image.description="Self-hosted, open-source document filling and signing platform — a WaboSign fork of DocuSeal." \
org.opencontainers.image.url="https://github.com/wabolabs/wabosign" \
org.opencontainers.image.source="https://github.com/wabolabs/wabosign" \
org.opencontainers.image.documentation="https://github.com/wabolabs/wabosign#readme" \
org.opencontainers.image.vendor="WaboLabs" \
org.opencontainers.image.licenses="AGPL-3.0-or-later" \
org.opencontainers.image.version="${VERSION}" \
org.opencontainers.image.revision="${REVISION}"
ENV RAILS_ENV=production ENV RAILS_ENV=production
ENV BUNDLE_WITHOUT="development:test" ENV BUNDLE_WITHOUT="development:test"
ENV OPENSSL_CONF=/etc/openssl_legacy.cnf ENV OPENSSL_CONF=/etc/openssl_legacy.cnf
WORKDIR /app WORKDIR /app
RUN apk add --no-cache libpq vips redis onnxruntime RUN apk add --no-cache libpq vips redis onnxruntime && \
rm -f /usr/bin/onnx_test_runner /usr/bin/onnxruntime_test
RUN addgroup -g 2000 wabosign && adduser -u 2000 -G wabosign -s /bin/sh -D -h /home/wabosign wabosign RUN addgroup -g 2000 wabosign && adduser -u 2000 -G wabosign -s /bin/sh -D -h /home/wabosign wabosign

@ -21,14 +21,10 @@ gem 'faraday'
gem 'faraday-follow_redirects' gem 'faraday-follow_redirects'
gem 'google-cloud-storage', require: false gem 'google-cloud-storage', require: false
gem 'hexapdf' gem 'hexapdf'
gem 'image_processing'
gem 'jwt', require: false gem 'jwt', require: false
gem 'lograge' gem 'lograge'
gem 'numo-narray-alt', require: false gem 'numo-narray-alt', require: false
gem 'oj' gem 'oj'
gem 'omniauth', '~> 2.1'
gem 'omniauth-google-oauth2', '~> 1.2'
gem 'omniauth-rails_csrf_protection', '~> 1.0'
gem 'onnxruntime', require: false gem 'onnxruntime', require: false
gem 'pagy' gem 'pagy'
gem 'pg', require: false gem 'pg', require: false

@ -264,9 +264,6 @@ GEM
strscan (>= 3.1.2) strscan (>= 3.1.2)
i18n (1.14.8) i18n (1.14.8)
concurrent-ruby (~> 1.0) concurrent-ruby (~> 1.0)
image_processing (1.14.0)
mini_magick (>= 4.9.5, < 6)
ruby-vips (>= 2.0.17, < 3)
io-console (0.8.2) io-console (0.8.2)
irb (1.18.0) irb (1.18.0)
pp (>= 0.6.0) pp (>= 0.6.0)
@ -274,7 +271,7 @@ GEM
rdoc (>= 4.0.0) rdoc (>= 4.0.0)
reline (>= 0.4.2) reline (>= 0.4.2)
jmespath (1.6.2) jmespath (1.6.2)
json (2.19.5) json (2.19.7)
jwt (3.2.0) jwt (3.2.0)
base64 base64
language_server-protocol (3.17.0.5) language_server-protocol (3.17.0.5)
@ -308,8 +305,6 @@ GEM
marcel (1.1.0) marcel (1.1.0)
matrix (0.4.3) matrix (0.4.3)
method_source (1.1.0) method_source (1.1.0)
mini_magick (5.3.1)
logger
mini_mime (1.1.5) mini_mime (1.1.5)
minitest (6.0.6) minitest (6.0.6)
drb (~> 2.0) drb (~> 2.0)
@ -434,7 +429,7 @@ GEM
erb erb
psych (>= 4.0.0) psych (>= 4.0.0)
tsort tsort
redis-client (0.28.0) redis-client (0.29.0)
connection_pool connection_pool
regexp_parser (2.11.3) regexp_parser (2.11.3)
reline (0.6.3) reline (0.6.3)
@ -516,12 +511,12 @@ GEM
rack-proxy (>= 0.6.1) rack-proxy (>= 0.6.1)
railties (>= 5.2) railties (>= 5.2)
semantic_range (>= 2.3.0) semantic_range (>= 2.3.0)
sidekiq (8.1.2) sidekiq (8.1.6)
connection_pool (>= 3.0.0) connection_pool (>= 3.0.0)
json (>= 2.16.0) json (>= 2.16.0)
logger (>= 1.7.0) logger (>= 1.7.0)
rack (>= 3.2.0) rack (>= 3.2.0)
redis-client (>= 0.26.0) redis-client (>= 0.29.0)
signet (0.21.0) signet (0.21.0)
addressable (~> 2.8) addressable (~> 2.8)
faraday (>= 0.17.5, < 3.a) faraday (>= 0.17.5, < 3.a)
@ -624,7 +619,6 @@ DEPENDENCIES
foreman foreman
google-cloud-storage google-cloud-storage
hexapdf hexapdf
image_processing
jwt jwt
letter_opener_web letter_opener_web
lograge lograge

@ -1,17 +1,5 @@
Additional Terms Additional Terms
In accordance with Section 7(b) of the GNU Affero General Public License, In accordance with Section 7(b) of the GNU Affero General Public License,
covered works derived from this software must retain attribution to: a covered work must retain the original DocuSeal attribution in interactive
user interfaces.
1. WaboSign — https://sign.wabo.cc — https://github.com/wabolabs/wabosign
2. DocuSeal (the upstream from which WaboSign is forked) —
https://github.com/docusealco/docuseal
Attribution must remain visible in interactive user interfaces (e.g. footer,
About page, generated emails, audit-trail PDFs, signing-completion screens).
WaboSign itself complies with this requirement by displaying both the
WaboSign and DocuSeal credits in the "Powered by" footer (see
app/views/shared/_powered_by.html.erb), in email attributions (see
app/views/shared/_email_attribution.html.erb), in generated PDF audit
trails, and in this repository's README and NOTICE files.

@ -1,77 +1,107 @@
<h1 align="center" style="border-bottom: none"> <h1 align="center" style="border-bottom: none">
<div> <div>
<a href="https://sign.wabo.cc"> <a href="https://www.docuseal.com">
<img alt="WaboSign" src="public/favicon.svg" width="80" /> <img alt="DocuSeal" src="https://github.com/user-attachments/assets/38b45682-ffa4-4919-abde-d2d422325c44" width="80" />
<br> <br>
</a> </a>
WaboSign DocuSeal
</div> </div>
</h1> </h1>
<h3 align="center"> <h3 align="center">
Self-hosted document filling and signing Open source document filling and signing
</h3> </h3>
<p align="center">
<a href="https://hub.docker.com/r/docuseal/docuseal">
<img alt="Docker releases" src="https://img.shields.io/docker/v/docuseal/docuseal">
</a>
<a href="https://discord.gg/qygYCDGck9">
<img src="https://img.shields.io/discord/1125112641170448454?logo=discord"/>
</a>
<a href="https://twitter.com/intent/follow?screen_name=docusealco">
<img src="https://img.shields.io/twitter/follow/docusealco?style=social" alt="Follow @docusealco" />
</a>
</p>
<p> <p>
WaboSign is a self-hosted, open-source platform for secure digital document signing and processing. Create PDF forms, fill them in online from any device, and collect signatures with an easy-to-use, mobile-optimized web tool. DocuSeal is an open source platform that provides secure and efficient digital document signing and processing. Create PDF forms to have them filled and signed online on any device with an easy-to-use, mobile-optimized web tool.
</p> </p>
<h2 align="center">
<a href="https://demo.docuseal.tech">✨ Live Demo</a>
<span>|</span>
<a href="https://docuseal.com/sign_up">☁️ Try in Cloud</a>
</h2>
WaboSign is a fork of [DocuSeal](https://github.com/docusealco/docuseal) under AGPLv3, with the upstream's "Pro" feature paywall removed so that every shipped capability is available out of the box on a self-hosted deployment. [![Demo](https://github.com/docusealco/docuseal/assets/5418788/d8703ea3-361a-423f-8bfe-eff1bd9dbe14)](https://demo.docuseal.tech)
## Features ## Features
- PDF form fields builder (WYSIWYG) - PDF form fields builder (WYSIWYG)
- 12 field types (Signature, Date, File, Checkbox, Phone, Verification, etc.) - 12 field types available (Signature, Date, File, Checkbox etc.)
- Multiple submitters per document - Multiple submitters per document
- Automated emails via SMTP - Automated emails via SMTP
- File storage on disk or AWS S3, Google Storage, Azure Blob - Files storage on disk or AWS S3, Google Storage, Azure Cloud
- Automatic PDF eSignature - Automatic PDF eSignature
- PDF signature verification - PDF signature verification
- User management and roles - Users management
- Mobile-optimized signing flow - Mobile-optimized
- 14 UI languages - 7 UI languages with signing available in 14 languages
- API + Webhooks for integrations - API and Webhooks for integrations
- SMS invitations via [BulkVS, Twilio, VoIP.ms, or SignalWire](SMS.md) - Easy to deploy in minutes
- Bulk send via CSV / XLSX import
- Google Workspace SSO ([setup guide](GOOGLE_SSO.md)) ## Pro Features
- Company logo and white-label
- User roles
- Automated reminders
- Invitation and identity verification via SMS
- Conditional fields and formulas - Conditional fields and formulas
- Custom branding (logo, colors, reply-to) - Bulk send with CSV, XLSX spreadsheet import
- Easy Docker deployment - SSO / SAML
- Template creation with HTML API ([Guide](https://www.docuseal.com/guides/create-pdf-document-fillable-form-with-html-api))
- Template creation with PDF or DOCX and field tags API ([Guide](https://www.docuseal.com/guides/use-embedded-text-field-tags-in-the-pdf-to-create-a-fillable-form))
- Embedded signing form ([React](https://github.com/docusealco/docuseal-react), [Vue](https://github.com/docusealco/docuseal-vue), [Angular](https://github.com/docusealco/docuseal-angular) or [JavaScript](https://www.docuseal.com/docs/embedded))
- Embedded document form builder ([React](https://github.com/docusealco/docuseal-react), [Vue](https://github.com/docusealco/docuseal-vue), [Angular](https://github.com/docusealco/docuseal-angular) or [JavaScript](https://www.docuseal.com/docs/embedded))
- [Learn more](https://www.docuseal.com/pricing)
## Deploy
|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.com/deploy/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 #### Docker
```sh ```sh
docker run --name wabosign -p 3000:3000 -v .:/data ghcr.io/wabolabs/wabosign:1.3.2 docker run --name docuseal -p 3000:3000 -v.:/data docuseal/docuseal
``` ```
`:latest` always tracks the most recent release; pin a `MAJOR.MINOR.PATCH` tag for reproducible deployments. By default DocuSeal docker container uses an SQLite database to store data and configurations. Alternatively, it is possible to use PostgreSQL or MySQL databases by specifying the `DATABASE_URL` env variable.
By default the container uses SQLite for data. Point at PostgreSQL or MySQL by setting `DATABASE_URL`. #### Docker Compose
### Docker Compose
Download docker-compose.yml into your private server:
```sh ```sh
sudo HOST=sign.example.com docker compose up curl https://raw.githubusercontent.com/docusealco/docuseal/master/docker-compose.yml > docker-compose.yml
``` ```
Make sure your DNS points at the server so Caddy can issue an SSL cert automatically. Run the app under a custom domain over https using docker compose (make sure your DNS points to the server to automatically issue ssl certs with Caddy):
```sh
## Authentication sudo HOST=your-domain-name.com docker compose up
```
WaboSign ships with email + password (Devise) and TOTP two-factor auth out of the box. Google Workspace SSO can be enabled by setting three environment variables — see [GOOGLE_SSO.md](GOOGLE_SSO.md) for the full operator guide. ## For Businesses
### Integrate seamless document signing into your web or mobile apps with DocuSeal
## Releases 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.
- **Current release:** 1.3.2 — see [CHANGELOG.md](CHANGELOG.md). [Book a Meeting](https://www.docuseal.com/contact)
- **Container image:** `ghcr.io/wabolabs/wabosign:1.3.2` (or `:latest`).
- **Versioning:** `MAJOR.MINOR.PATCH` per [semver.org](https://semver.org).
- **Tagging triggers a build:** pushing a `MAJOR.MINOR.PATCH` git tag runs [`.github/workflows/docker.yml`](.github/workflows/docker.yml), which builds `linux/amd64` + `linux/arm64` and pushes to GHCR.
## License ## License
WaboSign is distributed under the [GNU Affero General Public License v3.0](LICENSE), with the §7(b) [Additional Terms](LICENSE_ADDITIONAL_TERMS) preserved from upstream. Distributed under the AGPLv3 License with Section 7(b) Additional Terms. See [LICENSE](https://github.com/docusealco/docuseal/blob/master/LICENSE) and [LICENSE_ADDITIONAL_TERMS](https://github.com/docusealco/docuseal/blob/master/LICENSE_ADDITIONAL_TERMS) for more information.
Unless otherwise noted, all files © 2023-2026 DocuSeal LLC.
WaboSign is a fork of [DocuSeal](https://github.com/docusealco/docuseal) © 20232026 DocuSeal LLC. The upstream attribution required by §7(b) is preserved in interactive UIs and in the [NOTICE](NOTICE) file. Modifications © 2026 the WaboSign authors.
## Acknowledgements ## Tools
This software builds on the substantial work of the [DocuSeal](https://github.com/docusealco/docuseal) team. Their open-source release made this fork possible. WaboSign retains the embedding SDK contract (`<docuseal-form>`, `@docuseal/react`, `@docuseal/vue`, `@docuseal/angular`) so existing DocuSeal embedding code continues to work. - [Signature Maker](https://www.docuseal.com/online-signature)
- [Sign Document Online](https://www.docuseal.com/sign-documents-online)
- [Fill PDF Online](https://www.docuseal.com/fill-pdf)

@ -1,12 +1,14 @@
# Reporting a Vulnerability # Reporting a Vulnerability
If you discover a security concern or vulnerability in WaboSign, please report it privately by email to **wabosign@wabo.cc** rather than opening a public GitHub issue. We will acknowledge receipt, work with you to validate the report, and ship a fix. If you come across any security concern or vulnarability, please report the information via email to security@wabosign.com instead of opening a GitHub issue. We will promptly respond and will collaborate with you to validate the issue, and resolve it ASAP.
## Out of scope **We have a bug bounty program to reward security researchers.**
Out of scope vulnerabilities:
- CSRF - CSRF
- DNSSEC, CAA, CSP headers - DNSSEC, CAA, CSP headers
- DNS or email security related - DNS or email security related
- Rate limiting - Rate Limiting
We reserve the right to classify any reported issue as out of scope. Note: We reserve the right to classify any reported vulnerability as out of scope for the bug bounty program.

@ -11,8 +11,6 @@ module Api
@submitter = Submitter.find_by!(slug: params[:submitter_slug]) @submitter = Submitter.find_by!(slug: params[:submitter_slug])
unless can_upload?(@submitter) unless can_upload?(@submitter)
Rollbar.error("Can't upload: #{@submitter.id}") if defined?(Rollbar)
return render json: { error: I18n.t('form_has_been_archived') }, status: :unprocessable_content return render json: { error: I18n.t('form_has_been_archived') }, status: :unprocessable_content
end end
@ -33,9 +31,11 @@ module Api
return render json: { error: "#{params[:type]} error, try to sign on another device" }, return render json: { error: "#{params[:type]} error, try to sign on another device" },
status: :unprocessable_content status: :unprocessable_content
end end
metadata = { analyzed: true, identified: true, width: image.width, height: image.height }
end end
attachment = Submitters.create_attachment!(@submitter, file) attachment = Submitters.create_attachment!(@submitter, file, metadata:)
if params[:remember_signature] == 'true' && @submitter.email.present? if params[:remember_signature] == 'true' && @submitter.email.present?
cookies.encrypted[:signature_uuids] = build_new_cookie_signatures_json(@submitter, attachment) cookies.encrypted[:signature_uuids] = build_new_cookie_signatures_json(@submitter, attachment)

@ -9,20 +9,7 @@ module Api
templates = paginate(templates.preload(:author, folder: :parent_folder)) templates = paginate(templates.preload(:author, folder: :parent_folder))
schema_documents = schema_documents, dynamic_documents, preview_image_attachments = preload_relations(templates)
ActiveStorage::Attachment.where(record_id: templates.map(&:id),
record_type: 'Template',
name: :documents,
uuid: templates.flat_map { |t| t.schema.pluck('attachment_uuid') })
.preload(:blob)
preview_image_attachments =
ActiveStorage::Attachment.joins(:blob)
.where(blob: { filename: ['0.png', '0.jpg'] })
.where(record_id: schema_documents.map(&:id),
record_type: 'ActiveStorage::Attachment',
name: :preview_images)
.preload(:blob)
expires_at = Accounts.link_expires_at(current_account) expires_at = Accounts.link_expires_at(current_account)
@ -30,6 +17,7 @@ module Api
data: templates.map do |t| data: templates.map do |t|
Templates::SerializeForApi.call(t, Templates::SerializeForApi.call(t,
schema_documents: schema_documents.select { |e| e.record_id == t.id }, schema_documents: schema_documents.select { |e| e.record_id == t.id },
dynamic_documents:,
preview_image_attachments:, preview_image_attachments:,
expires_at:) expires_at:)
end, end,
@ -88,6 +76,41 @@ module Api
private private
def preload_relations(templates)
schema_documents =
ActiveStorage::Attachment.where(record_id: templates.map(&:id),
record_type: 'Template',
name: :documents,
uuid: templates.flat_map { |t| t.schema.pluck('attachment_uuid') })
.preload(:blob)
dynamic_document_uuids =
templates.flat_map { |t| t.schema.select { |item| item['dynamic'] }.pluck('attachment_uuid') }
dynamic_documents =
if dynamic_document_uuids.present?
DynamicDocument.where(template: templates.map(&:id))
.where(uuid: dynamic_document_uuids)
.preload(current_version: { document_attachment: :blob })
.select(:id, :uuid, :template_id, :sha1, :created_at, :updated_at)
else
DynamicDocument.none
end
preview_attachment_ids =
schema_documents.map(&:id) + dynamic_documents.filter_map { |d| d.current_version&.document_attachment&.id }
preview_image_attachments =
ActiveStorage::Attachment.joins(:blob)
.where(blob: { filename: ['0.png', '0.jpg'] })
.where(record_id: preview_attachment_ids,
record_type: 'ActiveStorage::Attachment',
name: :preview_images)
.preload(:blob)
[schema_documents, dynamic_documents, preview_image_attachments]
end
def filter_templates(templates, params) def filter_templates(templates, params)
templates = Templates.search(current_user, templates, params[:q]) templates = Templates.search(current_user, templates, params[:q])
templates = params[:archived].in?(['true', true]) ? templates.archived : templates.active templates = params[:archived].in?(['true', true]) ? templates.archived : templates.active

@ -121,6 +121,12 @@ class ApplicationController < ActionController::Base
Wabosign.default_url_options[:host] Wabosign.default_url_options[:host]
end end
def maybe_redirect_com
return if request.domain != 'wabosign.co'
redirect_to request.url.gsub('.co/', '.com/'), allow_other_host: true, status: :moved_permanently
end
def set_csp def set_csp
request.content_security_policy = current_content_security_policy.tap do |policy| request.content_security_policy = current_content_security_policy.tap do |policy|
policy.default_src :self policy.default_src :self

@ -0,0 +1,26 @@
# frozen_string_literal: true
class ConsoleRedirectController < ApplicationController
skip_before_action :authenticate_user!
skip_authorization_check
def index
if request.path == '/upgrade'
params[:redir] = Wabosign.multitenant? ? "#{Wabosign::CONSOLE_URL}/plans" : "#{Wabosign::CONSOLE_URL}/on_premises"
end
params[:redir] = "#{Wabosign::CONSOLE_URL}/manage" if request.path == '/manage'
return redirect_to(new_user_session_path({ redir: params[:redir] }.compact)) if true_user.blank?
auth = JsonWebToken.encode(uuid: true_user.uuid,
scope: :console,
exp: 1.minute.from_now.to_i)
redir_uri = Addressable::URI.parse(params[:redir])
path = redir_uri.path if params[:redir].to_s.starts_with?(Wabosign::CONSOLE_URL)
redirect_to "#{Wabosign::CONSOLE_URL}#{path}?#{{ **redir_uri&.query_values, 'auth' => auth }.to_query}",
allow_other_host: true
end
end

@ -28,7 +28,6 @@ class DashboardController < ApplicationController
def maybe_redirect_mfa_setup def maybe_redirect_mfa_setup
return unless signed_in? return unless signed_in?
return if current_user.otp_required_for_login return if current_user.otp_required_for_login
return if session[:bypass_otp_for_sso] || current_user.signed_in_via_sso?
return if !current_user.otp_required_for_login && !AccountConfig.exists?(value: true, return if !current_user.otp_required_for_login && !AccountConfig.exists?(value: true,
account_id: current_user.account_id, account_id: current_user.account_id,

@ -1,13 +1,18 @@
# frozen_string_literal: true # frozen_string_literal: true
class EmbedScriptsController < ActionController::Metal class EmbedScriptsController < ActionController::Metal
DUMMY_SCRIPT = <<~JAVASCRIPT DUMMY_SCRIPT = <<~JAVASCRIPT.freeze
const DummyBuilder = class extends HTMLElement { const DummyBuilder = class extends HTMLElement {
connectedCallback() { connectedCallback() {
this.innerHTML = ` this.innerHTML = `
<div style="text-align: center; padding: 20px; font-family: Arial, sans-serif;"> <div style="text-align: center; padding: 20px; font-family: Arial, sans-serif;">
<h2>Embedded components not loaded</h2> <h2>Upgrade to Pro</h2>
<p>The embed assets could not be served from this WaboSign instance. Check that the host is reachable and that the embed script is being served from the same origin.</p> <p>Unlock embedded components by upgrading to Pro</p>
<div style="margin-top: 40px;">
<a href="#{Wabosign::CONSOLE_URL}/on_premises" target="_blank" style="padding: 15px 25px; background-color: #222; color: white; text-decoration: none; border-radius: 5px; font-size: 16px; cursor: pointer;">
Learn More
</a>
</div>
</div> </div>
`; `;
} }
@ -15,11 +20,11 @@ class EmbedScriptsController < ActionController::Metal
const DummyForm = class extends DummyBuilder {}; const DummyForm = class extends DummyBuilder {};
if (!window.customElements.get('wabosign-builder')) { if (!window.customElements.get('docuseal-builder')) {
window.customElements.define('docuseal-builder', DummyBuilder); window.customElements.define('docuseal-builder', DummyBuilder);
} }
if (!window.customElements.get('wabosign-form')) { if (!window.customElements.get('docuseal-form')) {
window.customElements.define('docuseal-form', DummyForm); window.customElements.define('docuseal-form', DummyForm);
} }
JAVASCRIPT JAVASCRIPT

@ -0,0 +1,22 @@
# frozen_string_literal: true
class EnquiriesController < ApplicationController
skip_before_action :authenticate_user!
skip_authorization_check
def create
if params[:talk_to_sales] == 'on'
Faraday.post(Wabosign::ENQUIRIES_URL,
enquiry_params.merge(type: :talk_to_sales).to_json,
'Content-Type' => 'application/json')
end
head :ok
end
private
def enquiry_params
params.require(:user).permit(:email)
end
end

@ -1,12 +1,34 @@
# frozen_string_literal: true # frozen_string_literal: true
class ErrorsController < ActionController::Base class ErrorsController < ActionController::Base
ENTERPRISE_FEATURE_MESSAGE =
'This feature is available in Pro Edition: https://www.wabosign.com/pricing'
ENTERPRISE_PATHS = [
'/submissions/html',
'/api/submissions/html',
'/templates/html',
'/api/templates/html',
'/submissions/pdf',
'/api/submissions/pdf',
'/templates/pdf',
'/api/templates/pdf',
'/templates/doc',
'/api/templates/doc',
'/templates/docx',
'/api/templates/docx'
].freeze
SAFE_ERROR_MESSAGE_CLASSES = [ SAFE_ERROR_MESSAGE_CLASSES = [
ActionDispatch::Http::Parameters::ParseError, ActionDispatch::Http::Parameters::ParseError,
JSON::ParserError JSON::ParserError
].freeze ].freeze
def show def show
if request.original_fullpath.in?(ENTERPRISE_PATHS) && error_status_code == 404
return render json: { status: 404, message: ENTERPRISE_FEATURE_MESSAGE }, status: :not_found
end
respond_to do |f| respond_to do |f|
f.json do f.json do
set_cors_headers set_cors_headers

@ -11,6 +11,8 @@ class EsignSettingsController < ApplicationController
end end
end end
prepend_before_action :maybe_redirect_com, only: %i[show]
before_action :load_encrypted_config before_action :load_encrypted_config
authorize_resource :encrypted_config, parent: false, only: %i[new create] authorize_resource :encrypted_config, parent: false, only: %i[new create]
authorize_resource :encrypted_config, only: %i[update destroy show] authorize_resource :encrypted_config, only: %i[update destroy show]

@ -47,8 +47,7 @@ class McpController < ActionController::API
end end
def user_from_api_key def user_from_api_key
auth = request.headers['Authorization'].to_s token = request.headers['Authorization'].to_s[/\ABearer\s+(.+)\z/, 1]
token = auth.start_with?('Bearer ') ? auth[7..].strip.presence : nil
return if token.blank? return if token.blank?

@ -20,8 +20,7 @@ class MfaSetupController < ApplicationController
redirect_to settings_profile_index_path, notice: I18n.t('2fa_has_been_configured') redirect_to settings_profile_index_path, notice: I18n.t('2fa_has_been_configured')
else else
@provision_url = current_user.otp_provisioning_uri(current_user.email, @provision_url = current_user.otp_provisioning_uri(current_user.email, issuer: Wabosign.product_name)
issuer: Wabosign.branded_product_name(current_account))
@error_message = I18n.t('code_is_invalid') @error_message = I18n.t('code_is_invalid')
@ -50,7 +49,6 @@ class MfaSetupController < ApplicationController
current_user.save! current_user.save!
@provision_url = current_user.otp_provisioning_uri(current_user.email, @provision_url = current_user.otp_provisioning_uri(current_user.email, issuer: Wabosign.product_name)
issuer: Wabosign.branded_product_name(current_account))
end end
end end

@ -0,0 +1,21 @@
# frozen_string_literal: true
class NewslettersController < ApplicationController
skip_authorization_check
def show; end
def update
Faraday.post(Wabosign::NEWSLETTER_URL, newsletter_params.to_json, 'Content-Type' => 'application/json')
rescue StandardError => e
Rails.logger.error(e)
ensure
redirect_to root_path
end
private
def newsletter_params
params.require(:user).permit(:email)
end
end

@ -2,7 +2,6 @@
class PersonalizationSettingsController < ApplicationController class PersonalizationSettingsController < ApplicationController
ALLOWED_KEYS = [ ALLOWED_KEYS = [
AccountConfig::BRAND_NAME_KEY,
AccountConfig::FORM_COMPLETED_BUTTON_KEY, AccountConfig::FORM_COMPLETED_BUTTON_KEY,
AccountConfig::SUBMITTER_INVITATION_EMAIL_KEY, AccountConfig::SUBMITTER_INVITATION_EMAIL_KEY,
AccountConfig::SUBMITTER_INVITATION_REMINDER_EMAIL_KEY, AccountConfig::SUBMITTER_INVITATION_REMINDER_EMAIL_KEY,

@ -22,15 +22,14 @@ class SessionsController < Devise::SessionsController
super super
end end
def destroy
session.delete(:bypass_otp_for_sso)
super
end
private private
def after_sign_in_path_for(...) def after_sign_in_path_for(...)
return params[:redir] if params[:redir].present? if params[:redir].present?
return console_redirect_index_path(redir: params[:redir]) if params[:redir].starts_with?(Wabosign::CONSOLE_URL)
return params[:redir]
end
super super
end end

@ -40,7 +40,7 @@ class SetupController < ApplicationController
sign_in(@user) sign_in(@user)
redirect_to root_path redirect_to newsletter_path
else else
render :index, status: :unprocessable_content render :index, status: :unprocessable_content
end end

@ -1,67 +1,16 @@
# frozen_string_literal: true # frozen_string_literal: true
class SmsSettingsController < ApplicationController class SmsSettingsController < ApplicationController
SECRET_KEYS = %w[basic_auth_token twilio_auth_token voipms_api_password signalwire_api_token].freeze
before_action :load_encrypted_config before_action :load_encrypted_config
authorize_resource :encrypted_config, only: :index authorize_resource :encrypted_config, only: :index
authorize_resource :encrypted_config, parent: false, only: %i[create test_message] authorize_resource :encrypted_config, parent: false, except: :index
def index; end def index; end
def create
new_value = build_sms_value
if @encrypted_config.update(value: new_value)
redirect_to settings_sms_path, notice: I18n.t('changes_have_been_saved')
else
render :index, status: :unprocessable_content
end
rescue StandardError => e
flash[:alert] = e.message
render :index, status: :unprocessable_content
end
def test_message
to = params[:phone].to_s.strip
if to.blank?
flash[:alert] = 'Enter a phone number to test against.'
return redirect_to(settings_sms_path)
end
Sms.send_message(account: current_account,
to: to,
text: "Test SMS from #{Wabosign.branded_product_name(current_account)}.")
redirect_to settings_sms_path, notice: "Test SMS dispatched to #{to}."
rescue Sms::Error => e
redirect_to settings_sms_path, alert: "Test failed: #{e.message}"
rescue StandardError => e
redirect_to settings_sms_path, alert: "Unexpected error: #{e.message}"
end
private private
def load_encrypted_config def load_encrypted_config
@encrypted_config = @encrypted_config =
EncryptedConfig.find_or_initialize_by(account: current_account, EncryptedConfig.find_or_initialize_by(account: current_account, key: 'sms_configs')
key: EncryptedConfig::SMS_CONFIGS_KEY)
end
def build_sms_value
submitted = params.require(:encrypted_config).permit(value: {})[:value].to_h
existing = @encrypted_config.value || {}
# Password fields are rendered without their saved value, so an unedited
# submit posts back an empty string. Preserve the saved secret in that case
# so users can edit unrelated fields without re-pasting credentials.
SECRET_KEYS.each do |key|
submitted[key] = existing[key] if submitted[key].to_s.empty?
end
submitted['enabled'] = submitted['enabled'].to_s == '1' || submitted['enabled'].to_s == 'true'
submitted['provider'] = (submitted['provider'].presence || 'bulkvs').to_s
submitted.compact
end end
end end

@ -3,45 +3,13 @@
class SsoSettingsController < ApplicationController class SsoSettingsController < ApplicationController
before_action :load_encrypted_config before_action :load_encrypted_config
authorize_resource :encrypted_config, only: :index authorize_resource :encrypted_config, only: :index
authorize_resource :encrypted_config, parent: false, only: :create
def index; end def index; end
def create
new_value = build_sso_value
if @encrypted_config.update(value: new_value)
redirect_to settings_sso_index_path, notice: I18n.t('changes_have_been_saved')
else
render :index, status: :unprocessable_content
end
rescue StandardError => e
flash[:alert] = e.message
render :index, status: :unprocessable_content
end
private private
def load_encrypted_config def load_encrypted_config
@encrypted_config = @encrypted_config =
EncryptedConfig.find_or_initialize_by(account: current_account, EncryptedConfig.find_or_initialize_by(account: current_account, key: 'saml_configs')
key: EncryptedConfig::GOOGLE_SSO_KEY)
end
def build_sso_value
submitted = params.require(:encrypted_config).permit(value: {})[:value].to_h
existing = @encrypted_config.value || {}
# Don't clobber the saved secret with a blank one — the field is
# rendered empty (we never echo it back) so an unchanged form would
# otherwise wipe it out.
submitted['client_secret'] = existing['client_secret'] if submitted['client_secret'].to_s.empty?
submitted['allowed_domains'] =
submitted.delete('allowed_domains_csv').to_s.split(',').map(&:strip).reject(&:empty?)
submitted['enabled'] = submitted['enabled'].to_s == '1' || submitted['enabled'].to_s == 'true'
submitted.compact
end end
end end

@ -7,6 +7,7 @@ class StartFormController < ApplicationController
skip_authorization_check skip_authorization_check
around_action :with_browser_locale, only: %i[show update 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_resubmit_submitter, only: :update
before_action :load_template before_action :load_template
before_action :authorize_start!, only: :update before_action :authorize_start!, only: :update
@ -82,7 +83,7 @@ class StartFormController < ApplicationController
@submitter = Submitter.where(submission: @template.submissions) @submitter = Submitter.where(submission: @template.submissions)
.where.not(completed_at: nil) .where.not(completed_at: nil)
.find_by!(required_params.slice('email', 'phone')) .find_by!(required_params.except('name'))
end end
private private
@ -100,11 +101,18 @@ class StartFormController < ApplicationController
def load_resubmit_submitter def load_resubmit_submitter
@resubmit_submitter = @resubmit_submitter =
if params[:resubmit].present? && !params[:resubmit].in?([true, 'true']) if params[:resubmit].present? && !params[:resubmit].in?([true, 'true'])
Submitter.find_by(slug: params[:resubmit]) submitter = Submitter.find_by(slug: params[:resubmit])
submitter if submitter && can_resubmit?(submitter)
end end
end end
def can_resubmit?(submitter)
submitter.account.account_configs.find_or_initialize_by(key: AccountConfig::ALLOW_TO_RESUBMIT).value != false
end
def authorize_start! def authorize_start!
return redirect_to submit_form_path(@resubmit_submitter.slug) if @resubmit_submitter && @template.archived_at?
return redirect_to start_form_path(@template.slug) if @template.archived_at? return redirect_to start_form_path(@template.slug) if @template.archived_at?
return if @resubmit_submitter return if @resubmit_submitter
@ -120,7 +128,7 @@ class StartFormController < ApplicationController
required_params = required_fields.index_with { |key| submitter_params[key] } required_params = required_fields.index_with { |key| submitter_params[key] }
find_params = required_params.slice('email', 'phone') find_params = required_params.except('name')
submitter = Submitter.new if find_params.compact_blank.blank? submitter = Submitter.new if find_params.compact_blank.blank?

@ -6,6 +6,8 @@ class SubmissionsController < ApplicationController
load_and_authorize_resource :submission, only: %i[show destroy] load_and_authorize_resource :submission, only: %i[show destroy]
prepend_before_action :maybe_redirect_com, only: %i[show]
before_action only: :create do before_action only: :create do
authorize!(:create, Submission) authorize!(:create, Submission)
end end

@ -5,6 +5,8 @@ class SubmissionsPreviewController < ApplicationController
skip_before_action :authenticate_user! skip_before_action :authenticate_user!
skip_authorization_check skip_authorization_check
prepend_before_action :maybe_redirect_com, only: %i[show completed]
TTL = 40.minutes TTL = 40.minutes
def show def show

@ -1,10 +1,6 @@
# frozen_string_literal: true # frozen_string_literal: true
class TemplatesController < ApplicationController class TemplatesController < ApplicationController
TEMPLATE_FIELDS = %i[id author_id folder_id external_id name slug
schema fields submitters variables_schema preferences
shared_link source archived_at created_at updated_at].freeze
load_and_authorize_resource :template load_and_authorize_resource :template
def show def show
@ -31,19 +27,7 @@ class TemplatesController < ApplicationController
def new; end def new; end
def edit def edit
ActiveRecord::Associations::Preloader.new( @template_data = Templates.serialize_for_builder(@template)
records: [@template],
associations: [{ schema_documents: [:blob, { preview_images_attachments: :blob }] }]
).call
@template_data =
@template.as_json(only: TEMPLATE_FIELDS).merge(
documents: @template.schema_documents.as_json(
only: %i[id uuid],
methods: %i[metadata signed_key],
include: { preview_images: { only: %i[id], methods: %i[url metadata filename] } }
)
).to_json
render :edit, layout: 'plain' render :edit, layout: 'plain'
end end

@ -16,7 +16,12 @@ class TemplatesDetectFieldsController < ApplicationController
page_number = params[:page].presence&.to_i page_number = params[:page].presence&.to_i
documents.each do |document| documents.each do |document|
io = StringIO.new(document.download) io =
if document.image?
StringIO.new(document.preview_images.joins(:blob).find_by(blob: { filename: ['0.png', '0.jpg'] }).download)
else
StringIO.new(document.download)
end
Templates::DetectFields.call(io, attachment: document, page_number:) do |(attachment_uuid, page, fields)| Templates::DetectFields.call(io, attachment: document, page_number:) do |(attachment_uuid, page, fields)|
sse.write({ attachment_uuid:, page:, fields: }) sse.write({ attachment_uuid:, page:, fields: })

@ -4,18 +4,7 @@ class TemplatesPreviewController < ApplicationController
load_and_authorize_resource :template load_and_authorize_resource :template
def show def show
ActiveRecord::Associations::Preloader.new( @template_data = Templates.serialize_for_builder(@template)
records: [@template],
associations: [{ schema_documents: { preview_images_attachments: :blob } }]
).call
@template_data =
@template.as_json.merge(
documents: @template.schema_documents.as_json(
methods: %i[metadata signed_key],
include: { preview_images: { methods: %i[url metadata filename] } }
)
).to_json
render :show, layout: 'plain' render :show, layout: 'plain'
end end

@ -5,7 +5,9 @@ class TemplatesUploadsController < ApplicationController
layout 'plain' layout 'plain'
def show; end def show
redirect_to root_path if params[:url].blank?
end
def create def create
url_params = create_file_params_from_url if params[:url].present? url_params = create_file_params_from_url if params[:url].present?

@ -6,8 +6,6 @@ class UsersController < ApplicationController
before_action :build_user, only: %i[new create] before_action :build_user, only: %i[new create]
authorize_resource :user, only: %i[new create] authorize_resource :user, only: %i[new create]
before_action(only: :index) { authorize!(:manage, current_account) }
def index def index
@users = @users =
if params[:status] == 'archived' if params[:status] == 'archived'

@ -54,6 +54,7 @@ import GoogleDriveFilePicker from './elements/google_drive_file_picker'
import OpenModal from './elements/open_modal' import OpenModal from './elements/open_modal'
import BarChart from './elements/bar_chart' import BarChart from './elements/bar_chart'
import FieldCondition from './elements/field_condition' import FieldCondition from './elements/field_condition'
import ConfirmUpload from './elements/confirm_upload'
import * as TurboInstantClick from './lib/turbo_instant_click' import * as TurboInstantClick from './lib/turbo_instant_click'
@ -146,6 +147,7 @@ safeRegisterElement('google-drive-file-picker', GoogleDriveFilePicker)
safeRegisterElement('open-modal', OpenModal) safeRegisterElement('open-modal', OpenModal)
safeRegisterElement('bar-chart', BarChart) safeRegisterElement('bar-chart', BarChart)
safeRegisterElement('field-condition', FieldCondition) safeRegisterElement('field-condition', FieldCondition)
safeRegisterElement('confirm-upload', ConfirmUpload)
safeRegisterElement('template-builder', class extends HTMLElement { safeRegisterElement('template-builder', class extends HTMLElement {
connectedCallback () { connectedCallback () {
@ -197,10 +199,35 @@ safeRegisterElement('template-builder', class extends HTMLElement {
} }
onSubmit = (e) => { onSubmit = (e) => {
if (e.detail.success && e.detail?.formSubmission?.formElement?.id === 'submitters_form') { if (e.detail.success) {
e.detail.fetchResponse.response.json().then((data) => { if (e.detail?.formSubmission?.formElement?.id === 'submitters_form') {
this.component.template.submitters = data.submitters e.detail.fetchResponse.response.json().then((data) => {
}) this.component.template.submitters = data.submitters
})
}
if (e.detail?.formSubmission?.formElement?.action?.endsWith('/prefillable_fields')) {
e.detail.fetchResponse.response.text().then((data) => {
const doc = new DOMParser().parseFromString(data, 'text/html')
const fragment = doc.querySelector('turbo-stream template').content
const prefillableUuidsIndex = {}
fragment.querySelectorAll('[name="field_uuid"]').forEach((field) => {
prefillableUuidsIndex[field.value] = true
})
this.component.template.fields.forEach((field) => {
if (prefillableUuidsIndex[field.uuid]) {
field.prefillable = true
field.readonly = true
} else if (field.prefillable) {
delete field.prefillable
delete field.readonly
}
})
})
}
} }
} }

@ -0,0 +1,27 @@
import { target, targetable } from '@github/catalyst/lib/targetable'
export default targetable(class extends HTMLElement {
static [target.static] = [
'prompt',
'processing',
'logo'
]
connectedCallback () {
this.form.addEventListener('submit', this.onSubmit)
}
disconnectedCallback () {
this.form.removeEventListener('submit', this.onSubmit)
}
onSubmit = () => {
this.prompt.classList.add('hidden')
this.processing.classList.remove('hidden')
this.logo.classList.add('animate-bounce')
}
get form () {
return this.querySelector('form')
}
})

@ -77,17 +77,17 @@
<a <a
v-if="isDemo" v-if="isDemo"
target="_blank" target="_blank"
:href="githubUrl" href="https://github.com/docusealco/docuseal"
class="white-button flex items-center space-x-1 w-full" class="white-button flex items-center space-x-1 w-full"
> >
<IconBrandGithub /> <IconBrandGithub />
<span> <span>
{{ t('view_on_github') }} Star on Github
</span> </span>
</a> </a>
<a <a
v-if="isDemo" v-if="isDemo"
:href="productUrl" href="https://docuseal.com/sign_up"
class="white-button flex items-center space-x-1 w-full" class="white-button flex items-center space-x-1 w-full"
> >
<IconLogin /> <IconLogin />
@ -98,20 +98,14 @@
</div> </div>
<div <div
v-if="attribution" v-if="attribution"
class="text-center mt-4 text-sm" class="text-center mt-4"
> >
{{ t('powered_by') }} {{ t('powered_by') }}
<a <a
:href="productUrl" href="https://www.docuseal.com/start"
target="_blank" target="_blank"
class="underline" class="underline"
>{{ productName }}</a> >DocuSeal</a> - {{ t('open_source_documents_software') }}
&mdash; {{ t('based_on') }}
<a
href="https://github.com/docusealco/docuseal"
target="_blank"
class="underline"
>DocuSeal</a> (AGPLv3)
</div> </div>
</div> </div>
</template> </template>
@ -148,21 +142,6 @@ export default {
required: false, required: false,
default: true default: true
}, },
productName: {
type: String,
required: false,
default: 'WaboSign'
},
productUrl: {
type: String,
required: false,
default: 'https://sign.wabo.cc'
},
githubUrl: {
type: String,
required: false,
default: 'https://github.com/wabolabs/wabosign'
},
hasSignatureFields: { hasSignatureFields: {
type: Boolean, type: Boolean,
required: false, required: false,

@ -27,6 +27,7 @@
</label> </label>
<button <button
v-if="withToday" v-if="withToday"
type="button"
class="btn btn-outline btn-sm !normal-case font-normal set-current-date-button" class="btn btn-outline btn-sm !normal-case font-normal set-current-date-button"
@click.prevent="[setCurrentDate(), $emit('focus')]" @click.prevent="[setCurrentDate(), $emit('focus')]"
> >

@ -177,7 +177,7 @@ export default {
reader.onloadend = () => { reader.onloadend = () => {
resolve({ resolve({
url: reader.result, url: reader.result,
uuid: crypto.randomUUID(), uuid: Math.random().toString(),
filename: file.name filename: file.name
}) })
} }

@ -95,7 +95,7 @@
v-else v-else
id="complete_form_button" id="complete_form_button"
class="btn btn-sm btn-neutral text-white px-4 w-full flex justify-center" class="btn btn-sm btn-neutral text-white px-4 w-full flex justify-center"
form="steps_form" form="complete_form"
type="submit" type="submit"
name="completed" name="completed"
value="true" value="true"
@ -120,7 +120,7 @@
> >
<button <button
class="complete-button btn btn-sm btn-neutral text-white px-4" class="complete-button btn btn-sm btn-neutral text-white px-4"
form="steps_form" form="complete_form"
type="submit" type="submit"
name="completed" name="completed"
value="true" value="true"
@ -138,6 +138,14 @@
</span> </span>
</button> </button>
</Teleport> </Teleport>
<form
v-if="!isCompleted && !isInvite"
id="complete_form"
class="hidden"
:action="submitPath"
method="post"
@submit.prevent="submitStep"
/>
<button <button
v-if="!isFormVisible" v-if="!isFormVisible"
id="expand_form_button" id="expand_form_button"

@ -15,6 +15,7 @@
</template> </template>
</label> </label>
<button <button
type="button"
class="btn btn-outline btn-sm reupload-button" class="btn btn-outline btn-sm reupload-button"
@click.prevent="remove" @click.prevent="remove"
> >

@ -412,7 +412,7 @@ export default {
reader.readAsDataURL(file) reader.readAsDataURL(file)
reader.onloadend = () => { reader.onloadend = () => {
const attachment = { url: reader.result, uuid: crypto.randomUUID() } const attachment = { url: reader.result, uuid: Math.random().toString() }
this.$emit('attached', attachment) this.$emit('attached', attachment)
this.$emit('update:model-value', attachment.uuid) this.$emit('update:model-value', attachment.uuid)

@ -42,6 +42,7 @@
{{ error }} {{ error }}
</div> </div>
<button <button
type="button"
class="base-button w-full flex justify-center submit-form-button" class="base-button w-full flex justify-center submit-form-button"
@click="restartKba" @click="restartKba"
> >

@ -33,6 +33,7 @@
<div v-else> <div v-else>
<button <button
v-if="sessionId" v-if="sessionId"
type="button"
disabled disabled
class="base-button w-full modal-save-button" class="base-button w-full modal-save-button"
> >
@ -47,6 +48,7 @@
<button <button
v-else v-else
:id="field.uuid" :id="field.uuid"
type="button"
class="btn bg-[#7B73FF] text-white hover:bg-[#0A2540] text-lg w-full" class="btn bg-[#7B73FF] text-white hover:bg-[#0A2540] text-lg w-full"
:class="{ disabled: isCreatingCheckout }" :class="{ disabled: isCreatingCheckout }"
:disabled="isCreatingCheckout" :disabled="isCreatingCheckout"

@ -320,7 +320,7 @@
class="text-base-content/60 text-xs text-center w-full mt-1 select-none" class="text-base-content/60 text-xs text-center w-full mt-1 select-none"
> >
{{ t('by_clicking_you_agree_to_the').replace('{button}', buttonText.charAt(0).toUpperCase() + buttonText.slice(1)) }} <a {{ t('by_clicking_you_agree_to_the').replace('{button}', buttonText.charAt(0).toUpperCase() + buttonText.slice(1)) }} <a
:href="esignDisclosureUrl" href="https://www.wabosign.com/esign-disclosure"
target="_blank" target="_blank"
> >
<span class="inline md:hidden"> <span class="inline md:hidden">
@ -400,11 +400,6 @@ export default {
required: false, required: false,
default: false default: false
}, },
esignDisclosureUrl: {
type: String,
required: false,
default: ''
},
withQrButton: { withQrButton: {
type: Boolean, type: Boolean,
required: false, required: false,
@ -910,7 +905,7 @@ export default {
reader.readAsDataURL(file) reader.readAsDataURL(file)
reader.onloadend = () => { reader.onloadend = () => {
const attachment = { url: reader.result, uuid: crypto.randomUUID() } const attachment = { url: reader.result, uuid: Math.random().toString() }
this.$emit('attached', attachment) this.$emit('attached', attachment)
this.$emit('update:model-value', attachment.uuid) this.$emit('update:model-value', attachment.uuid)

@ -124,7 +124,7 @@ export default {
docId: this.eidEasyData.doc_id, docId: this.eidEasyData.doc_id,
language: this.locale, language: this.locale,
countryCode: this.countryCode, countryCode: this.countryCode,
sandbox: false, sandbox: ['demo.wabosign.tech'].includes(location.host),
enabledMethods: { enabledMethods: {
signature: this.eidEasyData.available_methods signature: this.eidEasyData.available_methods
}, },

@ -18,6 +18,16 @@
>&times;</a> >&times;</a>
</div> </div>
<div> <div>
<div
v-if="!withConditions"
class="bg-base-300 rounded-xl py-2 px-3 text-center"
>
<a
href="https://www.wabosign.com/pricing"
target="_blank"
class="link"
>{{ t('available_in_pro') }}</a>
</div>
<form @submit.prevent="validateSaveAndClose"> <form @submit.prevent="validateSaveAndClose">
<div class="my-4"> <div class="my-4">
<div <div

@ -270,6 +270,59 @@
</span> </span>
</div> </div>
</button> </button>
<div
v-else-if="type == 'phone' && (fieldTypes.length === 0 || fieldTypes.includes(type))"
class="tooltip tooltip-bottom flex"
:class="{'tooltip-bottom-end': withPayment, 'tooltip-bottom': !withPayment }"
:data-tip="t('unlock_sms_verified_phone_number_field_with_paid_plan_use_text_field_for_phone_numbers_without_verification')"
>
<a
href="https://www.wabosign.com/pricing"
target="_blank"
class="opacity-50 flex items-center justify-center border border-dashed border-base-300 w-full rounded relative fields-grid-item"
:style="{ backgroundColor }"
>
<div class="w-0 absolute left-0">
<IconLock
width="18"
height="18"
stroke-width="1.5"
/>
</div>
<div class="flex items-center flex-col px-2 py-2">
<component :is="icon" />
<span class="text-xs mt-1">
{{ fieldNames[type] }}
</span>
</div>
</a>
</div>
<div
v-else-if="withVerification === false && type == 'verification' && (fieldTypes.length === 0 || fieldTypes.includes(type))"
class="tooltip tooltip-bottom flex tooltip-bottom-start"
:data-tip="t('obtain_qualified_electronic_signature_with_the_trusted_provider_click_to_learn_more')"
>
<a
href="https://www.wabosign.com/qualified-electronic-signature"
target="_blank"
class="opacity-50 flex items-center justify-center border border-dashed border-base-300 w-full rounded relative fields-grid-item"
:style="{ backgroundColor }"
>
<div class="w-0 absolute left-0">
<IconLock
width="18"
height="18"
stroke-width="1.5"
/>
</div>
<div class="flex items-center flex-col px-2 py-2">
<component :is="icon" />
<span class="text-xs mt-1">
{{ fieldNames[type] }}
</span>
</div>
</a>
</div>
</template> </template>
</div> </div>
<div <div

@ -18,6 +18,16 @@
>&times;</a> >&times;</a>
</div> </div>
<div> <div>
<div
v-if="!withFormula"
class="bg-base-300 rounded-xl py-2 px-3 text-center"
>
<a
href="https://www.wabosign.com/pricing"
target="_blank"
class="link"
>{{ t('available_in_pro') }}</a>
</div>
<div class="flex flex-wrap mb-2 gap-y-1 pt-1"> <div class="flex flex-wrap mb-2 gap-y-1 pt-1">
<button <button
v-for="f in fields" v-for="f in fields"

@ -194,6 +194,13 @@
</span> </span>
</button> </button>
</form> </form>
<a
v-if="!isConnected"
class="block link text-center mt-1"
href="https://www.wabosign.com/blog/accept-payments-and-request-signatures-with-ease"
target="_blank"
data-turbo="false"
>{{ t('learn_more') }}</a>
</div> </div>
<li <li
class="field-settings-formula mb-1" class="field-settings-formula mb-1"

@ -5,7 +5,7 @@ class SendTestWebhookRequestJob
sidekiq_options retry: 0 sidekiq_options retry: 0
USER_AGENT = 'WaboSign Webhook' USER_AGENT = 'WaboSign.com Webhook'
HttpsError = Class.new(StandardError) HttpsError = Class.new(StandardError)
LocalhostError = Class.new(StandardError) LocalhostError = Class.new(StandardError)

@ -1,12 +1,7 @@
# frozen_string_literal: true # frozen_string_literal: true
class ApplicationMailer < ActionMailer::Base class ApplicationMailer < ActionMailer::Base
# Lambda is evaluated per-message in mailer context, so @current_account default from: 'WaboSign <info@wabosign.com>'
# (set by each mailer action) is available here.
default from: lambda {
account = instance_variable_defined?(:@current_account) ? @current_account : nil
"#{Wabosign.branded_product_name(account)} <#{Wabosign::SUPPORT_EMAIL}>"
}
layout 'mailer' layout 'mailer'
register_interceptor ActionMailerConfigsInterceptor register_interceptor ActionMailerConfigsInterceptor

@ -10,8 +10,7 @@ class UserMailer < ApplicationMailer
I18n.with_locale(@current_account.locale) do I18n.with_locale(@current_account.locale) do
mail(to: @user.friendly_name, mail(to: @user.friendly_name,
subject: I18n.t('you_are_invited_to_product_name', subject: I18n.t('you_are_invited_to_product_name', product_name: Wabosign.product_name))
product_name: Wabosign.branded_product_name(@current_account)))
end end
end end
end end

@ -18,13 +18,8 @@
# index_accounts_on_uuid (uuid) UNIQUE # index_accounts_on_uuid (uuid) UNIQUE
# #
class Account < ApplicationRecord class Account < ApplicationRecord
LOGO_CONTENT_TYPES = %w[image/png image/jpeg image/svg+xml].freeze
LOGO_MAX_BYTES = 2.megabytes
attribute :uuid, :string, default: -> { SecureRandom.uuid } attribute :uuid, :string, default: -> { SecureRandom.uuid }
has_one_attached :logo
has_many :users, dependent: :destroy has_many :users, dependent: :destroy
has_many :encrypted_configs, dependent: :destroy has_many :encrypted_configs, dependent: :destroy
has_many :account_configs, dependent: :destroy has_many :account_configs, dependent: :destroy
@ -66,10 +61,6 @@ class Account < ApplicationRecord
linked_account_account&.testing? linked_account_account&.testing?
end end
def brand_name
account_configs.find_by(key: AccountConfig::BRAND_NAME_KEY)&.value.to_s.strip.presence
end
def tz_info def tz_info
@tz_info ||= TZInfo::Timezone.get(ActiveSupport::TimeZone::MAPPING[timezone] || timezone) @tz_info ||= TZInfo::Timezone.get(ActiveSupport::TimeZone::MAPPING[timezone] || timezone)
end end

@ -22,7 +22,6 @@
# #
class AccountConfig < ApplicationRecord class AccountConfig < ApplicationRecord
SUBMITTER_INVITATION_EMAIL_KEY = 'submitter_invitation_email' SUBMITTER_INVITATION_EMAIL_KEY = 'submitter_invitation_email'
SUBMITTER_INVITATION_SMS_KEY = 'submitter_invitation_sms'
SUBMITTER_INVITATION_REMINDER_EMAIL_KEY = 'submitter_invitation_reminder_email' SUBMITTER_INVITATION_REMINDER_EMAIL_KEY = 'submitter_invitation_reminder_email'
SUBMITTER_COMPLETED_EMAIL_KEY = 'submitter_completed_email' SUBMITTER_COMPLETED_EMAIL_KEY = 'submitter_completed_email'
SUBMITTER_DOCUMENTS_COPY_EMAIL_KEY = 'submitter_documents_copy_email' SUBMITTER_DOCUMENTS_COPY_EMAIL_KEY = 'submitter_documents_copy_email'
@ -61,7 +60,6 @@ class AccountConfig < ApplicationRecord
TEMPLATE_CUSTOM_FIELDS_KEY = 'template_custom_fields' TEMPLATE_CUSTOM_FIELDS_KEY = 'template_custom_fields'
POLICY_LINKS_KEY = 'policy_links' POLICY_LINKS_KEY = 'policy_links'
ENABLE_MCP_KEY = 'enable_mcp' ENABLE_MCP_KEY = 'enable_mcp'
BRAND_NAME_KEY = 'brand_name'
EMAIL_VARIABLES = { EMAIL_VARIABLES = {
SUBMITTER_INVITATION_EMAIL_KEY => %w[template.name submitter.link account.name].freeze, SUBMITTER_INVITATION_EMAIL_KEY => %w[template.name submitter.link account.name].freeze,

@ -28,6 +28,12 @@ class DynamicDocument < ApplicationRecord
has_many :versions, class_name: 'DynamicDocumentVersion', dependent: :destroy has_many :versions, class_name: 'DynamicDocumentVersion', dependent: :destroy
has_one :current_version, class_name: 'DynamicDocumentVersion',
primary_key: %i[id sha1],
foreign_key: %i[dynamic_document_id sha1],
dependent: :destroy,
inverse_of: :dynamic_document
attribute :fields, :json attribute :fields, :json
before_validation :set_sha1 before_validation :set_sha1

@ -26,9 +26,7 @@ class EncryptedConfig < ApplicationRecord
EMAIL_SMTP_KEY = 'action_mailer_smtp', EMAIL_SMTP_KEY = 'action_mailer_smtp',
ESIGN_CERTS_KEY = 'esign_certs', ESIGN_CERTS_KEY = 'esign_certs',
TIMESTAMP_SERVER_URL_KEY = 'timestamp_server_url', TIMESTAMP_SERVER_URL_KEY = 'timestamp_server_url',
APP_URL_KEY = 'app_url', APP_URL_KEY = 'app_url'
GOOGLE_SSO_KEY = 'google_sso_configs',
SMS_CONFIGS_KEY = 'sms_configs'
].freeze ].freeze
belongs_to :account belongs_to :account

@ -75,7 +75,7 @@ class Template < ApplicationRecord
has_many :dynamic_document_versions, through: :dynamic_documents, source: :versions has_many :dynamic_document_versions, through: :dynamic_documents, source: :versions
has_many :schema_dynamic_documents, lambda { |e| has_many :schema_dynamic_documents, lambda { |e|
where(uuid: e.schema.select { |e| e['dynamic'] }.pluck('attachment_uuid')) where(uuid: e.schema.select { |item| item['dynamic'] }.pluck('attachment_uuid'))
}, class_name: 'DynamicDocument', dependent: :destroy, inverse_of: :template }, class_name: 'DynamicDocument', dependent: :destroy, inverse_of: :template
scope :active, -> { where(archived_at: nil) } scope :active, -> { where(archived_at: nil) }

@ -48,15 +48,13 @@
# #
class User < ApplicationRecord class User < ApplicationRecord
ROLES = [ ROLES = [
ADMIN_ROLE = 'admin', ADMIN_ROLE = 'admin'
EDITOR_ROLE = 'editor',
VIEWER_ROLE = 'viewer'
].freeze ].freeze
EMAIL_REGEXP = /[^@;,<>\s]+@[^@;,<>\s]+/ EMAIL_REGEXP = /[^@;,<>\s]+@[^@;,<>\s]+/
FULL_EMAIL_REGEXP = FULL_EMAIL_REGEXP =
/\A[a-z0-9_]+(?:[.'+-][a-z0-9_]+)*@(?:[a-z0-9]+[.-])*[a-z0-9]+\.[a-z]{2,}\z/i /\A[a-z0-9][.']?(?:(?:[a-z0-9_-]+[.+'])*[a-z0-9_-]+)*@(?:[a-z0-9]+[.-])*[a-z0-9]+\.[a-z]{2,}\z/i
has_one_attached :signature has_one_attached :signature
has_one_attached :initials has_one_attached :initials
@ -71,14 +69,7 @@ class User < ApplicationRecord
has_many :encrypted_configs, dependent: :destroy, class_name: 'EncryptedUserConfig' has_many :encrypted_configs, dependent: :destroy, class_name: 'EncryptedUserConfig'
has_many :email_messages, dependent: :destroy, foreign_key: :author_id, inverse_of: :author has_many :email_messages, dependent: :destroy, foreign_key: :author_id, inverse_of: :author
# :omniauthable is included unconditionally so the Devise routes are devise :two_factor_authenticatable, :recoverable, :rememberable, :validatable, :trackable, :lockable
# always declared. Whether the strategy actually works (and whether the
# Google button is shown on the sign-in page) is gated by
# Wabosign.google_sso_enabled? at runtime — driven by ENV and/or the
# `google_sso_configs` EncryptedConfig record.
devise :two_factor_authenticatable, :recoverable, :rememberable,
:validatable, :trackable, :lockable, :omniauthable,
omniauth_providers: [:google_oauth2]
attribute :role, :string, default: ADMIN_ROLE attribute :role, :string, default: ADMIN_ROLE
attribute :uuid, :string, default: -> { SecureRandom.uuid } attribute :uuid, :string, default: -> { SecureRandom.uuid }
@ -130,56 +121,4 @@ class User < ApplicationRecord
email email
end end
end end
def admin? = role == ADMIN_ROLE
def editor? = role == EDITOR_ROLE
def viewer? = role == VIEWER_ROLE
def signed_in_via_sso?
provider == 'google_oauth2' && uid.present?
end
def self.from_google_omniauth(auth)
raw_info = auth.extra&.raw_info
hosted_domain = raw_info.respond_to?(:hd) ? raw_info.hd : raw_info&.dig('hd')
return nil unless Wabosign.google_domain_allowed?(hosted_domain)
email = auth.info.email.to_s.downcase
return nil if email.blank?
user = find_by('lower(email) = ?', email)
if user
return nil if user.provider.present? && user.uid != auth.uid
user.update!(provider: 'google_oauth2', uid: auth.uid) if user.provider.blank?
return user
end
account = default_sso_account
return nil if account.nil?
create!(
account: account,
email: email,
first_name: auth.info.first_name,
last_name: auth.info.last_name,
role: ADMIN_ROLE,
password: SecureRandom.hex(32),
provider: 'google_oauth2',
uid: auth.uid,
confirmed_at: Time.current
)
end
def self.default_sso_account
# ENV override always wins.
return Account.find_by(id: Wabosign::GOOGLE_DEFAULT_ACCOUNT_ID) if Wabosign::GOOGLE_DEFAULT_ACCOUNT_ID.present?
# If an admin saved the Google SSO config via the UI, JIT-provision into
# that same account so admins land in the right tenant.
db_config = EncryptedConfig.find_by(key: EncryptedConfig::GOOGLE_SSO_KEY)
return db_config.account if db_config&.account && db_config.account.archived_at.nil?
Account.order(:created_at).first
end
end end

@ -70,7 +70,7 @@
<div class="flex items-center justify-between gap-4 py-2.5"> <div class="flex items-center justify-between gap-4 py-2.5">
<div class="flex items-center space-x-1"> <div class="flex items-center space-x-1">
<span class="text-left"><%= t('add_signature_id_to_the_documents') %></span> <span class="text-left"><%= t('add_signature_id_to_the_documents') %></span>
<span class="tooltip tooltip-top flex cursor-pointer" data-tip="<%= t('add_a_unique_signature_id_and_timestamp_to_each_signature_for_audit_and_traceability_purposes_along_with_the_timestamp_part_of_wabosigns_21_cfr_part_11_compliance_settings') %>"> <span class="tooltip tooltip-top flex cursor-pointer" data-tip="<%= t('add_a_unique_signature_id_and_timestamp_to_each_signature_for_audit_and_traceability_purposes_along_with_the_timestamp_part_of_docuseals_21_cfr_part_11_compliance_settings') %>">
<%= svg_icon('info_circle', class: 'hidden md:inline-block w-4 h-4 shrink-0') %> <%= svg_icon('info_circle', class: 'hidden md:inline-block w-4 h-4 shrink-0') %>
</span> </span>
</div> </div>
@ -87,7 +87,7 @@
<div class="flex items-center justify-between gap-4 py-2.5"> <div class="flex items-center justify-between gap-4 py-2.5">
<div class="flex items-center space-x-1"> <div class="flex items-center space-x-1">
<span class="text-left"><%= t('require_signing_reason') %></span> <span class="text-left"><%= t('require_signing_reason') %></span>
<span class="tooltip tooltip-top flex cursor-pointer" data-tip="<%= t('require_signer_to_provide_a_reason_for_signing_before_completing_their_signature_e_g_approvals_certifications_part_of_wabosigns_21_cfr_part_11_compliance_settings') %>"> <span class="tooltip tooltip-top flex cursor-pointer" data-tip="<%= t('require_signer_to_provide_a_reason_for_signing_before_completing_their_signature_e_g_approvals_certifications_part_of_docuseals_21_cfr_part_11_compliance_settings') %>">
<%= svg_icon('info_circle', class: 'hidden md:inline-block w-4 h-4 shrink-0') %> <%= svg_icon('info_circle', class: 'hidden md:inline-block w-4 h-4 shrink-0') %>
</span> </span>
</div> </div>
@ -142,9 +142,15 @@
<%= svg_icon('info_circle', class: 'hidden md:inline-block w-4 h-4 shrink-0') %> <%= svg_icon('info_circle', class: 'hidden md:inline-block w-4 h-4 shrink-0') %>
</span> </span>
</div> </div>
<submit-form data-on="change" class="flex"> <% if !Wabosign.multitenant? || can?(:manage, :disable_decline) %>
<%= f.check_box :value, class: 'toggle', checked: account_config.value != false %> <submit-form data-on="change" class="flex">
</submit-form> <%= f.check_box :value, class: 'toggle', checked: account_config.value != false %>
</submit-form>
<% else %>
<a href="<%= console_redirect_index_path(redir: "#{Wabosign::CONSOLE_URL}/plans") %>" data-turbo="false" data-tip="<%= I18n.t('unlock_with_docuseal_pro') %>" data-on="change" class="flex tooltip">
<%= f.check_box :value, class: 'toggle pointer-events-none', checked: account_config.value != false, disabled: true %>
</a>
<% end %>
</div> </div>
<% end %> <% end %>
<% end %> <% end %>
@ -159,9 +165,15 @@
<%= svg_icon('info_circle', class: 'hidden md:inline-block w-4 h-4 shrink-0') %> <%= svg_icon('info_circle', class: 'hidden md:inline-block w-4 h-4 shrink-0') %>
</span> </span>
</div> </div>
<submit-form data-on="change" class="flex"> <% if !Wabosign.multitenant? || can?(:manage, :delegate_form) %>
<%= f.check_box :value, class: 'toggle', checked: account_config.value == true %> <submit-form data-on="change" class="flex">
</submit-form> <%= f.check_box :value, class: 'toggle', checked: account_config.value == true %>
</submit-form>
<% else %>
<a href="<%= console_redirect_index_path(redir: "#{Wabosign::CONSOLE_URL}/plans") %>" data-turbo="false" data-tip="<%= I18n.t('unlock_with_docuseal_pro') %>" class="flex tooltip">
<%= f.check_box :value, class: 'toggle pointer-events-none', checked: account_config.value == true, disabled: true %>
</a>
<% end %>
</div> </div>
<% end %> <% end %>
<% end %> <% end %>
@ -233,7 +245,7 @@
</div> </div>
<% end %> <% end %>
<% end %> <% end %>
<% if true # personalization_advanced gate removed in fork %> <% if !Wabosign.multitenant? || can?(:manage, :personalization_advanced) %>
<% account_config = AccountConfig.find_or_initialize_by(account: current_account, key: AccountConfig::ENFORCE_SIGNING_ORDER_KEY) %> <% account_config = AccountConfig.find_or_initialize_by(account: current_account, key: AccountConfig::ENFORCE_SIGNING_ORDER_KEY) %>
<% if can?(:manage, account_config) %> <% if can?(:manage, account_config) %>
<%= form_for account_config, url: account_configs_path, method: :post do |f| %> <%= form_for account_config, url: account_configs_path, method: :post do |f| %>
@ -252,7 +264,7 @@
<% end %> <% end %>
<% end %> <% end %>
<% end %> <% end %>
<% if true # personalization_advanced gate removed in fork %> <% if !Wabosign.multitenant? || can?(:manage, :personalization_advanced) %>
<% account_config = AccountConfig.find_or_initialize_by(account: current_account, key: AccountConfig::WITH_FILE_LINKS_KEY) %> <% account_config = AccountConfig.find_or_initialize_by(account: current_account, key: AccountConfig::WITH_FILE_LINKS_KEY) %>
<% if can?(:manage, account_config) %> <% if can?(:manage, account_config) %>
<%= form_for account_config, url: account_configs_path, method: :post do |f| %> <%= form_for account_config, url: account_configs_path, method: :post do |f| %>

@ -5,6 +5,6 @@
<p><%= t('if_you_didnt_request_this_you_can_ignore_this_email') %></p> <p><%= t('if_you_didnt_request_this_you_can_ignore_this_email') %></p>
<p> <p>
<%= t('thanks') %>,<br> <%= t('thanks') %>,<br>
<%= Wabosign.branded_product_name(@resource&.account) %> <%= Wabosign.product_name %>
</p> </p>
<% content_for(:remove_attribution, true) %> <% content_for(:remove_attribution, true) %>

@ -1,12 +0,0 @@
<% if Wabosign.google_sso_enabled? %>
<div class="divider my-4 text-sm opacity-60"><%= t('or') %></div>
<div class="form-control">
<%= button_to user_google_oauth2_omniauth_authorize_path,
method: :post,
data: { turbo: false },
class: 'base-button !bg-white !text-base-content border border-base-300 flex items-center justify-center gap-2' do %>
<img src="/google_g.svg" alt="" width="20" height="20" class="w-5 h-5">
<span><%= t('sign_in_with_google') %></span>
<% end %>
</div>
<% end %>

@ -0,0 +1,25 @@
<tr scope="row" class="group">
<td class="flex items-center space-x-1">
<%= svg_icon('discount_check_filled', class: 'w-6 h-6 text-green-500') %>
<span class="flex items-center">
<%= t('wabosign_trusted_signature') %>
<div class="tooltip ml-1" data-tip="<%= t('sign_documents_with_trusted_certificate_provided_by_docu_seal_your_documents_and_data_are_never_shared_with_docu_seal_p_d_f_checksum_is_provided_to_generate_a_trusted_signature') %>">
<%= svg_icon('circle_question', class: 'w-4 h-4 stroke-1') %>
</div>
</span>
</td>
<td>
<a href="<%= "#{Wabosign::CLOUD_URL}/sign_up?#{{ redir: "#{Wabosign::CONSOLE_URL}/on_premises" }.to_query}" %>" class="btn btn-neutral btn-sm text-white">
<%= t('unlock_with_docuseal_pro') %>
</a>
</td>
<td>
<div class="tooltip" data-tip="<%= t('unlock_with_docuseal_pro') %>">
<%= button_to settings_esign_path, method: :put, params: { name: Wabosign::AATL_CERT_NAME }, class: 'btn btn-outline btn-neutral btn-xs whitespace-nowrap', title: t('make_default'), disabled: true do %>
<%= t('make_default') %>
<% end %>
</div>
</td>
<td>
</td>
</tr>

@ -99,11 +99,14 @@
</td> </td>
</tr> </tr>
<% end %> <% end %>
<% unless Wabosign.multitenant? %>
<%= render 'default_signature_row' %>
<% end %>
</tbody> </tbody>
</table> </table>
</div> </div>
<% encrypted_config = EncryptedConfig.find_or_initialize_by(account: current_account, key: EncryptedConfig::TIMESTAMP_SERVER_URL_KEY) %> <% encrypted_config = EncryptedConfig.find_or_initialize_by(account: current_account, key: EncryptedConfig::TIMESTAMP_SERVER_URL_KEY) %>
<% if can?(:manage, encrypted_config) %> <% if !Wabosign.multitenant? && can?(:manage, encrypted_config) %>
<div class="flex-grow max-w-xl"> <div class="flex-grow max-w-xl">
<div class="flex justify-between items-end mb-4 mt-8"> <div class="flex justify-between items-end mb-4 mt-8">
<h2 class="text-3xl font-bold"> <h2 class="text-3xl font-bold">

@ -1,7 +1,7 @@
<div class="max-w-xl mx-auto px-2"> <div class="max-w-xl mx-auto px-2">
<h1 class="flex text-4xl font-bold items-center justify-center my-8 space-x-2"> <h1 class="flex text-4xl font-bold items-center justify-center my-8 space-x-2">
<%= svg_icon('waving_hand', class: 'h-10 w-10') %> <%= svg_icon('waving_hand', class: 'h-10 w-10') %>
<span><%= t('welcome_to_product_name', product_name: Wabosign.branded_product_name(current_account)) %></span> <span><%= t('welcome_to_product_name', product_name: Wabosign.product_name) %></span>
</h1> </h1>
<%= form_for(resource, as: resource_name, url: invitation_path, html: { method: :put, class: 'space-y-6' }) do |f| %> <%= form_for(resource, as: resource_name, url: invitation_path, html: { method: :put, class: 'space-y-6' }) do |f| %>
<div class="space-y-2"> <div class="space-y-2">

@ -1,5 +1,4 @@
<% brand = Wabosign.branded_product_name(signed_in? ? current_account : nil) %>
<title> <title>
<%= content_for(:html_title) || (signed_in? ? brand : "#{brand} | Open Source Document Signing") %> <%= content_for(:html_title) || (signed_in? ? 'WaboSign' : 'WaboSign | Open Source Document Signing') %>
</title> </title>
<%= render 'shared/meta' %> <%= render 'shared/meta' %>

@ -33,7 +33,7 @@
</p> </p>
<div class="card bg-base-200"> <div class="card bg-base-200">
<div class="card-body p-6"> <div class="card-body p-6">
<p class="text-2xl font-semibold"><%= t('connect_to_product_name_mcp', product_name: Wabosign.branded_product_name(current_account)) %></p> <p class="text-2xl font-semibold"><%= t('connect_to_docuseal_mcp') %></p>
<p class="text-lg"><%= t('add_the_following_to_your_mcp_client_configuration') %>:</p> <p class="text-lg"><%= t('add_the_following_to_your_mcp_client_configuration') %>:</p>
<div class="mockup-code overflow-hidden"> <div class="mockup-code overflow-hidden">
<% text = JSON.pretty_generate({ mcpServers: { wabosign: { type: 'http', url: "#{root_url(Wabosign.default_url_options)}mcp", headers: { Authorization: "Bearer #{@mcp_token.token}" } } } }).strip %> <% text = JSON.pretty_generate({ mcpServers: { wabosign: { type: 'http', url: "#{root_url(Wabosign.default_url_options)}mcp", headers: { Authorization: "Bearer #{@mcp_token.token}" } } } }).strip %>

@ -0,0 +1,14 @@
<div class="max-w-xl mx-auto px-2">
<h1 class="text-4xl font-bold text-center my-8">👨‍💻 <%= t('developer_newsletters') %></h1>
<%= form_for current_user, url: newsletter_path do |f| %>
<div class="form-control">
<%= f.email_field :email, placeholder: t('email'), required: true, class: 'base-input' %>
</div>
<div class="form-control mt-4">
<%= f.button button_title, class: 'base-button' %>
</div>
<div class="text-center mt-2">
<a href="/" class="link"><%= t('skip') %></a>
</div>
<% end %>
</div>

@ -0,0 +1 @@
<%= render 'reminder_placeholder' %>

@ -2,7 +2,7 @@
<%= f.hidden_field :key %> <%= f.hidden_field :key %>
<div class="form-control"> <div class="form-control">
<% record = Struct.new(:first_duration, :second_duration, :third_duration).new(*(f.object.value || {}).values_at('first_duration', 'second_duration', 'third_duration')) %> <% record = Struct.new(:first_duration, :second_duration, :third_duration).new(*(f.object.value || {}).values_at('first_duration', 'second_duration', 'third_duration')) %>
<% durations = AccountConfigs::REMINDER_DURATIONS.keys.map { |v| [t(v.underscore), v] } %> <% durations = (Wabosign.multitenant? ? AccountConfigs::REMINDER_DURATIONS.except('one_hour', 'two_hours') : AccountConfigs::REMINDER_DURATIONS).keys.map { |v| [t(v.underscore), v] } %>
<div class="flex flex-col md:flex-row gap-2"> <div class="flex flex-col md:flex-row gap-2">
<div class="w-full"> <div class="w-full">
<%= f.fields_for :value, record do |ff| %> <%= f.fields_for :value, record do |ff| %>

@ -0,0 +1,15 @@
<div class="alert my-4">
<%= svg_icon('info_circle', class: 'w-6 h-6') %>
<div>
<p class="font-bold">
<%= t('unlock_with_docuseal_pro') %>
</p>
<p>
<%= t('send_automatic_email_reminders_to_your_recipients') %>
<br>
<a class="link font-medium" target="_blank" href="<%= Wabosign.multitenant? ? console_redirect_index_path(redir: "#{Wabosign::CONSOLE_URL}/plans") : "#{Wabosign::CLOUD_URL}/sign_up?#{{ redir: "#{Wabosign::CONSOLE_URL}/on_premises" }.to_query}" %>" data-turbo="false">
<%= t('learn_more') %>
</a>
</p>
</div>
</div>

@ -26,6 +26,7 @@
<%= t('sign_request_email_reminders') %> <%= t('sign_request_email_reminders') %>
</h2> </h2>
</div> </div>
<%= render 'reminder_banner' %>
<%= render 'reminder_form', config: @reminder_config %> <%= render 'reminder_form', config: @reminder_config %>
</div> </div>
<div class="w-0 md:w-52"></div> <div class="w-0 md:w-52"></div>

@ -5,10 +5,10 @@
<div class="relative flex flex-col items-center"> <div class="relative flex flex-col items-center">
<%= render 'shared/logo', width: '100', height: '100' %> <%= render 'shared/logo', width: '100', height: '100' %>
<h1 class="text-6xl font-bold mt-4 mb-4"> <h1 class="text-6xl font-bold mt-4 mb-4">
<%= Wabosign.branded_product_name %> WaboSign
</h1> </h1>
<% if Wabosign.version.present? %> <% if Wabosign.version.present? %>
<a href="<%= Wabosign::GITHUB_URL %>/releases" target="_blank" class="badge badge-outline badge-lg block mx-auto"> <a href="https://github.com/wabolabs/wabosign/releases" target="_blank" class="badge badge-outline badge-lg block mx-auto">
v<%= Wabosign.version %> v<%= Wabosign.version %>
</a> </a>
<% end %> <% end %>
@ -27,7 +27,7 @@
</div> </div>
<h3 class="mb-4 text-2xl font-semibold">Easy to Start</h3> <h3 class="mb-4 text-2xl font-semibold">Easy to Start</h3>
<p class="text-base text-gray-500"> <p class="text-base text-gray-500">
Run on your own host using Docker container, or deploy on your favorite managed PaaS. Run on your own host using Docker container, or deploy on your favorite managed PaaS with a single <a href="https://www.wabosign.com/install" class="link link-neutral font-bold">click</a>.
</p> </p>
</div> </div>
</div> </div>
@ -67,7 +67,7 @@
</div> </div>
<h3 class="mb-4 text-2xl font-semibold">Open Source</h3> <h3 class="mb-4 text-2xl font-semibold">Open Source</h3>
<p class="text-base text-gray-500"> <p class="text-base text-gray-500">
Source code is available on <a href="<%= Wabosign::GITHUB_URL %>" class="link link-neutral font-bold" target="_blank">GitHub</a>.<br> Source code is available under <a href="<%= Wabosign::GITHUB_URL %>" class="link link-neutral font-bold" target="_blank">github.com/docusealco</a>.<br>
Open-source contributors are always ready to help! Open-source contributors are always ready to help!
</p> </p>
</div> </div>

@ -14,10 +14,12 @@
<%= ff.text_field :subject, required: true, class: 'base-input', dir: 'auto' %> <%= ff.text_field :subject, required: true, class: 'base-input', dir: 'auto' %>
</div> </div>
<%= render 'personalization_settings/email_body_field', ff:, config: f.object %> <%= render 'personalization_settings/email_body_field', ff:, config: f.object %>
<div class="form-control"> <% if can?(:manage, :reply_to) || can?(:manage, :personalization_advanced) %>
<%= ff.label :reply_to, t('reply_to'), class: 'label' %> <div class="form-control">
<%= ff.email_field :reply_to, class: 'base-input', dir: 'auto', placeholder: t(:email) %> <%= ff.label :reply_to, t('reply_to'), class: 'label' %>
</div> <%= ff.email_field :reply_to, class: 'base-input', dir: 'auto', placeholder: t(:email) %>
</div>
<% end %>
<div class="space-y-3.5"> <div class="space-y-3.5">
<div class="flex items-center justify-between mx-1"> <div class="flex items-center justify-between mx-1">
<span> <span>
@ -31,18 +33,22 @@
</span> </span>
<%= ff.check_box :attach_audit_log, { checked: ff.object.attach_audit_log != false, class: 'toggle' }, 'true', 'false' %> <%= ff.check_box :attach_audit_log, { checked: ff.object.attach_audit_log != false, class: 'toggle' }, 'true', 'false' %>
</div> </div>
<div class="flex items-center justify-between mx-1"> <% unless Wabosign.multitenant? %>
<span> <div class="flex items-center justify-between mx-1">
<%= t('bcc_recipients') %> <span>
</span> <%= t('bcc_recipients') %>
<%= ff.check_box :bcc_recipients, { checked: ff.object.bcc_recipients == true, class: 'toggle' }, 'true', 'false' %> </span>
</div> <%= ff.check_box :bcc_recipients, { checked: ff.object.bcc_recipients == true, class: 'toggle' }, 'true', 'false' %>
<div class="flex items-center justify-between mx-1"> </div>
<span> <% end %>
<%= t('send_emails_automatically_on_completion') %> <% if !Wabosign.multitenant? || can?(:manage, :personalization_advanced) %>
</span> <div class="flex items-center justify-between mx-1">
<%= ff.check_box :enabled, { checked: ff.object.enabled != false, class: 'toggle' }, 'true', 'false' %> <span>
</div> <%= t('send_emails_automatically_on_completion') %>
</span>
<%= ff.check_box :enabled, { checked: ff.object.enabled != false, class: 'toggle' }, 'true', 'false' %>
</div>
<% end %>
</div> </div>
<% end %> <% end %>
<div class="form-control pt-2"> <div class="form-control pt-2">

@ -1,32 +1 @@
<div class="space-y-4"> <%= render 'logo_placeholder' %>
<% if current_account.logo.attached? %>
<div class="flex items-center justify-between bg-base-200 rounded-xl p-4">
<div class="flex items-center gap-3">
<%= image_tag rails_blob_path(current_account.logo, disposition: 'inline'),
class: 'w-12 h-12 object-contain bg-white rounded',
alt: current_account.name %>
<span class="text-sm opacity-70"><%= current_account.logo.filename %></span>
</div>
<%= button_to 'Remove', settings_account_logo_path, method: :delete,
class: 'btn btn-sm btn-outline btn-error',
data: { turbo_confirm: 'Remove the uploaded logo?' } %>
</div>
<% end %>
<%= form_with url: settings_account_logo_path, method: :post,
multipart: true, html: { class: 'space-y-3', autocomplete: 'off' } do %>
<div class="form-control">
<label class="label" for="logo_file">
<span class="label-text">Upload logo (PNG, JPEG, or SVG &middot; up to 2&nbsp;MB)</span>
</label>
<input type="file" name="logo" id="logo_file" required
accept="image/png,image/jpeg,image/svg+xml"
class="file-input file-input-bordered w-full">
</div>
<button type="submit" class="base-button">Upload</button>
<% end %>
<p class="text-sm opacity-70">
Replaces the default WaboSign mark on the sign-in page, signing flow, dashboard navbar, share-link QR page, and audit-trail PDFs. Browser favicons and the PWA manifest icon stay on the default brand.
</p>
</div>

@ -0,0 +1,15 @@
<div class="alert my-4">
<%= svg_icon('info_circle', class: 'w-6 h-6') %>
<div>
<p class="font-bold">
<%= t('unlock_with_docuseal_pro') %>
</p>
<p>
<%= t('display_your_company_name_and_logo_when_signing_documents') %>
<br>
<a class="link font-medium" target="_blank" href="<%= Wabosign.multitenant? ? console_redirect_index_path(redir: "#{Wabosign::CONSOLE_URL}/plans") : "#{Wabosign::CLOUD_URL}/sign_up?#{{ redir: "#{Wabosign::CONSOLE_URL}/on_premises" }.to_query}" %>" data-turbo="false">
<%= t('learn_more') %>
</a>
</p>
</div>
</div>

@ -8,12 +8,7 @@
<%= render 'signature_request_email_form' %> <%= render 'signature_request_email_form' %>
<%= render 'documents_copy_email_form' %> <%= render 'documents_copy_email_form' %>
<%= render 'submitter_completed_email_form' %> <%= render 'submitter_completed_email_form' %>
<%= render 'signature_request_sms_form' %>
</div> </div>
<p class="text-4xl font-bold mb-4 mt-8">
Product name
</p>
<%= render 'brand_name_form' %>
<p class="text-4xl font-bold mb-4 mt-8"> <p class="text-4xl font-bold mb-4 mt-8">
<%= t('company_logo') %> <%= t('company_logo') %>
</p> </p>

@ -36,7 +36,7 @@
<% if signature %> <% if signature %>
<div class="flex justify-center mb-4 relative"> <div class="flex justify-center mb-4 relative">
<%= button_to button_title(title: t('remove'), disabled_with: t('removing')), user_signature_path, method: :delete, class: 'right-0 top-0 absolute link' %> <%= button_to button_title(title: t('remove'), disabled_with: t('removing')), user_signature_path, method: :delete, class: 'right-0 top-0 absolute link' %>
<img src="<%= signature.url %>" style="max-height: 200px; width: auto" width="<%= signature.metadata['width'] %>" height="<%= signature.metadata['height'] %>"> <img src="<%= signature.url %>" style="height: 130px; width: 100%; object-fit: contain;">
</div> </div>
<% end %> <% end %>
<a href="<%= edit_user_signature_path %>" data-turbo-frame="modal" class="base-button w-full"> <a href="<%= edit_user_signature_path %>" data-turbo-frame="modal" class="base-button w-full">
@ -49,7 +49,7 @@
<% if initials %> <% if initials %>
<div class="flex justify-center mb-4 relative"> <div class="flex justify-center mb-4 relative">
<%= button_to button_title(title: t('remove'), disabled_with: t('removing')), user_initials_path, method: :delete, class: 'right-0 top-0 absolute link' %> <%= button_to button_title(title: t('remove'), disabled_with: t('removing')), user_initials_path, method: :delete, class: 'right-0 top-0 absolute link' %>
<img src="<%= initials.url %>" style="max-height: 200px; width: auto" width="<%= initials.metadata['width'] %>" height="<%= initials.metadata['height'] %>"> <img src="<%= initials.url %>" style="height: 130px; width: 100%; object-fit: contain;">
</div> </div>
<% end %> <% end %>
<a href="<%= edit_user_initials_path %>" data-turbo-frame="modal" class="base-button w-full"> <a href="<%= edit_user_initials_path %>" data-turbo-frame="modal" class="base-button w-full">

@ -1,7 +1,6 @@
<% brand = Wabosign.branded_product_name %>
{ {
"name": "<%= brand %>", "name": "<%= Wabosign.product_name %>",
"short_name": "<%= brand %>", "short_name": "<%= Wabosign.product_name %>",
"id": "/", "id": "/",
"icons": [ "icons": [
{ {
@ -19,7 +18,7 @@
"display": "standalone", "display": "standalone",
"scope": "/", "scope": "/",
"orientation": "any", "orientation": "any",
"description": "<%= brand %> is an open source platform that provides secure and efficient digital document signing and processing.", "description": "<%= Wabosign.product_name %> is an open source platform that provides secure and efficient digital document signing and processing.",
"categories": ["productivity", "utilities"], "categories": ["productivity", "utilities"],
"theme_color": "#FAF7F4", "theme_color": "#FAF7F4",
"background_color": "#FAF7F4" "background_color": "#FAF7F4"

@ -1 +1 @@
<%= render 'shared/powered_by', with_counter: local_assigns[:with_counter], link_path: local_assigns[:link_path], account: local_assigns[:account] %> <%= render 'shared/powered_by', with_counter: local_assigns[:with_counter], link_path: local_assigns[:link_path] %>

@ -1,17 +1,10 @@
<p> <p>
--- ---
</p> </p>
<% brand = Wabosign.branded_product_name(@current_account) %>
<p> <p>
<% if @current_account&.testing? %> <% if @current_account&.testing? %>
<%= t('sent_using_product_name_in_testing_mode_html', product_url: "#{Wabosign::PRODUCT_EMAIL_URL}/start", product_name: brand) %> <%= t('sent_using_product_name_in_testing_mode_html', product_url: "#{Docuseal::PRODUCT_EMAIL_URL}/start", product_name: Docuseal.product_name) %>
<% else %> <% else %>
<%= t('sent_using_product_name_free_document_signing_html', product_url: "#{Wabosign::PRODUCT_EMAIL_URL}/start", product_name: brand) %> <%= t('sent_using_product_name_free_document_signing_html', product_url: "#{Docuseal::PRODUCT_EMAIL_URL}/start", product_name: Docuseal.product_name) %>
<% end %> <% end %>
</p> </p>
<%# AGPL §7(b) DocuSeal attribution. Do not remove or rebrand. %>
<p style="font-size: 11px; opacity: 0.7;">
<%= t('based_on') %>
<a href="<%= Wabosign::UPSTREAM_URL %>"><%= Wabosign::UPSTREAM_NAME %></a>
(AGPLv3)
</p>

@ -1,6 +1,6 @@
<a target="_blank" href="<%= Wabosign::GITHUB_URL %>" rel="noopener noreferrer nofollow" class="relative flex items-center rounded-full px-2 py-0.5 text-xs leading-4 mt-1 text-base-content border border-base-300 tooltip tooltip-bottom" data-tip="Give a star on GitHub"> <a target="_blank" href="<%= Wabosign::GITHUB_URL %>" rel="noopener noreferrer nofollow" class="relative flex items-center rounded-full px-2 py-0.5 text-xs leading-4 mt-1 text-base-content border border-base-300 tooltip tooltip-bottom" data-tip="Give a star on GitHub">
<span class="flex items-center justify-between space-x-0.5 font-medium"> <span class="flex items-center justify-between space-x-0.5 font-medium">
<%= svg_icon('start', class: 'h-3 w-3') %> <%= svg_icon('start', class: 'h-3 w-3') %>
<span>16k</span> <span>17k</span>
</span> </span>
</a> </a>

@ -1,10 +1,10 @@
<a href="<%= Wabosign::GITHUB_URL %>" class="btn btn-neutral btn-sm btn-outline inline-flex items-center justify-center" target="_blank" alt="Star on GitHub" style="height: 37px"> <a href="https://github.com/wabolabs/wabosign" class="btn btn-neutral btn-sm btn-outline inline-flex items-center justify-center" target="_blank" alt="Star on GitHub" style="height: 37px">
<span> <span>
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"> <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
<path fill="currentColor" d="M12 0c-6.626 0-12 5.373-12 12 0 5.302 3.438 9.8 8.207 11.387.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23.957-.266 1.983-.399 3.003-.404 1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576 4.765-1.589 8.199-6.086 8.199-11.386 0-6.627-5.373-12-12-12z" /> <path fill="currentColor" d="M12 0c-6.626 0-12 5.373-12 12 0 5.302 3.438 9.8 8.207 11.387.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23.957-.266 1.983-.399 3.003-.404 1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576 4.765-1.589 8.199-6.086 8.199-11.386 0-6.627-5.373-12-12-12z" />
</svg> </svg>
</span> </span>
<span class="flex"> <span class="flex">
<span class="hidden lg:block">View on&nbsp</span>GitHub <span class="hidden lg:block">Star on&nbsp</span>GitHub
</span> </span>
</a> </a>

@ -1,4 +1,4 @@
<svg class="<%= local_assigns[:class] %>" height="<%= local_assigns.fetch(:height, '37') %>" width="<%= local_assigns.fetch(:width, '37') %>" style="color: #e97a42" viewBox="0 0 180 180" xmlns="http://www.w3.org/2000/svg"> <svg class="<%= local_assigns[:class] %>" height="<%= local_assigns.fetch(:height, '37') %>" width="<%= local_assigns.fetch(:width, '37') %>" style="color: #e97a42" viewBox="0 0 180 180" xmlns="http://www.w3.org/2000/svg">
<rect x="6" y="6" width="168" height="168" rx="34" ry="34" fill="currentColor" /> <path fill="currentColor" d="M 178.224 72.09 c -0.296 -1.463 -0.627 -2.919 -0.996 -4.364 -0.293 -1.151 -0.616 -2.293 -0.956 -3.433 -0.301 -1.008 -0.612 -2.014 -0.95 -3.012 -0.531 -1.578 -1.113 -3.142 -1.735 -4.694 -0.216 -0.54 -0.433 -1.082 -0.661 -1.618 -0.195 -0.462 -0.399 -0.917 -0.601 -1.375 -0.262 -0.591 -0.53 -1.177 -0.804 -1.762 -0.074 -0.159 -0.151 -0.315 -0.226 -0.474 -0.209 -0.441 -0.422 -0.881 -0.638 -1.318 -0.076 -0.154 -0.153 -0.306 -0.229 -0.459 -0.236 -0.471 -0.477 -0.939 -0.721 -1.406 -0.053 -0.101 -0.105 -0.201 -0.158 -0.302 -1.143 -2.16 -2.367 -4.269 -3.68 -6.322 -0.116 -0.181 -0.237 -0.359 -0.355 -0.539 -0.094 -0.144 -0.189 -0.288 -0.284 -0.432 -0.284 -0.431 -0.57 -0.861 -0.862 -1.287 -0.112 -0.164 -0.225 -0.326 -0.338 -0.489 -0.193 -0.279 -0.382 -0.56 -0.579 -0.836 -0.089 -0.125 -0.182 -0.249 -0.273 -0.374 -0.13 -0.182 -0.264 -0.362 -0.395 -0.542 -0.277 -0.38 -0.556 -0.76 -0.838 -1.135 -0.15 -0.199 -0.303 -0.395 -0.454 -0.593 -0.21 -0.274 -0.417 -0.552 -0.63 -0.823 -0.055 -0.069 -0.111 -0.136 -0.166 -0.205 -0.482 -0.61 -0.971 -1.216 -1.47 -1.814 -0.129 -0.155 -0.262 -0.306 -0.392 -0.461 -0.402 -0.476 -0.808 -0.95 -1.22 -1.417 -0.186 -0.212 -0.375 -0.422 -0.563 -0.631 -0.384 -0.428 -0.773 -0.854 -1.167 -1.276 -0.176 -0.189 -0.351 -0.379 -0.529 -0.567 -0.564 -0.595 -1.134 -1.186 -1.716 -1.768 -1.091 -1.091 -2.207 -2.15 -3.346 -3.178 -1.016 -0.919 -2.05 -1.815 -3.103 -2.684 -0.772 -0.636 -1.557 -1.255 -2.348 -1.864 -3.465 -2.67 -7.112 -5.075 -10.927 -7.209 -2.869 -1.604 -5.83 -3.06 -8.883 -4.351 -2.443 -1.033 -4.922 -1.948 -7.428 -2.756 -8.879 -2.863 -18.13 -4.318 -27.605 -4.318 -3.19 0 -6.354 0.169 -9.488 0.496 -4.036 0.421 -8.019 1.114 -11.94 2.073 -1.732 0.423 -3.452 0.892 -5.157 1.42 -2.856 0.883 -5.673 1.912 -8.447 3.085 -2.645 1.118 -5.222 2.357 -7.729 3.711 -2.574 1.39 -5.073 2.901 -7.494 4.533 -1.195 0.805 -2.37 1.64 -3.527 2.503 -1.156 0.864 -2.292 1.756 -3.408 2.676 -0.553 0.456 -1.1 0.919 -1.643 1.389 -1.649 1.427 -3.252 2.92 -4.806 4.473 -2.582 2.582 -4.991 5.299 -7.222 8.138 -0.892 1.135 -1.756 2.292 -2.59 3.467 -0.417 0.588 -0.827 1.18 -1.23 1.778 -0.403 0.597 -0.798 1.199 -1.186 1.806 -0.388 0.607 -0.769 1.218 -1.143 1.835 -2.241 3.697 -4.216 7.562 -5.916 11.582 -1.095 2.589 -2.059 5.217 -2.901 7.877 -0.153 0.482 -0.3 0.965 -0.444 1.449 -0.339 1.14 -0.663 2.282 -0.956 3.433 -0.369 1.446 -0.7 2.901 -0.996 4.364 -1.034 5.121 -1.618 10.343 -1.749 15.637 -0.018 0.757 -0.028 1.514 -0.028 2.274 0 1.123 0.02 2.244 0.062 3.361 0.285 7.82 1.568 15.475 3.825 22.879 0.044 0.147 0.088 0.295 0.133 0.441 0.877 2.823 1.894 5.608 3.054 8.35 0.85 2.009 1.769 3.98 2.755 5.912 0.539 1.057 1.105 2.099 1.685 3.132 4.013 7.142 8.98 13.698 14.846 19.564 7.713 7.713 16.611 13.878 26.477 18.352 0.705 0.32 1.415 0.632 2.131 0.935 2.081 0.88 4.185 1.679 6.313 2.396 9.217 3.106 18.85 4.677 28.719 4.677 8.031 0 15.902 -1.047 23.522 -3.107 0.633 -0.172 1.266 -0.35 1.895 -0.535 0.757 -0.222 1.509 -0.456 2.26 -0.698 0.717 -0.232 1.431 -0.474 2.145 -0.723 1.752 -0.616 3.49 -1.281 5.211 -2.009 0.755 -0.319 1.503 -0.651 2.247 -0.989 1.237 -0.563 2.459 -1.15 3.664 -1.766 0.644 -0.328 1.283 -0.665 1.917 -1.009 1.654 -0.896 3.274 -1.848 4.865 -2.844 5.736 -3.591 11.06 -7.827 15.912 -12.679 0.775 -0.775 1.534 -1.562 2.278 -2.36 5.204 -5.59 9.636 -11.754 13.246 -18.417 0.343 -0.634 0.68 -1.274 1.009 -1.917 0.482 -0.944 0.943 -1.9 1.392 -2.863 0.471 -1.007 0.928 -2.021 1.364 -3.049 1.22 -2.886 2.281 -5.82 3.187 -8.793 0.559 -1.833 1.056 -3.68 1.494 -5.542 0.108 -0.458 0.211 -0.916 0.312 -1.376 0.194 -0.883 0.373 -1.77 0.539 -2.659 1.02 -5.455 1.542 -11.02 1.542 -16.663 0 -6.074 -0.595 -12.058 -1.776 -17.911 z m -161.733 19.614 c -1.118 -56.662 44.604 -74.877 60.998 -67.647 2.187 0.965 4.732 2.431 7.042 2.96 5.295 1.213 13.432 -3.113 13.521 6.273 0.078 8.156 -3.389 13.108 -10.797 16.177 -7.539 3.124 -14.777 9.181 -19.95 15.493 -21.487 26.216 -31.231 68.556 -7.565 94.296 -13.679 -5.545 -42.418 -25.467 -43.248 -67.552 z m 91.109 72.619 c -0.053 0.008 -4.171 0.775 -4.171 0.775 0 0 -15.862 -22.957 -23.509 -21.719 11.291 16.04 12.649 22.625 12.649 22.625 -0.053 0.001 -0.107 0.001 -0.161 0.003 -51.831 2.131 -42.785 -64.026 -28.246 -86.502 -1.555 13.073 8.878 39.992 39.034 44.1 9.495 1.293 32.302 -3.275 41.015 -11.38 0.098 1.825 0.163 3.85 0.159 6.013 -0.046 23.538 -13.47 42.743 -36.77 46.085 z m 30.575 -15.708 c 9.647 -9.263 12.869 -27.779 9.103 -44.137 -4.608 -20.011 -28.861 -32.383 -40.744 -35.564 5.766 -8.089 27.908 -14.274 39.567 5.363 -5.172 -10.519 -13.556 -23.023 -1.732 -33.128 12.411 13.329 19.411 29.94 20.161 48.7 0.75 18.753 -6.64 41.768 -26.355 58.765 z" />
<path fill="#faf7f5" d="M 40 56 L 60 134 L 78 78 L 90 132 L 102 78 L 120 134 L 140 56" stroke="#faf7f5" stroke-width="11" stroke-linejoin="round" stroke-linecap="round" fill="none" /> <circle fill="currentColor" cx="71.927" cy="32.004" r="2.829" />
</svg> </svg>

Before

Width:  |  Height:  |  Size: 500 B

After

Width:  |  Height:  |  Size: 4.8 KiB

@ -1,15 +1,14 @@
<% if Wabosign.demo? || (request.path != '/' && !devise_controller?) %> <% if Wabosign.demo? || (request.path != '/' && !devise_controller?) %>
<meta name="robots" content="noindex"> <meta name="robots" content="noindex">
<% end %> <% end %>
<% brand = Wabosign.branded_product_name(signed_in? ? current_account : nil) %> <% title = content_for(:html_title) || (signed_in? ? 'WaboSign' : 'WaboSign | Open Source Document Signing') %>
<% title = content_for(:html_title) || (signed_in? ? brand : "#{brand} | Open Source Document Signing") %>
<% description = content_for(:html_description) || 'Open source, self-hosted tool to streamline document filling and signing. Create custom PDF forms to complete and sign with an easy to use online tool.' %> <% description = content_for(:html_description) || 'Open source, self-hosted tool to streamline document filling and signing. Create custom PDF forms to complete and sign with an easy to use online tool.' %>
<meta name="description" content="<%= description %>"> <meta name="description" content="<%= description %>">
<meta property="og:title" content="<%= title %>"> <meta property="og:title" content="<%= title %>">
<meta property="og:description" content="<%= description %>"> <meta property="og:description" content="<%= description %>">
<meta property="og:type" content="website"> <meta property="og:type" content="website">
<meta property="og:url" content="<%= root_url %>"> <meta property="og:url" content="<%= root_url %>">
<meta property="og:site_name" content="<%= brand %>"> <meta property="og:site_name" content="WaboSign">
<% if content_for(:disable_image_preview) %> <% if content_for(:disable_image_preview) %>
<meta property="og:image" content=""> <meta property="og:image" content="">
<meta name="twitter:image" content=""> <meta name="twitter:image" content="">
@ -20,6 +19,8 @@
<meta name="twitter:image" content="<%= content_for(:preview_image_url).presence || "#{root_url}preview.png" %>"> <meta name="twitter:image" content="<%= content_for(:preview_image_url).presence || "#{root_url}preview.png" %>">
<% end %> <% end %>
<meta name="twitter:card" content="summary"> <meta name="twitter:card" content="summary">
<meta name="twitter:creator" content="@docusealco">
<meta name="twitter:site" content="@docusealco">
<meta name="twitter:title" content="<%= title %>"> <meta name="twitter:title" content="<%= title %>">
<meta name="twitter:description" content="<%= description %>"> <meta name="twitter:description" content="<%= description %>">
<link rel="apple-touch-icon" sizes="180x180" href="/apple-icon-180x180.png"> <link rel="apple-touch-icon" sizes="180x180" href="/apple-icon-180x180.png">

@ -11,7 +11,7 @@
<% if signed_in? %> <% if signed_in? %>
<div class="space-x-4 flex items-center"> <div class="space-x-4 flex items-center">
<% if Wabosign.demo? %> <% if Wabosign.demo? %>
<a href="<%= Wabosign::PRODUCT_URL %>" class="btn btn-neutral btn-sm btn-outline inline-flex items-center justify-center" style="height: 37px"> <a href="https://wabosign.com/sign_up" class="btn btn-neutral btn-sm btn-outline inline-flex items-center justify-center" style="height: 37px">
<%= t('sign_up') %> <%= t('sign_up') %>
</a> </a>
<span class="hidden sm:inline"> <span class="hidden sm:inline">
@ -34,6 +34,14 @@
<span class="mr-1"><%= t('profile') %></span> <span class="mr-1"><%= t('profile') %></span>
<% end %> <% end %>
</li> </li>
<% if !Wabosign.demo? && can?(:manage, EncryptedConfig) %>
<li>
<%= link_to Wabosign.multitenant? ? console_redirect_index_path : Wabosign::CONSOLE_URL, data: { prefetch: false }, class: 'flex items-center' do %>
<%= svg_icon('terminal', class: 'w-5 h-5 flex-shrink-0 stroke-2') %>
<%= t('console') %>
<% end %>
</li>
<% end %>
<% if can?(:read, EncryptedConfig.new(key: EncryptedConfig::ESIGN_CERTS_KEY, account: current_account)) %> <% if can?(:read, EncryptedConfig.new(key: EncryptedConfig::ESIGN_CERTS_KEY, account: current_account)) %>
<li> <li>
<%= link_to settings_esign_path, class: 'flex items-center' do %> <%= link_to settings_esign_path, class: 'flex items-center' do %>
@ -42,6 +50,14 @@
<% end %> <% end %>
</li> </li>
<% end %> <% end %>
<% if Wabosign.multitenant? || current_user.role == 'superadmin' %>
<li>
<%= link_to Wabosign::CHATGPT_URL, target: 'blank', class: 'flex items-center' do %>
<%= svg_icon('sparkles', class: 'w-5 h-5 flex-shrink-0 stroke-2') %>
<span class="mr-1 whitespace-nowrap"><%= t('ask_ai') %></span>
<% end %>
</li>
<% end %>
<% if (can?(:manage, EncryptedConfig) && current_user == true_user) || (current_user != true_user && current_account.testing?) %> <% if (can?(:manage, EncryptedConfig) && current_user == true_user) || (current_user != true_user && current_account.testing?) %>
<%= form_for '', url: testing_account_path, method: current_account.testing? ? :delete : :post, html: { class: 'w-full py-1' } do |f| %> <%= form_for '', url: testing_account_path, method: current_account.testing? ? :delete : :post, html: { class: 'w-full py-1' } do |f| %>
<label class="flex items-center pl-6 pr-4 py-2 border-y border-base-300 -ml-2 -mr-2" for="testing_toggle"> <label class="flex items-center pl-6 pr-4 py-2 border-y border-base-300 -ml-2 -mr-2" for="testing_toggle">

@ -1,3 +1,8 @@
<% if signed_in? && current_user != true_user %> <% if signed_in? && current_user != true_user %>
<%= render 'shared/test_alert' %> <%= render 'shared/test_alert' %>
<% elsif request.path.starts_with?('/settings') %>
<%= link_to "#{Wabosign::CLOUD_URL}/sign_up?#{{ redir: "#{Wabosign::CONSOLE_URL}/on_premises" }.to_query}", class: 'hidden md:inline-flex btn btn-warning btn-sm', data: { prefetch: false } do %>
<%= t('upgrade') %>
<% end %>
<span class="hidden md:inline-flex h-3 border-r border-base-content"></span>
<% end %> <% end %>

@ -9,11 +9,5 @@
<% else %> <% else %>
<%= t('powered_by') %> <%= t('powered_by') %>
<% end %> <% end %>
<a href="<%= Wabosign::PRODUCT_URL %><%= local_assigns[:link_path] %>" class="underline"><%= Wabosign.branded_product_name(local_assigns[:account]) %></a> - <%= t('open_source_documents_software') %> <a href="<%= Docuseal::PRODUCT_URL %><%= local_assigns[:link_path] %>" class="underline"><%= Docuseal.product_name %></a> - <%= t('open_source_documents_software') %>
</div>
<%# AGPL §7(b) DocuSeal attribution. Do not remove or rebrand. %>
<div class="text-center px-2 text-xs opacity-70 mt-1">
<%= t('based_on') %>
<a href="<%= Wabosign::UPSTREAM_URL %>" class="underline" target="_blank" rel="noopener"><%= Wabosign::UPSTREAM_NAME %></a>
(AGPLv3)
</div> </div>

@ -64,13 +64,33 @@
</li> </li>
<% end %> <% end %>
<% end %> <% end %>
<% if !Wabosign.demo? && can?(:manage, EncryptedConfig) && (current_user != true_user || !current_account.linked_account_account) %>
<li>
<%= content_for(:pro_link) || link_to(Wabosign.multitenant? ? console_redirect_index_path(redir: "#{Wabosign::CONSOLE_URL}/plans") : "#{Wabosign::CLOUD_URL}/sign_up?#{{ redir: "#{Wabosign::CONSOLE_URL}/on_premises" }.to_query}", class: 'text-base hover:bg-base-300', data: { turbo: false }) do %>
<%= t('plans') %>
<span class="badge badge-warning"><%= t('pro') %></span>
<% end %>
</li>
<% end %>
<% if !Wabosign.demo? && can?(:manage, EncryptedConfig) && (current_user == true_user || current_account.testing?) %> <% if !Wabosign.demo? && can?(:manage, EncryptedConfig) && (current_user == true_user || current_account.testing?) %>
<% if can?(:read, EncryptedConfig.new(key: 'saml_configs', account: current_account)) && true_user == current_user %> <li>
<%= link_to Wabosign.multitenant? ? console_redirect_index_path(redir: "#{Wabosign::CONSOLE_URL}#{'/test' if current_account.testing?}/api") : "#{Wabosign::CONSOLE_URL}/on_premises", class: 'text-base hover:bg-base-300', data: { turbo: false } do %>
<% if Wabosign.multitenant? %> API <% else %> <%= t('console') %> <% end %>
<% end %>
</li>
<% if Wabosign.multitenant? %>
<li>
<%= link_to console_redirect_index_path(redir: "#{Wabosign::CONSOLE_URL}#{'/test' if current_account.testing?}/embedding/form"), class: 'text-base hover:bg-base-300', data: { turbo: false } do %>
<%= t('embedding') %>
<% end %>
</li>
<% end %>
<% if (!Wabosign.multitenant? || can?(:manage, :saml_sso)) && can?(:read, EncryptedConfig.new(key: 'saml_configs', account: current_account)) && true_user == current_user %>
<li> <li>
<%= link_to 'SSO', settings_sso_index_path, class: 'text-base hover:bg-base-300' %> <%= link_to 'SSO', settings_sso_index_path, class: 'text-base hover:bg-base-300' %>
</li> </li>
<% end %> <% end %>
<% if can?(:read, McpToken) && can?(:manage, :mcp) %> <% if !Wabosign.multitenant? && can?(:read, McpToken) && can?(:manage, :mcp) %>
<li> <li>
<%= link_to 'MCP', settings_mcp_index_path, class: 'text-base hover:bg-base-300' %> <%= link_to 'MCP', settings_mcp_index_path, class: 'text-base hover:bg-base-300' %>
</li> </li>
@ -104,12 +124,24 @@
<%= svg_icon('brand_github', class: 'w-8 h-8') %> <%= svg_icon('brand_github', class: 'w-8 h-8') %>
</a> </a>
</div> </div>
<div class="tooltip" data-tip="<%= t('discord_community') %>">
<a href="<%= Wabosign::DISCORD_URL %>" target="_blank" class="btn btn-circle btn-primary btn-md">
<%= svg_icon('brand_discord', class: 'w-8 h-8') %>
</a>
</div>
<%= capture do %>
<div class="tooltip" data-tip="<%= t('ai_assistant') %>">
<a href="<%= Wabosign::CHATGPT_URL %>" target="_blank" class="btn btn-circle btn-primary btn-md">
<%= svg_icon('brand_openai', class: 'w-8 h-8') %>
</a>
</div>
<% end %>
</div> </div>
<a href="mailto:<%= Wabosign::SUPPORT_EMAIL %>" target="_blank" class="w-full block mt-4 underline text-center"> <a href="mailto:<%= Wabosign::SUPPORT_EMAIL %>" target="_blank" class="w-full block mt-4 underline text-center">
<%= Wabosign::SUPPORT_EMAIL %> <%= Wabosign::SUPPORT_EMAIL %>
</a> </a>
<% if Wabosign.version.present? && !Wabosign.multitenant? && can?(:manage, EncryptedConfig) %> <% if Wabosign.version.present? && !Wabosign.multitenant? && can?(:manage, EncryptedConfig) %>
<a href="<%= Wabosign::GITHUB_URL %>/releases" target="_blank" class="badge badge-outline text-xs block mx-auto mt-4"> <a href="https://github.com/wabolabs/wabosign/releases" target="_blank" class="badge badge-outline text-xs block mx-auto mt-4">
v<%= Wabosign.version %> v<%= Wabosign.version %>
</a> </a>
<% end %> <% end %>

@ -1,2 +1,2 @@
<%= render 'shared/account_logo', account: current_account %> <%= render 'shared/logo' %>
<span><%= Wabosign.branded_product_name(current_account) %></span> <span>WaboSign</span>

@ -0,0 +1,15 @@
<div class="alert">
<%= svg_icon('info_circle', class: 'w-6 h-6') %>
<div>
<p class="font-bold">
<%= t('send_signature_requests_via_sms') %>
</p>
<p class="text-gray-700">
<%= t('unlock_with_docuseal_pro') %>
<br>
<a class="link font-medium" target="_blank" href="<%= Wabosign.multitenant? ? console_redirect_index_path(redir: "#{Wabosign::CONSOLE_URL}/plans") : "#{Wabosign::CLOUD_URL}/sign_up?#{{ redir: "#{Wabosign::CONSOLE_URL}/on_premises" }.to_query}" %>" data-turbo="false">
<%= t('learn_more') %>
</a>
</p>
</div>
</div>

@ -2,190 +2,7 @@
<%= render 'shared/settings_nav' %> <%= render 'shared/settings_nav' %>
<div class="flex-grow max-w-xl mx-auto"> <div class="flex-grow max-w-xl mx-auto">
<h1 class="text-4xl font-bold mb-4">SMS</h1> <h1 class="text-4xl font-bold mb-4">SMS</h1>
<%= render 'placeholder' %>
<% value = @encrypted_config.value || {} %>
<% sms_live = Sms.enabled_for?(current_account) %>
<% provider_labels = { 'bulkvs' => 'BulkVS', 'twilio' => 'Twilio', 'voipms' => 'VoIP.ms', 'signalwire' => 'SignalWire' } %>
<% sending_number_keys = { 'twilio' => 'twilio_from', 'voipms' => 'voipms_did', 'signalwire' => 'signalwire_from' } %>
<% sending_number = value[sending_number_keys[value['provider'].to_s] || 'from_number'] %>
<% selected_provider = value['provider'].presence || 'bulkvs' %>
<% if sms_live %>
<div class="alert alert-success mb-4">
<%= svg_icon('discount_check_filled', class: 'w-6 h-6') %>
<div>
<p class="font-bold">SMS is enabled</p>
<p class="text-gray-700">
Provider: <code><%= provider_labels[value['provider'].to_s] || value['provider'].to_s.upcase %></code>.
From: <code><%= sending_number %></code>.
</p>
</div>
</div>
<% else %>
<div class="alert mb-4">
<%= svg_icon('info_circle', class: 'w-6 h-6') %>
<div>
<p class="font-bold">SMS provider is not configured</p>
<p class="text-gray-700">
WaboSign supports
<a href="https://www.bulkvs.com/" target="_blank" rel="noopener" class="link">BulkVS</a>,
<a href="https://www.twilio.com/" target="_blank" rel="noopener" class="link">Twilio</a>,
<a href="https://voip.ms/" target="_blank" rel="noopener" class="link">VoIP.ms</a>, and
<a href="https://signalwire.com/" target="_blank" rel="noopener" class="link">SignalWire</a>.
Pick a provider below and paste its credentials.
</p>
</div>
</div>
<% end %>
<%= form_for @encrypted_config, url: settings_sms_path, method: :post, html: { autocomplete: 'off', class: 'space-y-4', id: 'sms_settings_form' } do |f| %>
<%= f.fields_for :value do |ff| %>
<div class="form-control">
<label class="label cursor-pointer" for="encrypted_config_value_enabled">
<span class="label-text font-medium">Enable SMS</span>
<%= ff.check_box :enabled, { class: 'toggle', checked: value['enabled'] == true }, '1', '0' %>
</label>
</div>
<div class="form-control">
<%= ff.label :provider, 'Provider', class: 'label' %>
<%= ff.select :provider,
Sms::SUPPORTED_PROVIDERS.map { |p| [provider_labels[p] || p, p] },
{ selected: selected_provider },
class: 'base-select',
id: 'sms_provider_select' %>
</div>
<div data-provider-block="bulkvs" class="space-y-4<%= ' hidden' unless selected_provider == 'bulkvs' %>">
<div class="form-control">
<%= ff.label :basic_auth_token, 'BulkVS Basic Auth Token', class: 'label' %>
<%= ff.password_field :basic_auth_token, value: '', class: 'base-input', placeholder: value['basic_auth_token'].present? ? '*************' : 'Paste from BulkVS portal' %>
<% if value['basic_auth_token'].present? %>
<span class="label-text-alt mt-1 opacity-70">Leave blank to keep the saved token.</span>
<% else %>
<span class="label-text-alt mt-1 opacity-70">In the BulkVS portal, open the API tab and copy the pre-encoded Basic Auth header value (do not include "Basic ").</span>
<% end %>
</div>
<div class="form-control">
<%= ff.label :from_number, 'From Number', class: 'label' %>
<%= ff.text_field :from_number, value: value['from_number'], class: 'base-input', placeholder: '15551234567' %>
<span class="label-text-alt mt-1 opacity-70">E.164 format (digits only, country code first; e.g. <code>15551234567</code>).</span>
</div>
<div class="form-control">
<%= ff.label :delivery_webhook_url, 'Delivery Status Webhook (optional)', class: 'label' %>
<%= ff.url_field :delivery_webhook_url, value: value['delivery_webhook_url'], class: 'base-input', placeholder: 'https://your-app.example/webhooks/sms' %>
<span class="label-text-alt mt-1 opacity-70">If set, BulkVS will POST delivery-status events here for each message.</span>
</div>
</div>
<div data-provider-block="twilio" class="space-y-4<%= ' hidden' unless selected_provider == 'twilio' %>">
<div class="form-control">
<%= ff.label :twilio_account_sid, 'Twilio Account SID', class: 'label' %>
<%= ff.text_field :twilio_account_sid, value: value['twilio_account_sid'], class: 'base-input', placeholder: 'AC...' %>
<span class="label-text-alt mt-1 opacity-70">From your Twilio Console "Account Info" panel.</span>
</div>
<div class="form-control">
<%= ff.label :twilio_auth_token, 'Twilio Auth Token', class: 'label' %>
<%= ff.password_field :twilio_auth_token, value: '', class: 'base-input', placeholder: value['twilio_auth_token'].present? ? '*************' : 'Click "show" in the Console to reveal' %>
<% if value['twilio_auth_token'].present? %>
<span class="label-text-alt mt-1 opacity-70">Leave blank to keep the saved token.</span>
<% else %>
<span class="label-text-alt mt-1 opacity-70">Found next to the Account SID in the Twilio Console.</span>
<% end %>
</div>
<div class="form-control">
<%= ff.label :twilio_from, 'From Number', class: 'label' %>
<%= ff.text_field :twilio_from, value: value['twilio_from'], class: 'base-input', placeholder: '+15551234567' %>
<span class="label-text-alt mt-1 opacity-70">Twilio number purchased in <strong>Phone Numbers → Manage</strong>. Use full E.164 with leading <code>+</code>.</span>
</div>
</div>
<div data-provider-block="voipms" class="space-y-4<%= ' hidden' unless selected_provider == 'voipms' %>">
<div class="form-control">
<%= ff.label :voipms_api_username, 'API Username', class: 'label' %>
<%= ff.text_field :voipms_api_username, value: value['voipms_api_username'], class: 'base-input', placeholder: 'your-account@example.com' %>
<span class="label-text-alt mt-1 opacity-70">Your VoIP.ms portal login email.</span>
</div>
<div class="form-control">
<%= ff.label :voipms_api_password, 'API Password', class: 'label' %>
<%= ff.password_field :voipms_api_password, value: '', class: 'base-input', placeholder: value['voipms_api_password'].present? ? '*************' : 'Set this at voip.ms/m/api.php' %>
<% if value['voipms_api_password'].present? %>
<span class="label-text-alt mt-1 opacity-70">Leave blank to keep the saved password.</span>
<% else %>
<span class="label-text-alt mt-1 opacity-70">Set the dedicated <strong>API password</strong> at <a href="https://voip.ms/m/api.php" target="_blank" rel="noopener" class="link">voip.ms/m/api.php</a> — this is <em>not</em> your portal login password. On the same page, enable API access and whitelist this server's egress IP, or every call will fail with <code>ip_not_authorized</code>.</span>
<% end %>
</div>
<div class="form-control">
<%= ff.label :voipms_did, 'DID (Sending Number)', class: 'label' %>
<%= ff.text_field :voipms_did, value: value['voipms_did'], class: 'base-input', placeholder: '5551234567' %>
<span class="label-text-alt mt-1 opacity-70">An SMS-enabled DID from <strong>Manage DIDs</strong>. Digits only, no <code>+</code>. The DID must have the SMS feature enabled.</span>
</div>
</div>
<div data-provider-block="signalwire" class="space-y-4<%= ' hidden' unless selected_provider == 'signalwire' %>">
<div class="form-control">
<%= ff.label :signalwire_space_url, 'Space URL', class: 'label' %>
<%= ff.text_field :signalwire_space_url, value: value['signalwire_space_url'], class: 'base-input', placeholder: 'yourname.signalwire.com' %>
<span class="label-text-alt mt-1 opacity-70">From <strong>Dashboard → API</strong>. Omit <code>https://</code>.</span>
</div>
<div class="form-control">
<%= ff.label :signalwire_project_id, 'Project ID', class: 'label' %>
<%= ff.text_field :signalwire_project_id, value: value['signalwire_project_id'], class: 'base-input', placeholder: '00000000-0000-0000-0000-000000000000' %>
<span class="label-text-alt mt-1 opacity-70">The UUID labelled "Your Project ID" on the API tab.</span>
</div>
<div class="form-control">
<%= ff.label :signalwire_api_token, 'API Token', class: 'label' %>
<%= ff.password_field :signalwire_api_token, value: '', class: 'base-input', placeholder: value['signalwire_api_token'].present? ? '*************' : 'PT...' %>
<% if value['signalwire_api_token'].present? %>
<span class="label-text-alt mt-1 opacity-70">Leave blank to keep the saved token.</span>
<% else %>
<span class="label-text-alt mt-1 opacity-70">Generate on the API tab. The token must have the <strong>Messaging</strong> scope enabled or sends return 401.</span>
<% end %>
</div>
<div class="form-control">
<%= ff.label :signalwire_from, 'From Number', class: 'label' %>
<%= ff.text_field :signalwire_from, value: value['signalwire_from'], class: 'base-input', placeholder: '+15551234567' %>
<span class="label-text-alt mt-1 opacity-70">A SignalWire number from <strong>Phone Numbers</strong>. Full E.164 with leading <code>+</code>.</span>
</div>
</div>
<% end %>
<div class="form-control pt-2">
<%= f.button button_title(title: t('save'), disabled_with: t('saving')), class: 'base-button' %>
</div>
<% end %>
<% if sms_live %>
<div class="card bg-base-200 mt-8">
<div class="card-body p-6 space-y-3">
<p class="text-xl font-semibold">Send a test SMS</p>
<%= form_with url: test_message_settings_sms_path, method: :post, html: { autocomplete: 'off', class: 'space-y-3' } do |f| %>
<div class="form-control">
<label for="test_phone" class="label">Phone number</label>
<input type="tel" name="phone" id="test_phone" class="base-input" placeholder="15551234567" required pattern="^\+?[0-9\s\-]+$" autocomplete="off">
<span class="label-text-alt mt-1 opacity-70">A short test message is sent to this number using your saved config.</span>
</div>
<div class="form-control">
<button type="submit" class="base-button">Send test</button>
</div>
<% end %>
</div>
</div>
<% end %>
</div> </div>
<div class="w-0 md:w-52"></div> <div class="w-0 md:w-52"></div>
</div> </div>
<script nonce="<%= content_security_policy_nonce %>">
(function () {
const select = document.getElementById('sms_provider_select')
if (!select) return
const blocks = document.querySelectorAll('[data-provider-block]')
const sync = () => {
const current = select.value
blocks.forEach((block) => {
block.classList.toggle('hidden', block.dataset.providerBlock !== current)
})
}
select.addEventListener('change', sync)
sync()
})()
</script>

@ -0,0 +1,15 @@
<div class="alert">
<%= svg_icon('info_circle', class: 'w-6 h-6') %>
<div>
<p class="font-bold">
<%= t('single_sign_on_with_saml_2_0') %>
</p>
<p class="text-gray-700">
<%= t('unlock_with_docuseal_pro') %>
<br>
<a class="link font-medium" target="_blank" href="<%= Wabosign.multitenant? ? console_redirect_index_path(redir: "#{Wabosign::CONSOLE_URL}/plans") : "#{Wabosign::CLOUD_URL}/sign_up?#{{ redir: "#{Wabosign::CONSOLE_URL}/on_premises" }.to_query}" %>" data-turbo="false">
<%= t('learn_more') %>
</a>
</p>
</div>
</div>

@ -1,81 +1,8 @@
<div class="flex flex-wrap space-y-4 md:flex-nowrap md:space-y-0"> <div class="flex flex-wrap space-y-4 md:flex-nowrap md:space-y-0">
<%= render 'shared/settings_nav' %> <%= render 'shared/settings_nav' %>
<div class="flex-grow max-w-xl mx-auto"> <div class="flex-grow max-w-xl mx-auto">
<h1 class="text-4xl font-bold mb-4">Google SSO</h1> <h1 class="text-4xl font-bold mb-4">SAML SSO</h1>
<%= render 'placeholder' %>
<% creds = Wabosign.google_sso_credentials %>
<% value = @encrypted_config.value || {} %>
<% if creds[:source] == :env %>
<div class="alert mb-4">
<%= svg_icon('info_circle', class: 'w-6 h-6') %>
<div>
<p class="font-bold">Google SSO is configured via environment variables</p>
<p class="text-gray-700">
<code>GOOGLE_CLIENT_ID</code> and <code>GOOGLE_CLIENT_SECRET</code> are set on the running process, so ENV-driven configuration is in effect. ENV always takes precedence over anything saved on this page. Unset the env vars (and restart) to switch to the values configured here.
</p>
</div>
</div>
<% elsif creds[:source] == :db %>
<div class="alert alert-success mb-4">
<%= svg_icon('discount_check_filled', class: 'w-6 h-6') %>
<div>
<p class="font-bold">Google SSO is enabled</p>
<p class="text-gray-700">
<% if creds[:allowed_domains].any? %>
Allowed Workspace domain<%= 's' if creds[:allowed_domains].size > 1 %>: <code><%= creds[:allowed_domains].join(', ') %></code>.
<% else %>
<strong>Warning:</strong> no domain allowlist is set. Any Google account can sign in.
<% end %>
</p>
</div>
</div>
<% else %>
<div class="alert mb-4">
<%= svg_icon('info_circle', class: 'w-6 h-6') %>
<div>
<p class="font-bold">Google SSO is not configured</p>
<p class="text-gray-700">
Fill in your Google Cloud OAuth client details below. The OAuth redirect URI to register in <a href="https://console.cloud.google.com/apis/credentials" target="_blank" rel="noopener" class="link">Google Cloud Console</a> is
<code><%= begin
"#{root_url}auth/google_oauth2/callback"
rescue StandardError
'/auth/google_oauth2/callback'
end %></code>.
</p>
</div>
</div>
<% end %>
<%= form_for @encrypted_config, url: settings_sso_index_path, method: :post, html: { autocomplete: 'off', class: 'space-y-4' } do |f| %>
<%= f.fields_for :value do |ff| %>
<div class="form-control">
<label class="label cursor-pointer" for="encrypted_config_value_enabled">
<span class="label-text font-medium">Enable Google SSO</span>
<%= ff.check_box :enabled, { class: 'toggle', checked: value['enabled'] == true }, '1', '0' %>
</label>
</div>
<div class="form-control">
<%= ff.label :client_id, 'Client ID', class: 'label' %>
<%= ff.text_field :client_id, value: value['client_id'], class: 'base-input', placeholder: '1234567890.apps.googleusercontent.com' %>
</div>
<div class="form-control">
<%= ff.label :client_secret, 'Client Secret', class: 'label' %>
<%= ff.password_field :client_secret, class: 'base-input', placeholder: value['client_secret'].present? ? '*************' : 'GOCSPX-…' %>
<% if value['client_secret'].present? %>
<span class="label-text-alt mt-1 opacity-70">Leave blank to keep the saved secret.</span>
<% end %>
</div>
<div class="form-control">
<%= ff.label :allowed_domains_csv, 'Allowed Workspace Domains', class: 'label' %>
<%= ff.text_field :allowed_domains_csv, value: Array(value['allowed_domains']).join(', '), class: 'base-input', placeholder: 'wabo.cc, partner.example' %>
<span class="label-text-alt mt-1 opacity-70">Comma-separated. Only Google accounts whose Workspace <code>hd</code> claim matches one of these domains can sign in. Leave blank to allow any Google account (not recommended).</span>
</div>
<% end %>
<div class="form-control pt-2">
<%= f.button button_title(title: t('save'), disabled_with: t('saving')), class: 'base-button' %>
</div>
<% end %>
</div> </div>
<div class="w-0 md:w-52"></div> <div class="w-0 md:w-52"></div>
</div> </div>

@ -1 +1 @@
<%= render 'start_form/brand_logo' %> <%= render 'start_form/wabosign_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">WaboSign</h1>
</a>

@ -1,4 +1,4 @@
<% content_for(:html_title, "#{@template.name} | #{Wabosign.branded_product_name(@template.account)}") %> <% content_for(:html_title, "#{@template.name} | WaboSign") %>
<% I18n.with_locale(@template.account.locale) do %> <% I18n.with_locale(@template.account.locale) do %>
<% content_for(:html_description, t('account_name_has_invited_you_to_fill_and_sign_documents_online_effortlessly_with_a_secure_fast_and_user_friendly_digital_document_signing_solution', account_name: @template.account.name)) %> <% content_for(:html_description, t('account_name_has_invited_you_to_fill_and_sign_documents_online_effortlessly_with_a_secure_fast_and_user_friendly_digital_document_signing_solution', account_name: @template.account.name)) %>
<% end %> <% end %>

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

Loading…
Cancel
Save