Merge pull request #1 from Intebec/claudetest

Claudetest
pull/627/head
ChapsJust 3 months ago committed by GitHub
commit 363561c582
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -0,0 +1 @@
HOST=hostname

1
.gitattributes vendored

@ -1 +1,2 @@
*.html linguist-detectable=false
bin/* text eol=lf

5
.gitignore vendored

@ -21,6 +21,7 @@
/public/assets
/config/master.key
/config/config.yml
/public/packs
/public/packs-test
@ -38,3 +39,7 @@ yarn-debug.log*
/ee
dump.rdb
*.onnx
pg_data/
caddy/
docuseal/
.env

@ -40,7 +40,7 @@ COPY ./tailwind.application.config.js ./tailwind.application.config.js
COPY ./app/javascript ./app/javascript
COPY ./app/views ./app/views
RUN echo "gem 'shakapacker'" > Gemfile && ./bin/shakapacker
RUN echo "gem 'shakapacker'" > Gemfile && sed -i 's/\r$//' bin/shakapacker && ./bin/shakapacker
FROM ruby:4.0.1-alpine AS app

@ -6,9 +6,12 @@ class ApplicationController < ActionController::Base
include ActiveStorage::SetCurrent
include Pagy::Method
helper WhitelabelHelper
check_authorization unless: :devise_controller?
around_action :with_locale
before_action :enforce_licence
before_action :sign_in_for_demo, if: -> { Docuseal.demo? }
before_action :maybe_redirect_to_setup, unless: :signed_in?
before_action :authenticate_user!, unless: :devise_controller?
@ -37,7 +40,7 @@ class ApplicationController < ActionController::Base
rescue_from CanCan::AccessDenied do |e|
Rollbar.warning(e) if defined?(Rollbar)
redirect_to root_path, alert: e.message
redirect_to root_path, alert: I18n.t('unauthorized.default', locale: current_account&.locale)
end
end
@ -65,12 +68,13 @@ class ApplicationController < ActionController::Base
private
def with_locale(&)
return yield unless current_account
locale = params[:lang].presence if Rails.env.development?
locale ||= current_account.locale
locale = if current_account
(params[:lang].presence if Rails.env.development?) || current_account.locale
else
request.env['HTTP_ACCEPT_LANGUAGE'].to_s[BROWSER_LOCALE_REGEXP].to_s.split('-').first.presence
end
I18n.with_locale(locale, &)
I18n.with_locale(locale || I18n.default_locale, &)
end
def with_browser_locale(&)
@ -95,6 +99,21 @@ class ApplicationController < ActionController::Base
sign_in(User.active.order('random()').take) unless signed_in?
end
def enforce_licence
return if request.path == '/up'
return if request.path.start_with?('/assets', '/packs')
Whitelabel.ensure_valid!
rescue Whitelabel::ConfigError => e
Rails.logger.error(e.message)
if request.format.json?
render json: { error: 'service_unavailable' }, status: :service_unavailable
else
render plain: 'Service unavailable.', status: :service_unavailable
end
end
def current_account
current_user&.account
end
@ -122,6 +141,8 @@ class ApplicationController < ActionController::Base
end
def maybe_redirect_com
# NOTE: upstream DocuSeal cloud redirect — no-op for self-hosted / white-label
return unless Docuseal.multitenant?
return if request.domain != 'docuseal.co'
redirect_to request.url.gsub('.co/', '.com/'), allow_other_host: true, status: :moved_permanently

@ -1,39 +1,43 @@
# frozen_string_literal: true
class EmbedScriptsController < ActionController::Metal
DUMMY_SCRIPT = <<~JAVASCRIPT.freeze
const DummyBuilder = class extends HTMLElement {
connectedCallback() {
this.innerHTML = `
<div style="text-align: center; padding: 20px; font-family: Arial, sans-serif;">
<h2>Upgrade to Pro</h2>
<p>Unlock embedded components by upgrading to Pro</p>
<div style="margin-top: 40px;">
<a href="#{Docuseal::CONSOLE_URL}/on_premises" target="_blank" style="padding: 15px 25px; background-color: #222; color: white; text-decoration: none; border-radius: 5px; font-size: 16px; cursor: pointer;">
Learn More
</a>
</div>
</div>
`;
}
};
def show
headers['Content-Type'] = 'application/javascript'
const DummyForm = class extends DummyBuilder {};
self.response_body = dummy_script
if (!window.customElements.get('docuseal-builder')) {
window.customElements.define('docuseal-builder', DummyBuilder);
}
self.status = 200
end
if (!window.customElements.get('docuseal-form')) {
window.customElements.define('docuseal-form', DummyForm);
}
JAVASCRIPT
private
def show
headers['Content-Type'] = 'application/javascript'
def dummy_script
<<~JAVASCRIPT
const DummyBuilder = class extends HTMLElement {
connectedCallback() {
this.innerHTML = `
<div style="text-align: center; padding: 20px; font-family: Arial, sans-serif;">
<h2>Upgrade to Pro</h2>
<p>Unlock embedded components by upgrading to Pro</p>
<div style="margin-top: 40px;">
<a href="#{Docuseal::CONSOLE_URL}/on_premises" target="_blank" style="padding: 15px 25px; background-color: #222; color: white; text-decoration: none; border-radius: 5px; font-size: 16px; cursor: pointer;">
Learn More
</a>
</div>
</div>
`;
}
};
self.response_body = DUMMY_SCRIPT
const DummyForm = class extends DummyBuilder {};
self.status = 200
if (!window.customElements.get('docuseal-builder')) {
window.customElements.define('docuseal-builder', DummyBuilder);
}
if (!window.customElements.get('docuseal-form')) {
window.customElements.define('docuseal-form', DummyForm);
}
JAVASCRIPT
end
end

@ -2,7 +2,7 @@
class ErrorsController < ActionController::Base
ENTERPRISE_FEATURE_MESSAGE =
'This feature is available in Pro Edition: https://www.docuseal.com/pricing'
"This feature is available in Pro Edition: #{Whitelabel.website_url}/pricing"
ENTERPRISE_PATHS = [
'/submissions/html',

@ -1,7 +1,7 @@
# frozen_string_literal: true
class EsignSettingsController < ApplicationController
DEFAULT_CERT_NAME = 'DocuSeal Self-Host Autogenerated'
DEFAULT_CERT_NAME = Whitelabel.cert_name
CertFormRecord = Struct.new(:name, :file, :password, keyword_init: true) do
include ActiveModel::Validations

@ -1,16 +1,6 @@
# frozen_string_literal: true
class PersonalizationSettingsController < ApplicationController
ALLOWED_KEYS = [
AccountConfig::FORM_COMPLETED_BUTTON_KEY,
AccountConfig::SUBMITTER_INVITATION_EMAIL_KEY,
AccountConfig::SUBMITTER_INVITATION_REMINDER_EMAIL_KEY,
AccountConfig::SUBMITTER_DOCUMENTS_COPY_EMAIL_KEY,
AccountConfig::SUBMITTER_COMPLETED_EMAIL_KEY,
AccountConfig::FORM_COMPLETED_MESSAGE_KEY,
*(Docuseal.multitenant? ? [] : [AccountConfig::POLICY_LINKS_KEY])
].freeze
InvalidKey = Class.new(StandardError)
before_action :load_and_authorize_account_config, only: :create
@ -45,11 +35,23 @@ class PersonalizationSettingsController < ApplicationController
authorize!(:create, @account_config)
raise InvalidKey unless ALLOWED_KEYS.include?(@account_config.key)
raise InvalidKey unless allowed_keys.include?(@account_config.key)
@account_config
end
def allowed_keys
[
AccountConfig::FORM_COMPLETED_BUTTON_KEY,
AccountConfig::SUBMITTER_INVITATION_EMAIL_KEY,
AccountConfig::SUBMITTER_INVITATION_REMINDER_EMAIL_KEY,
AccountConfig::SUBMITTER_DOCUMENTS_COPY_EMAIL_KEY,
AccountConfig::SUBMITTER_COMPLETED_EMAIL_KEY,
AccountConfig::FORM_COMPLETED_MESSAGE_KEY,
*(Docuseal.multitenant? ? [] : [AccountConfig::POLICY_LINKS_KEY])
]
end
def account_config_params
attrs = params.require(:account_config).permit(:key, :value, { value: {} }, { value: [] })

@ -16,6 +16,9 @@ class UsersController < ApplicationController
@users.active.where.not(role: 'integration')
end
# Restrict visibility to roles at or below the current user's rank.
@users = @users.where(role: Whitelabel.manageable_roles(current_user.role))
@pagy, @users = pagy(@users.preload(account: :account_accesses).where(account: current_account).order(id: :desc))
end
@ -40,12 +43,18 @@ class UsersController < ApplicationController
end
@user.password = SecureRandom.hex if @user.password.blank?
@user.role = User::ADMIN_ROLE unless role_valid?(@user.role)
@user.role = User.admin_role unless role_valid?(@user.role)
if @user.save
UserMailer.invitation_email(@user).deliver_later!
begin
UserMailer.invitation_email(@user).deliver_later!
redirect_back fallback_location: settings_users_path, notice: I18n.t('user_has_been_invited')
rescue StandardError => e
Rollbar.error(e) if defined?(Rollbar)
redirect_back fallback_location: settings_users_path, notice: I18n.t('user_has_been_invited')
redirect_back fallback_location: settings_users_path, alert: I18n.t('failed')
end
else
render turbo_stream: turbo_stream.replace(:modal, template: 'users/new'), status: :unprocessable_content
end
@ -92,7 +101,8 @@ class UsersController < ApplicationController
private
def role_valid?(role)
User::ROLES.include?(role)
# Role must exist AND be at or below the current user's own rank.
Whitelabel.manageable_roles(current_user.role).include?(role.to_s)
end
def build_user

@ -0,0 +1,18 @@
# frozen_string_literal: true
# =============================================================================
# WhitelabelHelper — makes Whitelabel config available in all views
# =============================================================================
# Include this in ApplicationController to use `wl` in all ERB templates.
#
# Usage in views:
# <%= wl.brand_name %>
# <%= wl.logo_path %>
# <%= wl.support_email %>
# =============================================================================
module WhitelabelHelper
def wl
Whitelabel
end
end

@ -182,6 +182,8 @@ safeRegisterElement('template-builder', class extends HTMLElement {
showTourStartForm: this.dataset.showTourStartForm === 'true'
})
this.app.config.globalProperties.document = document
this.component = this.app.mount(this.appElem)
this.appendChild(this.appElem)

@ -1,36 +1,18 @@
<template>
<div
id="form_completed"
class="mx-auto max-w-md flex flex-col completed-form"
dir="auto"
>
<div id="form_completed" class="mx-auto max-w-md flex flex-col completed-form" dir="auto">
<div class="font-medium text-2xl flex items-center space-x-1.5 mx-auto">
<IconCircleCheck
class="inline text-green-600"
:width="30"
:height="30"
/>
<IconCircleCheck class="inline text-green-600" :width="30" :height="30" />
<span class="completed-form-message-title">
{{ completedMessage.title || (hasSignatureFields ? (hasMultipleDocuments ? t('documents_have_been_signed') : t('document_has_been_signed')) : t('form_has_been_completed')) }}
{{ completedMessage.title || (hasSignatureFields ? (hasMultipleDocuments ? t("documents_have_been_signed") : t("document_has_been_signed")) : t("form_has_been_completed")) }}
</span>
</div>
<div
v-if="completedMessage.body"
class="mt-2 completed-form-message-body"
>
<MarkdownContent
:string="completedMessage.body"
/>
<div v-if="completedMessage.body" class="mt-2 completed-form-message-body">
<MarkdownContent :string="completedMessage.body" />
</div>
<div class="space-y-3 mt-5">
<a
v-if="completedButton.url"
:href="sanitizeUrl(completedButton.url)"
rel="noopener noreferrer nofollow"
class="white-button flex items-center w-full completed-form-completed-button"
>
<a v-if="completedButton.url" :href="sanitizeUrl(completedButton.url)" rel="noopener noreferrer nofollow" class="white-button flex items-center w-full completed-form-completed-button">
<span>
{{ completedButton.title || 'Back to Website' }}
{{ completedButton.title || "Back to Website" }}
</span>
</a>
<button
@ -39,73 +21,44 @@
:disabled="isSendingCopy"
@click.prevent="sendCopyToEmail"
>
<IconInnerShadowTop
v-if="isSendingCopy"
class="animate-spin"
/>
<IconInnerShadowTop v-if="isSendingCopy" class="animate-spin" />
<IconMail v-else />
<span>
{{ t('send_copy_via_email') }}
{{ t("send_copy_via_email") }}
</span>
</button>
<button
v-if="!isWebView && withDownloadButton"
class="base-button flex items-center space-x-1 w-full completed-form-download-button"
:disabled="isDownloading"
@click.prevent="download"
>
<IconInnerShadowTop
v-if="isDownloading"
class="animate-spin"
/>
<button v-if="!isWebView && withDownloadButton" class="base-button flex items-center space-x-1 w-full completed-form-download-button" :disabled="isDownloading" @click.prevent="download">
<IconInnerShadowTop v-if="isDownloading" class="animate-spin" />
<IconDownload v-else />
<span>
{{ t('download') }}
{{ t("download") }}
</span>
</button>
<a
v-if="isDemo"
target="_blank"
href="https://github.com/docusealco/docuseal"
class="white-button flex items-center space-x-1 w-full"
>
<a v-if="isDemo" target="_blank" :href="brandGithubUrl" class="white-button flex items-center space-x-1 w-full">
<IconBrandGithub />
<span>
Star on Github
</span>
<span> Star on Github </span>
</a>
<a
v-if="isDemo"
href="https://docuseal.com/sign_up"
class="white-button flex items-center space-x-1 w-full"
>
<a v-if="isDemo" :href="brandWebsiteUrl" class="white-button flex items-center space-x-1 w-full">
<IconLogin />
<span>
{{ t('create_a_free_account') }}
{{ t("create_a_free_account") }}
</span>
</a>
</div>
<div
v-if="attribution"
class="text-center mt-4"
>
{{ t('powered_by') }}
<a
href="https://www.docuseal.com/start"
target="_blank"
class="underline"
>DocuSeal</a> - {{ t('open_source_documents_software') }}
<div v-if="attribution" class="text-center mt-4">
{{ t("powered_by") }}
<a :href="brandWebsiteUrl" target="_blank" class="underline">{{ brandName }}</a> - {{ t("open_source_documents_software") }}
</div>
</div>
</template>
<script>
import { IconCircleCheck, IconBrandGithub, IconMail, IconDownload, IconInnerShadowTop, IconLogin } from '@tabler/icons-vue'
import MarkdownContent from './markdown_content'
import { sanitizeUrl } from '@braintree/sanitize-url'
import { IconCircleCheck, IconBrandGithub, IconMail, IconDownload, IconInnerShadowTop, IconLogin } from "@tabler/icons-vue";
import MarkdownContent from "./markdown_content";
import { sanitizeUrl } from "@braintree/sanitize-url";
export default {
name: 'FormCompleted',
name: "FormCompleted",
components: {
MarkdownContent,
IconCircleCheck,
@ -113,181 +66,193 @@ export default {
IconBrandGithub,
IconMail,
IconLogin,
IconDownload
IconDownload,
},
inject: ['baseUrl', 't'],
inject: ["baseUrl", "t"],
props: {
submitterSlug: {
type: String,
required: true
required: true,
},
isDemo: {
type: Boolean,
required: false,
default: false
default: false,
},
attribution: {
type: Boolean,
required: false,
default: true
default: true,
},
hasSignatureFields: {
type: Boolean,
required: false,
default: false
default: false,
},
hasMultipleDocuments: {
type: Boolean,
required: false,
default: false
default: false,
},
withDownloadButton: {
type: Boolean,
required: false,
default: true
default: true,
},
withSendCopyButton: {
type: Boolean,
required: false,
default: true
default: true,
},
withConfetti: {
type: Boolean,
required: false,
default: false
default: false,
},
canSendEmail: {
type: Boolean,
required: false,
default: false
default: false,
},
fetchOptions: {
type: Object,
required: false,
default: () => ({})
default: () => ({}),
},
completedButton: {
type: Object,
required: false,
default: () => ({})
default: () => ({}),
},
completedMessage: {
type: Object,
required: false,
default: () => ({})
}
default: () => ({}),
},
},
data () {
data() {
return {
isSendingCopy: false,
isDownloading: false
}
isDownloading: false,
};
},
computed: {
isWebView () {
return /webview|wv|ip((?!.*Safari)|(?=.*like Safari))/i.test(window.navigator.userAgent)
}
brandName() {
return document.querySelector('meta[name="brand-name"]')?.content || "Intébec";
},
brandWebsiteUrl() {
return document.querySelector('meta[name="brand-website-url"]')?.content || "/";
},
brandGithubUrl() {
return document.querySelector('meta[name="brand-github-url"]')?.content || "";
},
isWebView() {
return /webview|wv|ip((?!.*Safari)|(?=.*like Safari))/i.test(window.navigator.userAgent);
},
},
async mounted () {
async mounted() {
if (this.withConfetti) {
const { default: confetti } = await import('canvas-confetti')
const { default: confetti } = await import("canvas-confetti");
confetti({
particleCount: 50,
startVelocity: 30,
spread: 140
})
spread: 140,
});
}
document.querySelectorAll('#decline_button').forEach((button) => {
button.setAttribute('disabled', 'true')
})
document.querySelectorAll("#decline_button").forEach((button) => {
button.setAttribute("disabled", "true");
});
},
methods: {
sanitizeUrl,
sendCopyToEmail () {
this.isSendingCopy = true
sendCopyToEmail() {
this.isSendingCopy = true;
fetch(this.baseUrl + `/send_submission_email.json?submitter_slug=${this.submitterSlug}`, {
method: 'POST'
}).then(() => {
alert(this.t('email_has_been_sent'))
}).finally(() => {
this.isSendingCopy = false
method: "POST",
})
.then(() => {
alert(this.t("email_has_been_sent"));
})
.finally(() => {
this.isSendingCopy = false;
});
},
download () {
this.isDownloading = true
download() {
this.isDownloading = true;
fetch(this.baseUrl + `/submitters/${this.submitterSlug}/download`, {
method: 'GET',
...this.fetchOptions
method: "GET",
...this.fetchOptions,
}).then(async (response) => {
if (response.ok) {
const urls = await response.json()
const isMobileSafariIos = 'ontouchstart' in window && navigator.maxTouchPoints > 0 && /AppleWebKit/i.test(navigator.userAgent)
const isSafariIos = isMobileSafariIos || /iPhone|iPad|iPod/i.test(navigator.userAgent)
const urls = await response.json();
const isMobileSafariIos = "ontouchstart" in window && navigator.maxTouchPoints > 0 && /AppleWebKit/i.test(navigator.userAgent);
const isSafariIos = isMobileSafariIos || /iPhone|iPad|iPod/i.test(navigator.userAgent);
if (isSafariIos && urls.length > 1) {
this.downloadSafariIos(urls)
this.downloadSafariIos(urls);
} else {
this.downloadUrls(urls)
this.downloadUrls(urls);
}
} else {
alert(this.t('failed_to_download_files'))
alert(this.t("failed_to_download_files"));
}
})
});
},
downloadUrls (urls) {
downloadUrls(urls) {
const fileRequests = urls.map((url) => {
return () => {
return fetch(url).then(async (resp) => {
const blobUrl = URL.createObjectURL(await resp.blob())
const link = document.createElement('a')
const blobUrl = URL.createObjectURL(await resp.blob());
const link = document.createElement("a");
link.href = blobUrl
link.setAttribute('download', decodeURI(url.split('/').pop()))
link.href = blobUrl;
link.setAttribute("download", decodeURI(url.split("/").pop()));
link.click()
link.click();
URL.revokeObjectURL(blobUrl)
})
}
})
URL.revokeObjectURL(blobUrl);
});
};
});
fileRequests.reduce(
(prevPromise, request) => prevPromise.then(() => request()),
Promise.resolve()
).finally(() => {
this.isDownloading = false
})
fileRequests
.reduce((prevPromise, request) => prevPromise.then(() => request()), Promise.resolve())
.finally(() => {
this.isDownloading = false;
});
},
downloadSafariIos (urls) {
downloadSafariIos(urls) {
const fileRequests = urls.map((url) => {
return fetch(url).then(async (resp) => {
const blob = await resp.blob()
const blobUrl = URL.createObjectURL(blob.slice(0, blob.size, 'application/octet-stream'))
const link = document.createElement('a')
const blob = await resp.blob();
const blobUrl = URL.createObjectURL(blob.slice(0, blob.size, "application/octet-stream"));
const link = document.createElement("a");
link.href = blobUrl
link.setAttribute('download', decodeURI(url.split('/').pop()))
link.href = blobUrl;
link.setAttribute("download", decodeURI(url.split("/").pop()));
return link
})
})
return link;
});
});
Promise.all(fileRequests).then((links) => {
links.forEach((link, index) => {
setTimeout(() => {
link.click()
Promise.all(fileRequests)
.then((links) => {
links.forEach((link, index) => {
setTimeout(() => {
link.click();
URL.revokeObjectURL(link.href)
}, index * 50)
URL.revokeObjectURL(link.href);
}, index * 50);
});
})
}).finally(() => {
this.isDownloading = false
})
}
}
}
.finally(() => {
this.isDownloading = false;
});
},
},
};
</script>

@ -288,7 +288,7 @@
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
href="https://www.docuseal.com/esign-disclosure"
:href="(document.querySelector('meta[name=brand-website-url]')?.content || '') + '/esign-disclosure'"
target="_blank"
>
<span class="inline md:hidden">

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

@ -23,7 +23,7 @@
class="bg-base-300 rounded-xl py-2 px-3 text-center"
>
<a
href="https://www.docuseal.com/pricing"
:href="(document.querySelector('meta[name=brand-website-url]')?.content || '') + '/pricing'"
target="_blank"
class="link"
>{{ t('available_in_pro') }}</a>

File diff suppressed because it is too large Load Diff

@ -23,7 +23,7 @@
class="bg-base-300 rounded-xl py-2 px-3 text-center"
>
<a
href="https://www.docuseal.com/pricing"
:href="(document.querySelector('meta[name=brand-website-url]')?.content || '') + '/pricing'"
target="_blank"
class="link"
>{{ t('available_in_pro') }}</a>

@ -1,20 +1,13 @@
<template>
<svg
height="40"
width="40"
style="color: #e0753f"
viewBox="0 0 180 180"
width="37"
viewBox="0 0 819 897"
xmlns="http://www.w3.org/2000/svg"
>
<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"
/>
<circle
fill="currentColor"
cx="71.927"
cy="32.004"
r="2.829"
fill="rgb(77,138,225)"
d="M 441.00 718.00 L 441.00 831.00 L 62.07 831.00 L 62.29 772.25 L 62.50 713.50 L 85.25 713.23 L 108.00 712.96 L 108.00 186.00 L 63.26 186.00 L 62.64 182.25 C61.82,177.29 61.82,78.61 62.64,72.64 L 63.27 68.00 L 226.00 68.00 L 226.25 392.75 L 226.50 717.50 L 248.93 717.81 C261.27,717.99 272.40,717.86 273.68,717.54 L 276.00 716.96 L 276.00 68.00 L 441.00 68.00 L 441.00 180.74 L 436.84 181.41 C434.55,181.78 424.53,182.04 414.59,182.00 C404.64,181.95 395.94,181.94 395.25,181.96 C394.26,181.99 394.00,202.90 394.00,283.07 L 394.00 384.15 L 450.25 383.73 C511.08,383.28 511.26,383.26 528.95,377.36 C562.24,366.27 587.29,341.03 595.99,309.83 C602.17,287.70 600.18,258.96 591.08,238.67 C577.56,208.56 547.01,189.15 504.20,183.49 C497.98,182.67 490.90,182.00 488.45,182.00 L 484.00 182.00 L 484.00 68.00 L 497.85 68.00 C529.54,68.00 565.68,72.85 590.87,80.49 C661.51,101.91 703.26,148.71 714.44,219.00 C720.04,254.19 717.89,290.64 708.58,318.48 C700.93,341.33 688.25,361.76 671.00,379.00 C658.27,391.72 650.19,397.88 634.74,406.64 C628.27,410.31 623.09,413.42 623.24,413.57 C623.38,413.71 627.10,415.25 631.50,417.00 C663.91,429.86 696.53,454.01 716.42,479.89 C744.77,516.76 757.99,557.53 757.99,608.00 C757.99,635.30 753.16,668.04 746.04,689.00 C740.09,706.53 735.88,715.82 727.50,729.91 C701.08,774.35 654.97,805.82 594.20,820.90 C567.64,827.49 547.18,829.92 511.75,830.68 L 484.00 831.29 L 484.00 718.27 L 500.75 717.58 C509.96,717.20 521.55,716.42 526.50,715.86 C592.96,708.22 632.14,672.78 637.21,615.70 C640.68,576.72 626.22,544.52 596.05,524.03 C577.26,511.27 550.10,502.25 520.93,499.07 C513.92,498.31 428.28,497.57 395.25,497.98 C394.26,498.00 394.00,520.68 394.00,608.00 L 394.00 718.00 Z"
/>
</svg>
</template>

@ -197,7 +197,7 @@
<a
v-if="!isConnected"
class="block link text-center mt-1"
href="https://www.docuseal.com/blog/accept-payments-and-request-signatures-with-ease"
:href="(document.querySelector('meta[name=brand-website-url]')?.content || '') + '/blog/accept-payments-and-request-signatures-with-ease'"
target="_blank"
data-turbo="false"
>{{ t('learn_more') }}</a>

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

@ -1,7 +1,7 @@
# frozen_string_literal: true
class ApplicationMailer < ActionMailer::Base
default from: 'DocuSeal <info@docuseal.com>'
default from: -> { Whitelabel.email_from }
layout 'mailer'
register_interceptor ActionMailerConfigsInterceptor

@ -48,9 +48,22 @@
#
class User < ApplicationRecord
ROLES = [
ADMIN_ROLE = 'admin'
ADMIN_ROLE = 'admin',
GESTIONNAIRE_ROLE = 'gestionnaire',
USER_ROLE = 'user'
].freeze
# Config-driven role list. Falls back to ROLES constant if no config.
def self.available_roles
Whitelabel.roles
rescue StandardError
ROLES
end
def self.admin_role
available_roles.first
end
EMAIL_REGEXP = /[^@;,<>\s]+@[^@;,<>\s]+/
FULL_EMAIL_REGEXP =
@ -70,12 +83,12 @@ class User < ApplicationRecord
devise :two_factor_authenticatable, :recoverable, :rememberable, :validatable, :trackable, :lockable
attribute :role, :string, default: ADMIN_ROLE
attribute :role, :string, default: -> { User.admin_role }
attribute :uuid, :string, default: -> { SecureRandom.uuid }
scope :active, -> { where(archived_at: nil) }
scope :archived, -> { where.not(archived_at: nil) }
scope :admins, -> { where(role: ADMIN_ROLE) }
scope :admins, -> { where(role: admin_role) }
validates :email, format: { with: /\A[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}\z/ }
@ -94,7 +107,7 @@ class User < ApplicationRecord
def sidekiq?
return true if Rails.env.development?
role == 'admin'
role == User.admin_role
end
def self.sign_in_after_reset_password

@ -118,7 +118,7 @@
</div>
</div>
<div class="text-center">
<%= link_to t('open_full_api_reference'), "#{Docuseal::PRODUCT_URL}/docs/api", class: 'btn btn-warning text-base mt-4 px-8', target: '_blank', rel: 'noopener' %>
<%= link_to t('open_full_api_reference'), "#{Whitelabel.website_url}/docs/api", class: 'btn btn-warning text-base mt-4 px-8', target: '_blank', rel: 'noopener' %>
</div>
</div>
</div>

@ -1,3 +1,4 @@
<% return unless Whitelabel.show_pro_upsells? %>
<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') %>

@ -1,4 +1,6 @@
<title>
<%= content_for(:html_title) || (signed_in? ? 'DocuSeal' : 'DocuSeal | Open Source Document Signing') %>
<%= content_for(:html_title) || (signed_in? ? wl.page_title(signed_in: true) : wl.page_title(signed_in: false)) %>
</title>
<%= render 'shared/meta' %>
<link rel="stylesheet" href="/intebec.css">
<style id="whitelabel-theme-vars"><%= wl.inline_css_variables %></style>

@ -2,6 +2,9 @@
<html data-theme="docuseal" lang="<%= I18n.locale %>">
<head>
<%= render 'layouts/head_tags' %>
<meta name="brand-name" content="<%= Whitelabel.brand_name %>">
<meta name="brand-website-url" content="<%= Whitelabel.website_url %>">
<meta name="brand-github-url" content="<%= Whitelabel.github_url %>">
<% if Docuseal.enable_pwa? %>
<link rel="manifest" href="/manifest.json">
<% end %>
@ -18,7 +21,7 @@
<% end %>
<%= stylesheet_pack_tag 'application', media: 'all' %>
</head>
<body>
<body class="min-h-screen">
<% if params[:modal].present? %>
<% url_params = Rails.application.routes.recognize_path(params[:modal], method: :get) %>
<% if url_params[:action] == 'new' %>

@ -2,6 +2,9 @@
<html data-theme="docuseal" lang="<%= I18n.locale %>">
<head>
<%= render 'layouts/head_tags' %>
<meta name="brand-name" content="<%= Whitelabel.brand_name %>">
<meta name="brand-website-url" content="<%= Whitelabel.website_url %>">
<meta name="brand-github-url" content="<%= Whitelabel.github_url %>">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<%= csrf_meta_tags %>
<% if ENV['ROLLBAR_CLIENT_TOKEN'] %>

@ -1,3 +1,4 @@
<% return unless Whitelabel.show_pro_upsells? %>
<div class="alert my-4">
<%= svg_icon('info_circle', class: 'w-6 h-6') %>
<div>

@ -1,79 +1,2 @@
<section class="flex items-center justify-center">
<div class="py-10 mx-auto max-w-7xl">
<div class="max-w-xl mx-auto">
<div class="text-center">
<div class="relative flex flex-col items-center">
<%= render 'shared/logo', width: '100', height: '100' %>
<h1 class="text-6xl font-bold mt-4 mb-4">
DocuSeal
</h1>
<% if Docuseal.version.present? %>
<a href="https://github.com/docusealco/docuseal/releases" target="_blank" class="badge badge-outline badge-lg block mx-auto">
v<%= Docuseal.version %>
</a>
<% end %>
</div>
<h2 class="mt-4 mb-16 text-lg text-center text-gray-600">
A self-hosted and open-source web platform that provides secure and efficient digital document signing and processing.
</h2>
</div>
</div>
<div class="grid grid-cols-1 gap-12 md:gap-10 md:grid-cols-2">
<div class="card bg-base-200">
<div class="card-body">
<div class="text-center transition-all">
<div class="inline-block p-4 mb-4 -mt-16 bg-base-content rounded-full">
<%= svg_icon('brand_docker', class: 'w-10 h-10 text-base-100 stroke-1') %>
</div>
<h3 class="mb-4 text-2xl font-semibold">Easy to Start</h3>
<p class="text-base text-gray-500">
Run on your own host using Docker container, or deploy on your favorite managed PaaS with a single <a href="https://www.docuseal.com/install" class="link link-neutral font-bold">click</a>.
</p>
</div>
</div>
</div>
<div class="card bg-base-200">
<div class="card-body">
<div class="text-center transition-all">
<div class="inline-block p-4 mb-4 -mt-16 bg-base-content rounded-full">
<%= svg_icon('devices', class: 'w-10 h-10 text-base-100') %>
</div>
<h3 class="mb-4 text-2xl font-semibold">Mobile Optimized</h3>
<p class="text-base text-gray-500">
Review and sign digital documents online from any device.
Docuseal document forms are optimized for screens of all sizes.
</p>
</div>
</div>
</div>
<div class="card bg-base-200">
<div class="card-body">
<div class="text-center transition-all">
<div class="inline-block p-4 mb-4 -mt-16 bg-base-content rounded-full">
<%= svg_icon('shield_check', class: 'w-10 h-10 text-base-100') %>
</div>
<h3 class="mb-4 text-2xl font-semibold">Secure</h3>
<p class="text-base text-gray-500">
Host it on your hardware under a VPN to ensure that important documents can be accesses only within your organization.
</p>
</div>
</div>
</div>
<div class="card bg-base-200">
<div class="card-body">
<div class="text-center transition-all">
<div class="inline-block p-4 mb-4 -mt-16 bg-base-content rounded-full">
<%= svg_icon('brand_github', class: 'w-10 h-10 text-base-100') %>
</div>
<h3 class="mb-4 text-2xl font-semibold">Open Source</h3>
<p class="text-base text-gray-500">
Source code is available under <a href="<%= Docuseal::GITHUB_URL %>" class="link link-neutral font-bold" target="_blank">github.com/docusealco</a>.<br>
Open-source contributors are always ready to help!
</p>
</div>
</div>
</div>
</div>
</div>
</section>
<%= render 'shared/attribution', with_counter: true %>
<% response.headers['Location'] = new_user_session_path %>
<% response.status = 302 %>

@ -1,3 +1,4 @@
<% return unless Whitelabel.show_pro_upsells? %>
<div class="alert my-4">
<%= svg_icon('info_circle', class: 'w-6 h-6') %>
<div>

@ -1,26 +1,28 @@
<div class="flex flex-wrap space-y-4 md:flex-nowrap md:space-y-0">
<%= render 'shared/settings_nav' %>
<div class="flex-grow max-w-xl mx-auto">
<p class="text-4xl font-bold mb-4">
<h1 id="email-templates-heading" class="text-4xl font-bold mb-4">
<%= t('email_templates') %>
</p>
<div class="space-y-4">
</h1>
<section aria-labelledby="email-templates-heading" class="space-y-4">
<%= render 'signature_request_email_form' %>
<%= render 'documents_copy_email_form' %>
<%= render 'submitter_completed_email_form' %>
</div>
<p class="text-4xl font-bold mb-4 mt-8">
</section>
<h2 id="company-logo-heading" class="text-4xl font-bold mb-4 mt-8">
<%= t('company_logo') %>
</p>
<%= render 'logo_form' %>
<p class="text-4xl font-bold mb-4 mt-8">
</h2>
<section aria-labelledby="company-logo-heading" class="space-y-4">
<%= render 'logo_form' %>
</section>
<h2 id="submission-form-heading" class="text-4xl font-bold mb-4 mt-8">
<%= t('submission_form') %>
</p>
<div class="space-y-4">
</h2>
<section aria-labelledby="submission-form-heading" class="space-y-4">
<%= render 'form_completed_message_form' %>
<%= render 'form_completed_button_form' %>
<%= render 'form_policy_links_form' %>
</div>
</section>
<%= render 'form_customization_settings' %>
</div>
<div class="w-0 md:w-52"></div>

@ -1,15 +1,15 @@
{
"name": "<%= Docuseal.product_name %>",
"short_name": "<%= Docuseal.product_name %>",
"name": "<%= Whitelabel.brand_name %>",
"short_name": "<%= Whitelabel.brand_short_name %>",
"id": "/",
"icons": [
{
"src": "/favicon.svg",
"src": "<%= Whitelabel.favicon_svg %>",
"type": "image/svg+xml",
"sizes": "any"
},
{
"src": "/apple-touch-icon.png",
"src": "<%= Whitelabel.apple_touch_icon %>",
"type": "image/png",
"sizes": "192x192"
}
@ -18,8 +18,8 @@
"display": "standalone",
"scope": "/",
"orientation": "any",
"description": "<%= Docuseal.product_name %> is an open source platform that provides secure and efficient digital document signing and processing.",
"description": "<%= Whitelabel.pwa_description %>",
"categories": ["productivity", "utilities"],
"theme_color": "#FAF7F4",
"background_color": "#FAF7F4"
"theme_color": "<%= Whitelabel.pwa_theme_color %>",
"background_color": "<%= Whitelabel.pwa_background_color %>"
}

@ -2,9 +2,8 @@
---
</p>
<p>
<% if @current_account&.testing? %>
<%= t('sent_using_product_name_in_testing_mode_html', product_url: "#{Docuseal::PRODUCT_EMAIL_URL}/start", product_name: Docuseal.product_name) %>
<% else %>
<%= t('sent_using_product_name_free_document_signing_html', product_url: "#{Docuseal::PRODUCT_EMAIL_URL}/start", product_name: Docuseal.product_name) %>
<% end %>
---
</p>
<p>
<%= Whitelabel.email_attribution_html.html_safe %>
</p>

@ -1,6 +0,0 @@
<a target="_blank" href="<%= Docuseal::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">
<%= svg_icon('start', class: 'h-3 w-3') %>
<span>11k</span>
</span>
</a>

@ -1,10 +1,12 @@
<a href="https://github.com/docusealco/docuseal" class="btn btn-neutral btn-sm btn-outline inline-flex items-center justify-center" target="_blank" alt="Star on GitHub" style="height: 37px">
<span>
<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" />
</svg>
</span>
<span class="flex">
<span class="hidden lg:block">Star on&nbsp</span>GitHub
</span>
</a>
<% if Whitelabel.show_github_button? && Whitelabel.github_url.present? %>
<a href="<%= Whitelabel.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">
<span>
<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" />
</svg>
</span>
<span class="flex">
<span class="hidden lg:block">Star on&nbsp</span>GitHub
</span>
</a>
<% end %>

@ -1,4 +1 @@
<svg class="<%= local_assigns[:class] %>" height="<%= local_assigns.fetch(:height, '37') %>" width="<%= local_assigns.fetch(:width, '37') %>" style="color: #e0753f" viewBox="0 0 180 180" xmlns="http://www.w3.org/2000/svg">
<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" />
<circle fill="currentColor" cx="71.927" cy="32.004" r="2.829" />
</svg>
<img src="<%= wl.logo_path %>" class="<%= local_assigns[:class] %>" style="max-width: 100%; max-height: 100%;" width="<%= local_assigns[:width] || wl.logo_width %>" height="<%= local_assigns[:height] || wl.logo_height %>" />

Before

Width:  |  Height:  |  Size: 4.8 KiB

After

Width:  |  Height:  |  Size: 225 B

@ -1,14 +1,14 @@
<% if Docuseal.demo? || (request.path != '/' && !devise_controller?) %>
<meta name="robots" content="noindex">
<% end %>
<% title = content_for(:html_title) || (signed_in? ? 'DocuSeal' : 'DocuSeal | 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.' %>
<% title = content_for(:html_title) || (signed_in? ? wl.page_title(signed_in: true) : wl.page_title(signed_in: false)) %>
<% description = content_for(:html_description) || wl.description %>
<meta name="description" content="<%= description %>">
<meta property="og:title" content="<%= title %>">
<meta property="og:description" content="<%= description %>">
<meta property="og:type" content="website">
<meta property="og:url" content="<%= root_url %>">
<meta property="og:site_name" content="DocuSeal">
<meta property="og:site_name" content="<%= wl.brand_name %>">
<% if content_for(:disable_image_preview) %>
<meta property="og:image" content="">
<meta name="twitter:image" content="">
@ -19,13 +19,15 @@
<meta name="twitter:image" content="<%= content_for(:preview_image_url).presence || "#{root_url}preview.png" %>">
<% end %>
<meta name="twitter:card" content="summary">
<meta name="twitter:creator" content="@docusealco">
<meta name="twitter:site" content="@docusealco">
<% if wl.twitter_handle.present? %>
<meta name="twitter:creator" content="<%= wl.twitter_handle %>">
<meta name="twitter:site" content="<%= wl.twitter_handle %>">
<% end %>
<meta name="twitter:title" content="<%= title %>">
<meta name="twitter:description" content="<%= description %>">
<link rel="apple-touch-icon" sizes="180x180" href="/apple-icon-180x180.png">
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png">
<link rel="icon" type="image/png" sizes="96x96" href="/favicon-96x96.png">
<link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png">
<link rel="icon" type="image/svg+xml" href="/favicon.svg">
<meta name="theme-color" content="#faf7f5">
<link rel="apple-touch-icon" sizes="180x180" href="<%= wl.apple_touch_icon %>">
<link rel="icon" type="image/png" sizes="32x32" href="<%= wl.favicon_32 %>">
<link rel="icon" type="image/png" sizes="96x96" href="<%= wl.favicon_96 %>">
<link rel="icon" type="image/png" sizes="16x16" href="<%= wl.favicon_16 %>">
<link rel="icon" type="image/svg+xml" href="<%= wl.favicon_ico %>">
<meta name="theme-color" content="<%= wl.pwa_theme_color %>">

@ -11,7 +11,7 @@
<% if signed_in? %>
<div class="space-x-4 flex items-center">
<% if Docuseal.demo? %>
<a href="https://docuseal.com/sign_up" class="btn btn-neutral btn-sm btn-outline inline-flex items-center justify-center" style="height: 37px">
<a href="<%= Whitelabel.website_url %>" class="btn btn-neutral btn-sm btn-outline inline-flex items-center justify-center" style="height: 37px">
<%= t('sign_up') %>
</a>
<span class="hidden sm:inline">
@ -50,7 +50,7 @@
<% end %>
</li>
<% end %>
<% if Docuseal.multitenant? || current_user.role == 'superadmin' %>
<% if Whitelabel.show_ai_link? && (Docuseal.multitenant? || current_user.role == 'superadmin') %>
<li>
<%= link_to Docuseal::CHATGPT_URL, target: 'blank', class: 'flex items-center' do %>
<%= svg_icon('sparkles', class: 'w-5 h-5 flex-shrink-0 stroke-2') %>

@ -1,8 +1,3 @@
<% if signed_in? && current_user != true_user %>
<%= render 'shared/test_alert' %>
<% elsif request.path.starts_with?('/settings') %>
<%= link_to "#{Docuseal::CLOUD_URL}/sign_up?#{{ redir: "#{Docuseal::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 %>

@ -1,13 +1,15 @@
<div class="text-center px-2">
<% if local_assigns[:with_counter] %>
<% count = CompletedSubmitter.distinct.count(:submission_id) %>
<% if count > 1 %>
<%= t('count_documents_signed_with_html', count:) %>
<% if Whitelabel.show_powered_by? %>
<div class="text-center px-2">
<% if local_assigns[:with_counter] %>
<% count = CompletedSubmitter.distinct.count(:submission_id) %>
<% if count > 1 %>
<%= t('count_documents_signed_with_html', count:) %>
<% else %>
<%= t('powered_by') %>
<% end %>
<% else %>
<%= t('powered_by') %>
<% end %>
<% else %>
<%= t('powered_by') %>
<% end %>
<a href="<%= Docuseal::PRODUCT_URL %><%= local_assigns[:link_path] %>" class="underline"><%= Docuseal.product_name %></a> - <%= t('open_source_documents_software') %>
</div>
<a href="<%= Whitelabel.website_url %><%= local_assigns[:link_path] %>" class="underline"><%= Whitelabel.powered_by_text %></a> - <%= t('open_source_documents_software') %>
</div>
<% end %>

@ -6,90 +6,63 @@
<span class="!bg-transparent"><%= t('settings') %></span>
</li>
<li></li>
<%# Profile is always visible — personal section %>
<li>
<%= link_to t('profile'), settings_profile_index_path, class: 'text-base hover:bg-base-300' %>
</li>
<li>
<%= link_to t('account'), settings_account_path, class: 'text-base hover:bg-base-300' %>
</li>
<% if Whitelabel.setting_section_visible?(current_user.role, 'account') %>
<li>
<%= link_to t('account'), settings_account_path, class: 'text-base hover:bg-base-300' %>
</li>
<% end %>
<% unless Docuseal.multitenant? %>
<% if can?(:read, EncryptedConfig.new(key: EncryptedConfig::EMAIL_SMTP_KEY, account: current_account)) && ENV['SMTP_ADDRESS'].blank? && true_user == current_user %>
<% if Whitelabel.setting_section_visible?(current_user.role, 'email') && can?(:read, EncryptedConfig.new(key: EncryptedConfig::EMAIL_SMTP_KEY, account: current_account)) && ENV['SMTP_ADDRESS'].blank? && true_user == current_user %>
<li>
<%= link_to t('email'), settings_email_index_path, class: 'text-base hover:bg-base-300' %>
</li>
<% end %>
<% if can?(:read, EncryptedConfig.new(key: EncryptedConfig::FILES_STORAGE_KEY, account: current_account)) && true_user == current_user %>
<% if Whitelabel.setting_section_visible?(current_user.role, 'storage') && can?(:read, EncryptedConfig.new(key: EncryptedConfig::FILES_STORAGE_KEY, account: current_account)) && true_user == current_user %>
<li>
<%= link_to t('storage'), settings_storage_index_path, class: 'text-base hover:bg-base-300' %>
</li>
<% end %>
<% if can?(:read, EncryptedConfig.new(key: 'submitter_invitation_sms', account: current_account)) && true_user == current_user %>
<li>
<%= link_to 'SMS', settings_sms_path, class: 'text-base hover:bg-base-300' %>
</li>
<% end %>
<% end %>
<% if can?(:read, AccountConfig) %>
<% if Whitelabel.setting_section_visible?(current_user.role, 'notifications') && can?(:read, AccountConfig) %>
<li>
<%= link_to t('notifications'), settings_notifications_path, class: 'text-base hover:bg-base-300' %>
</li>
<% end %>
<% if can?(:read, EncryptedConfig.new(key: EncryptedConfig::ESIGN_CERTS_KEY, account: current_account)) %>
<% if Whitelabel.setting_section_visible?(current_user.role, 'esign') && can?(:read, EncryptedConfig.new(key: EncryptedConfig::ESIGN_CERTS_KEY, account: current_account)) %>
<li>
<%= link_to t('e_signature'), settings_esign_path, class: 'text-base hover:bg-base-300' %>
</li>
<% end %>
<% if can?(:read, AccountConfig) %>
<% if Whitelabel.setting_section_visible?(current_user.role, 'personalization') && can?(:read, AccountConfig) %>
<li>
<%= link_to t('personalization'), settings_personalization_path, class: 'text-base hover:bg-base-300' %>
</li>
<% end %>
<% if can?(:read, User) %>
<% if Whitelabel.setting_section_visible?(current_user.role, 'users') && can?(:read, User) %>
<li>
<%= link_to t('users'), settings_users_path, class: 'text-base hover:bg-base-300' %>
</li>
<% end %>
<%= render 'shared/settings_nav_extra' %>
<% if Docuseal.demo? || !Docuseal.multitenant? %>
<% if can?(:read, AccessToken) %>
<% if Whitelabel.setting_section_visible?(current_user.role, 'api') && can?(:read, AccessToken) %>
<li>
<%= link_to 'API', settings_api_index_path, class: 'text-base hover:bg-base-300' %>
</li>
<% end %>
<% end %>
<% if Docuseal.demo? || !Docuseal.multitenant? || (current_user != true_user && !current_account.testing?) %>
<% if can?(:read, WebhookUrl) %>
<% if Whitelabel.setting_section_visible?(current_user.role, 'webhooks') && can?(:read, WebhookUrl) %>
<li>
<%= link_to 'Webhooks', settings_webhooks_path, class: 'text-base hover:bg-base-300' %>
</li>
<% end %>
<% end %>
<% if !Docuseal.demo? && can?(:manage, EncryptedConfig) && (current_user != true_user || !current_account.linked_account_account) %>
<li>
<%= content_for(:pro_link) || link_to(Docuseal.multitenant? ? console_redirect_index_path(redir: "#{Docuseal::CONSOLE_URL}/plans") : "#{Docuseal::CLOUD_URL}/sign_up?#{{ redir: "#{Docuseal::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 !Docuseal.demo? && can?(:manage, EncryptedConfig) && (current_user == true_user || current_account.testing?) %>
<li>
<%= link_to Docuseal.multitenant? ? console_redirect_index_path(redir: "#{Docuseal::CONSOLE_URL}#{'/test' if current_account.testing?}/api") : "#{Docuseal::CONSOLE_URL}/on_premises", class: 'text-base hover:bg-base-300', data: { turbo: false } do %>
<% if Docuseal.multitenant? %> API <% else %> <%= t('console') %> <% end %>
<% end %>
</li>
<% if Docuseal.multitenant? %>
<li>
<%= link_to console_redirect_index_path(redir: "#{Docuseal::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 (!Docuseal.multitenant? || can?(:manage, :saml_sso)) && can?(:read, EncryptedConfig.new(key: 'saml_configs', account: current_account)) && true_user == current_user %>
<li>
<%= link_to 'SSO', settings_sso_index_path, class: 'text-base hover:bg-base-300' %>
</li>
<% end %>
<%= render 'shared/settings_nav_extra2' %>
<% 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 : :get, html: { class: 'w-full' } do |f| %>
@ -108,38 +81,4 @@
<% end %>
</ul>
</menu-active>
<% if Docuseal.multitenant? || cannot?(:manage, :tenants) %>
<div id="support_channels" class="mx-4 border-t border-base-300 hidden md:block">
<div class="text-sm mt-3">
<%= t('need_help_ask_a_question_') %>
</div>
<div class="flex mt-3 space-x-3">
<div class="tooltip" data-tip="GitHub">
<a href="<%= Docuseal::GITHUB_URL %>" target="_blank" class="btn btn-circle btn-primary btn-md">
<%= svg_icon('brand_github', class: 'w-8 h-8') %>
</a>
</div>
<div class="tooltip" data-tip="<%= t('discord_community') %>">
<a href="<%= Docuseal::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="<%= Docuseal::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>
<a href="mailto:<%= Docuseal::SUPPORT_EMAIL %>" target="_blank" class="w-full block mt-4 underline text-center">
<%= Docuseal::SUPPORT_EMAIL %>
</a>
<% if Docuseal.version.present? && !Docuseal.multitenant? && can?(:manage, EncryptedConfig) %>
<a href="https://github.com/docusealco/docuseal/releases" target="_blank" class="badge badge-outline text-xs block mx-auto mt-4">
v<%= Docuseal.version %>
</a>
<% end %>
</div>
<% end %>
</div>

@ -1,2 +1,2 @@
<%= render 'shared/logo' %>
<span>DocuSeal</span>
<span><%= wl.brand_name %></span>

@ -1,3 +1,4 @@
<% return unless Whitelabel.show_pro_upsells? %>
<div class="alert">
<%= svg_icon('info_circle', class: 'w-6 h-6') %>
<div>

@ -1,3 +1,4 @@
<% return unless Whitelabel.show_pro_upsells? %>
<div class="alert">
<%= svg_icon('info_circle', class: 'w-6 h-6') %>
<div>

@ -2,5 +2,5 @@
<span class="mr-3">
<%= render 'shared/logo', width: '50px', height: '50px' %>
</span>
<h1 class="text-5xl font-bold text-center">DocuSeal</h1>
<h1 class="text-5xl font-bold text-center"><%= Whitelabel.brand_name %></h1>
</a>

@ -1,4 +1,4 @@
<% content_for(:html_title, "#{@template.name} | DocuSeal") %>
<% content_for(:html_title, "#{@template.name} | #{Whitelabel.brand_name}") %>
<% 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)) %>
<% end %>

@ -1,4 +1,4 @@
<% content_for(:html_title, "#{@template.name} | DocuSeal") %>
<% content_for(:html_title, "#{@template.name} | #{Whitelabel.brand_name}") %>
<% I18n.with_locale(@template.account.locale) do %>
<% content_for(:html_description, t('share_link_is_currently_disabled')) %>
<% end %>

@ -1,4 +1,4 @@
<% content_for(:html_title, "#{@template.name} | DocuSeal") %>
<% content_for(:html_title, "#{@template.name} | #{Whitelabel.brand_name}") %>
<% 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)) %>
<% end %>

@ -1,3 +1,4 @@
<% return unless Whitelabel.show_pro_upsells? %>
<div class="alert">
<%= svg_icon('info_circle', class: 'w-6 h-6') %>
<div>

@ -1,3 +1,4 @@
<% return unless Whitelabel.show_pro_upsells? %>
<div class="mt-2 mb-1">
<div class="tooltip w-full" data-tip="<%= t('unlock_with_docuseal_pro') %>">
<%= link_to submitter.sent_at? ? t('re_send_sms') : t('send_sms'), Docuseal.multitenant? ? console_redirect_index_path(redir: "#{Docuseal::CONSOLE_URL}/plans") : "#{Docuseal::CLOUD_URL}/sign_up?#{{ redir: "#{Docuseal::CONSOLE_URL}/on_premises" }.to_query}", class: 'btn btn-sm btn-primary text-gray-400 w-full' %>

@ -1,4 +1,4 @@
<a href="<%= root_path %>" class="mx-auto text-2xl md:text-3xl font-bold items-center flex space-x-3">
<%= render 'shared/logo', class: 'w-9 h-9 md:w-12 md:h-12' %>
<span><%= Docuseal.product_name %></span>
<span><%= Whitelabel.brand_name %></span>
</a>

@ -1,4 +1,4 @@
<% content_for(:html_title, "#{@submitter.submission.name || @submitter.submission.template.name} | DocuSeal") %>
<% content_for(:html_title, "#{@submitter.submission.name || @submitter.submission.template.name} | #{Whitelabel.brand_name}") %>
<% I18n.with_locale(@submitter.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: @submitter.account.name)) %>
<% end %>

@ -1,4 +1,4 @@
<% content_for(:html_title, "#{@submitter.submission.name || @submitter.submission.template.name} | DocuSeal") %>
<% content_for(:html_title, "#{@submitter.submission.name || @submitter.submission.template.name} | #{Whitelabel.brand_name}") %>
<% I18n.with_locale(@submitter.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: @submitter.account.name)) %>
<% end %>

@ -1,3 +1,4 @@
<% return unless Whitelabel.show_pro_upsells? %>
<div class="alert">
<%= svg_icon('info_circle', class: 'w-6 h-6') %>
<div>

@ -1,9 +1,9 @@
<div class="form-control">
<%= f.label :role, class: 'label' %>
<%= f.select :role, nil, {}, class: 'base-select' do %>
<option value="admin"><%= t('admin') %></option>
<option value="editor" disabled><%= t('editor') %></option>
<option value="viewer" disabled><%= t('viewer') %></option>
<% Whitelabel.manageable_roles(current_user.role).each do |role_slug| %>
<option value="<%= role_slug %>"><%= t("role_#{role_slug}", default: role_slug.humanize) %></option>
<% end %>
<% end %>
<% if Docuseal.multitenant? %>
<label class="label">
@ -12,9 +12,11 @@
</span>
</label>
<% end %>
<a class="text-sm mt-3 px-4 py-2 bg-base-300 rounded-full block" target="_blank" href="<%= Docuseal.multitenant? ? console_redirect_index_path(redir: "#{Docuseal::CONSOLE_URL}/plans") : "#{Docuseal::CLOUD_URL}/sign_up?#{{ redir: "#{Docuseal::CONSOLE_URL}/on_premises" }.to_query}" %>">
<%= svg_icon('info_circle', class: 'w-4 h-4 inline align-text-bottom') %>
<%= t('unlock_more_user_roles_with_docuseal_pro') %>
<span class="link font-medium"><%= t('learn_more') %></span>
</a>
<% if Whitelabel.show_pro_upsells? %>
<a class="text-sm mt-3 px-4 py-2 bg-base-300 rounded-full block" target="_blank" href="<%= Docuseal.multitenant? ? console_redirect_index_path(redir: "#{Docuseal::CONSOLE_URL}/plans") : "#{Docuseal::CLOUD_URL}/sign_up?#{{ redir: "#{Docuseal::CONSOLE_URL}/on_premises" }.to_query}" %>">
<%= svg_icon('info_circle', class: 'w-4 h-4 inline align-text-bottom') %>
<%= t('unlock_more_user_roles_with_docuseal_pro') %>
<span class="link font-medium"><%= t('learn_more') %></span>
</a>
<% end %>
</div>

@ -19,6 +19,19 @@ module DocuSeal
class Application < Rails::Application
config.load_defaults 8.1
# Rails 8.x / Ruby 4.0 compatibility: several ActiveRecord class attributes
# that were formerly configurable were permanently hardcoded and their setter
# methods removed from ActiveRecord::Base. load_defaults 8.1 still adds them
# to the config.active_record hash (via cumulative 5.x-7.x defaults), and the
# AR railtie's set_configs initializer blindly calls the setter for every key,
# raising NoMethodError. Deleting the keys here prevents the setters from
# being called.
%i[
belongs_to_required_by_default
has_many_inversing
run_commit_callbacks_on_first_saved_instances_in_transaction
].each { |key| config.active_record.delete(key) }
config.autoload_lib(ignore: %w[assets tasks puma])
config.active_storage.routes_prefix = ''

@ -73,9 +73,9 @@ Rails.application.configure do
config.action_mailer.perform_caching = false
# Ignore bad email addresses and do not raise email delivery errors.
# Set this to true and configure the email server for immediate delivery to raise delivery errors.
config.action_mailer.raise_delivery_errors = false
# Keep delivery errors visible in production by default so failed invite
# emails are observable. Set to false to silence errors if needed.
config.action_mailer.raise_delivery_errors = ENV.fetch('RAISE_EMAIL_DELIVERY_ERRORS', 'true') == 'true'
if ENV['SMTP_ADDRESS']
config.action_mailer.delivery_method = :smtp

@ -0,0 +1,95 @@
# frozen_string_literal: true
# =============================================================================
# Whitelabel initializer
# =============================================================================
# Triggers config loading (from local file or Dashboard API) and patches the
# Docuseal module constants so that every existing call to
# Docuseal.product_name, Docuseal::PRODUCT_URL, etc. automatically returns
# the white-labelled value.
# =============================================================================
require_relative '../../lib/whitelabel'
# Ensure lib/docuseal.rb is fully loaded before we reopen and patch the module.
# Without this, Zeitwerk sees `module Docuseal` below and marks the constant as
# already defined, so it never loads lib/docuseal.rb — leaving multitenant? and
# other module_function methods undefined during eager loading.
require Rails.root.join('lib/docuseal')
# Patch Docuseal module to delegate brand-related values to Whitelabel
module Docuseal
# Override the product_name method to use Whitelabel config
def self.product_name
Whitelabel.brand_name
end
# Override constants that are used in views/mailers — we make them
# methods instead so they pick up the Whitelabel config dynamically.
# The constants still exist for backward compat but the methods take
# precedence when called as Docuseal.xxx.
def self.product_url
Whitelabel.website_url
end
def self.support_email_address
Whitelabel.support_email
end
def self.github_url_value
Whitelabel.github_url || ''
end
def self.twitter_url_value
Whitelabel.twitter_url || ''
end
def self.twitter_handle_value
Whitelabel.twitter_handle || ''
end
def self.discord_url_value
Whitelabel.discord_url || ''
end
end
Rails.application.config.i18n.default_locale = Whitelabel.default_locale.to_sym
Rails.application.config.i18n.available_locales = Whitelabel.available_locales.map(&:to_sym)
Rails.application.config.i18n.fallbacks = [Whitelabel.fallback_locale.to_sym]
deep_stringify_keys = lambda do |hash|
hash.each_with_object({}) do |(key, value), memo|
string_key = key.to_s
memo[string_key] = value.is_a?(Hash) ? deep_stringify_keys.call(value) : value
end
end
deep_merge_hash = lambda do |left, right|
left.merge(right) do |_key, left_value, right_value|
if left_value.is_a?(Hash) && right_value.is_a?(Hash)
deep_merge_hash.call(left_value, right_value)
else
right_value
end
end
end
undot_keys = lambda do |hash|
hash.each_with_object({}) do |(key, value), memo|
if key.include?('.')
head, *tail = key.split('.')
nested = tail.reverse.reduce(value) { |acc, segment| { segment => acc } }
memo[head] = memo.key?(head) ? deep_merge_hash.call(memo[head], nested) : nested
else
memo[key] = value.is_a?(Hash) ? undot_keys.call(value) : value
end
end
end
Whitelabel.translation_overrides.each do |locale, raw_values|
normalized = undot_keys.call(deep_stringify_keys.call(raw_values))
I18n.backend.store_translations(locale.to_sym, normalized)
end
Rails.logger.info "[Whitelabel] Loaded brand: #{Whitelabel.brand_name}"

@ -0,0 +1,128 @@
# =============================================================================
# White-Label Locale Overrides
# =============================================================================
# This file overrides i18n keys that contain brand-specific text (DocuSeal).
# Rails automatically merges locale files, so keys here take precedence.
#
# When upstream DocuSeal updates add new branded keys to i18n.yml,
# add the override here — keeping the main file untouched for easy merging.
#
# Languages: English (en) + French (fr)
# =============================================================================
en: &en_wl
docuseal_trusted_signature: "Intébec Trusted Signature"
sent_with_docuseal_pro_html: 'Sent with <a href="%{product_url}">Intébec Pro</a>'
show_send_with_docuseal_pro_attribution_in_emails_html: 'Show "Sent with <span class="link">Intébec Pro</span>" attribution in emails'
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: "Sign documents with trusted certificate provided by Intébec. Your documents and data are never shared with Intébec. PDF checksum is provided to generate a trusted signature."
by_creating_an_account_you_agree_to_our_html: 'By creating an account, you agree to our <a target="_blank" href="https://intebec.ca/privacy">Privacy Policy</a> and <a target="_blank" href="https://intebec.ca/terms">Terms of Service</a>.'
connect_salesforce_account_to_integrate_with_docuseal: "Connect Salesforce account to integrate with Intébec"
sign_up_in_docuseal_console_to_upgrade_on_premises_app_is_completely_standalone_console_is_used_only_to_manage_your_license: "Sign up in Intébec Console to upgrade. On-premises app is completely standalone, Console is used only to manage your license."
unlock_with_docuseal_pro: "Unlock with Intébec Pro"
activate_with_docuseal_pro: "Activate with Intébec Pro"
send_signature_request_emails_without_limits_with_docuseal_pro: "Send signature request emails without limits with Intébec Pro"
unlock_more_user_roles_with_docuseal_pro: "Unlock more user roles with Intébec Pro."
docuseal_support: "Intébec Support"
click_here_to_learn_more_about_user_roles_and_permissions_html: '<a href="https://intebec.ca/resources/manage-users-and-roles" class="link" rel="noopener noreferrer nofollow" target="_blank">Click here</a> to learn more about user roles and permissions.'
on_a_scale_of_1_to_10_how_satisfied_are_you_with_the_docuseal_product_: "On a scale of 1 to 10, how satisfied are you with the Intébec product?"
your_pro_plan_has_been_suspended_due_to_unpaid_invoices_you_can_update_your_payment_details_to_settle_the_invoice_and_continue_using_docuseal_or_cancel_your_subscription: "Your Pro Plan has been suspended due to unpaid invoices. You can update your payment details to settle the invoice and continue using Intébec or cancel your subscription."
this_submission_has_multiple_signers_which_prevents_the_use_of_a_sharing_link_html: 'This submission has multiple signers, which prevents the use of a sharing link as it''s unclear which signer is responsible for specific fields. To resolve this, follow this <a href="https://intebec.ca/resources/pre-filling-recipients" class="link font-bold" rel="noopener noreferrer nofollow" target="_blank">guide</a> to define the default signer details.'
welcome_to_docuseal: "Welcome to Intébec"
start_a_quick_tour_to_learn_how_to_create_an_send_your_first_document: "Start a quick tour to learn how to create and send your first document with Intébec."
your_email_could_not_be_reached_this_may_happen_if_there_was_a_typo_in_your_address_or_if_your_mailbox_is_not_available_please_contact_support_email_to_log_in: "Your email could not be reached. This may happen if there was a typo in your address or if your mailbox is not available. Please contact support@intebec.ca to log in."
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: "Add a unique Signature ID and timestamp to each signature for audit and traceability purposes along with the timestamp. Part of Intébec's 21 CFR Part 11 compliance settings."
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: "Require signers to provide a reason for signing before completing their signature (e.g., approvals, certifications). Part of Intébec's 21 CFR Part 11 compliance settings."
role_admin: "Administrator"
role_gestionnaire: "Manager"
role_user: "User"
unauthorized:
default: "You are not authorized to access this page."
manage:
all: "You are not authorized to perform this action."
onboarding:
support_description: "You can use our self-service AI assistant or email us at support@intebec.ca if you have any questions."
fr: &fr_wl
docuseal_trusted_signature: "Signature de confiance Intébec"
sent_with_docuseal_pro_html: 'Envoyé avec <a href="%{product_url}">Intébec Pro</a>'
show_send_with_docuseal_pro_attribution_in_emails_html: 'Afficher l''attribution « Envoyé avec <span class="link">Intébec Pro</span> » dans les courriels'
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: "Signez des documents avec un certificat de confiance fourni par Intébec. Vos documents et données ne sont jamais partagés avec Intébec. Une somme de contrôle PDF est fournie pour générer une signature de confiance."
by_creating_an_account_you_agree_to_our_html: 'En créant un compte, vous acceptez notre <a target="_blank" href="https://intebec.ca/privacy">Politique de confidentialité</a> et nos <a target="_blank" href="https://intebec.ca/terms">Conditions d''utilisation</a>.'
connect_salesforce_account_to_integrate_with_docuseal: "Connectez votre compte Salesforce pour l'intégrer à Intébec"
sign_up_in_docuseal_console_to_upgrade_on_premises_app_is_completely_standalone_console_is_used_only_to_manage_your_license: "Inscrivez-vous dans la console Intébec pour effectuer la mise à niveau. L'application sur site est entièrement autonome, la console est utilisée uniquement pour gérer votre licence."
unlock_with_docuseal_pro: "Débloquer avec Intébec Pro"
activate_with_docuseal_pro: "Activer avec Intébec Pro"
send_signature_request_emails_without_limits_with_docuseal_pro: "Envoyez des courriels de demande de signature sans limites avec Intébec Pro"
unlock_more_user_roles_with_docuseal_pro: "Débloquez plus de rôles utilisateurs avec Intébec Pro."
docuseal_support: "Soutien Intébec"
click_here_to_learn_more_about_user_roles_and_permissions_html: '<a href="https://intebec.ca/resources/manage-users-and-roles" class="link" rel="noopener noreferrer nofollow" target="_blank">Cliquez ici</a> pour en savoir plus sur les rôles et les permissions des utilisateurs.'
on_a_scale_of_1_to_10_how_satisfied_are_you_with_the_docuseal_product_: "Sur une échelle de 1 à 10, à quel point êtes-vous satisfait du produit Intébec?"
your_pro_plan_has_been_suspended_due_to_unpaid_invoices_you_can_update_your_payment_details_to_settle_the_invoice_and_continue_using_docuseal_or_cancel_your_subscription: "Votre plan Pro a été suspendu en raison de factures impayées. Vous pouvez mettre à jour vos informations de paiement pour régler la facture et continuer à utiliser Intébec ou annuler votre abonnement."
this_submission_has_multiple_signers_which_prevents_the_use_of_a_sharing_link_html: 'Cette soumission comporte plusieurs signataires, ce qui empêche l''utilisation d''un lien de partage car on ne sait pas quel signataire est responsable de quels champs. Pour résoudre ce problème, suivez ce <a href="https://intebec.ca/resources/pre-filling-recipients" class="link font-bold" rel="noopener noreferrer nofollow" target="_blank">guide</a> pour définir les détails du signataire par défaut.'
welcome_to_docuseal: "Bienvenue sur Intébec"
start_a_quick_tour_to_learn_how_to_create_an_send_your_first_document: "Lancez une visite rapide pour apprendre à créer et envoyer votre premier document avec Intébec."
your_email_could_not_be_reached_this_may_happen_if_there_was_a_typo_in_your_address_or_if_your_mailbox_is_not_available_please_contact_support_email_to_log_in: "Votre courriel n'a pas pu être atteint. Cela peut arriver s'il y a une faute de frappe dans votre adresse ou si votre boîte de réception n'est pas disponible. Veuillez contacter support@intebec.ca pour vous connecter."
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: "Ajoutez un identifiant de signature unique et un horodatage à chaque signature à des fins d'audit et de traçabilité, ainsi que l'horodatage. Fait partie des paramètres de conformité 21 CFR Part 11 d'Intébec."
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: "Exiger que le signataire fournisse une raison de signature avant de terminer sa signature (ex. : approbations, certifications). Fait partie des paramètres de conformité 21 CFR Part 11 d'Intébec."
role_admin: "Administrateur"
role_gestionnaire: "Gestionnaire"
role_user: "Utilisateur"
onboarding:
support_description: "Vous pouvez utiliser notre assistant IA en libre-service ou nous écrire à support@intebec.ca si vous avez des questions."
# -------------------------------------------------------------------------
# Common French overrides for base UI strings
# -------------------------------------------------------------------------
sign_in: "Connexion"
sign_out: "Déconnexion"
sign_up: "Inscription"
settings: "Paramètres"
profile: "Profil"
password: "Mot de passe"
email: "Courriel"
submit: "Soumettre"
save: "Enregistrer"
cancel: "Annuler"
delete: "Supprimer"
search: "Rechercher"
download: "Télécharger"
powered_by: "Propulsé par"
open_source_documents_software: "logiciel de signature de documents"
unauthorized:
default: "Vous n'êtes pas autorisé à accéder à cette page."
manage:
all: "Vous n'êtes pas autorisé à effectuer cette action."
devise:
sessions:
signed_in: "Connexion réussie."
signed_out: "Déconnexion réussie."
already_signed_in: "Vous êtes déjà connecté."
failure:
already_authenticated: "Vous êtes déjà connecté."
unauthenticated: "Vous devez vous connecter pour accéder à cette page."
locked: "Votre compte est verrouillé."
invalid: "Adresse courriel ou mot de passe invalide."
last_attempt: "Il vous reste une tentative avant que votre compte soit verrouillé."
not_found_in_database: "Adresse courriel ou mot de passe invalide."
timeout: "Votre session a expiré. Veuillez vous reconnecter."
inactive: "Votre compte n'est pas encore activé."
passwords:
send_instructions: "Vous recevrez un courriel avec les instructions pour réinitialiser votre mot de passe."
send_paranoid_instructions: "Si votre courriel est dans notre base de données, vous recevrez un lien pour réinitialiser votre mot de passe."
updated: "Votre mot de passe a été modifié. Vous êtes maintenant connecté."
updated_not_active: "Votre mot de passe a été modifié avec succès."
mailer:
reset_password_instructions:
subject: "Réinitialisation de votre mot de passe"
# ---------------------------------------------------------------------------
# Regional locale aliases — inherit all overrides from the base locales above
# (mirrors the YAML anchor pattern used in i18n.yml)
# ---------------------------------------------------------------------------
en-US:
<<: *en_wl
en-GB:
<<: *en_wl
fr-FR:
<<: *fr_wl

@ -1,39 +1,16 @@
services:
app:
depends_on:
postgres:
condition: service_healthy
image: docuseal/docuseal:latest
build: .
image: intebec/docuseal:latest
ports:
- 3000:3000
volumes:
- ./docuseal:/data/docuseal
- ${INTEBEC_CONFIG_FILE:-./config/config.yml}:/run/secrets/config.yml:ro
environment:
- FORCE_SSL=${HOST}
- DATABASE_URL=postgresql://postgres:postgres@postgres:5432/docuseal
postgres:
image: postgres:18
volumes:
- './pg_data:/var/lib/postgresql/18/docker'
environment:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
POSTGRES_DB: docuseal
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres"]
interval: 5s
timeout: 5s
retries: 5
caddy:
image: caddy:latest
command: caddy reverse-proxy --from $HOST --to app:3000
ports:
- 80:80
- 443:443
- 443:443/udp
volumes:
- ./caddy:/data/caddy
environment:
- HOST=${HOST}
- FORCE_SSL=${FORCE_SSL:-false}
# File mode: mount a config.yml (see volumes below).
# API mode: remove the volume mount and set INTEBEC_LICENCE_KEY + INTEBEC_SECRET_KEY.
- INTEBEC_CONFIG_PATH=/run/secrets/config.yml
- INTEBEC_LICENCE_KEY=${INTEBEC_LICENCE_KEY:-}
- INTEBEC_SECRET_KEY=${INTEBEC_SECRET_KEY:-}

@ -0,0 +1,51 @@
# White-Label Developer Reference (Internal)
> **This file is for Intebec developers only.**
> Do NOT add config schema details, API contracts, or example YAML here.
> The private config template is managed in the Intebec Dashboard, not in this repo.
### In ERB views
```erb
<%= wl.brand_name %>
<%= wl.logo_path %>
<%= wl.support_email %>
<%= wl.page_title(signed_in: true) %>
```
### In Ruby (controllers, mailers, lib)
```ruby
Whitelabel.brand_name
Whitelabel.website_url
Whitelabel.email_from
Whitelabel.sign_reason("John")
Whitelabel.theme(:primary)
```
## Upstream Merge Strategy
This system is designed to minimise merge conflicts with the upstream DocuSeal repo:
1. **New files** (no conflicts): `lib/whitelabel.rb`, `config/initializers/whitelabel.rb`, `app/helpers/whitelabel_helper.rb`, `config/locales/whitelabel.yml`
2. **Patched files** (potential conflicts, but isolated changes):
- `lib/docuseal.rb` — only added a comment block; the `product_name` method is overridden at runtime
- View templates — changes are surgical (replacing one hardcoded string with a `Whitelabel.xxx` call)
3. **Untouched internal identifiers**: `data-theme="docuseal"`, `Docuseal` module name, `#docuseal_modal_container`, `docuseal_clipboard` localStorage keys — all kept as-is for compatibility
### When Upstream Adds New Branded Content
1. Check if new views/lib files have hardcoded "DocuSeal" text
2. Replace with `Whitelabel.brand_name` or `wl.brand_name`
3. If it's an i18n key, add the override to `config/locales/whitelabel.yml`
## File Reference
| File | Purpose | Upstream risk |
| ----------------------------------- | ---------------------------- | ----------------------- |
| `lib/whitelabel.rb` | Config loader + licence gate | New file — zero risk |
| `config/initializers/whitelabel.rb` | Boot-time patching | New file — zero risk |
| `app/helpers/whitelabel_helper.rb` | View helper | New file — zero risk |
| `config/locales/whitelabel.yml` | i18n overrides | New file — zero risk |
| `public/intebec.css` | Theme CSS | Custom file — zero risk |
| `lib/docuseal.rb` | Added comment | Low risk — comment only |

@ -3,23 +3,101 @@
class Ability
include CanCan::Ability
# Maps config resource names → CanCan model + action rules.
# All condition procs MUST return hashes (not AR relations) so that
# class-level can?/authorize! checks work (e.g. `authorize! :index, Template`).
RESOURCE_MAP = {
'templates' => [
[Template, :read, ->(u) { { account_id: u.account_id } }],
[Template, :create, ->(u) { { account_id: u.account_id } }],
[Template, :update, ->(u) { { account_id: u.account_id } }],
[Template, :destroy, ->(u) { { account_id: u.account_id } }],
[TemplateFolder, :manage, ->(u) { { account_id: u.account_id } }],
[TemplateSharing, :manage, ->(u) { { template: { account_id: u.account_id } } }]
],
'submissions' => [
[Submission, :manage, ->(u) { { account_id: u.account_id } }],
[Submitter, :manage, ->(u) { { account_id: u.account_id } }]
],
'users' => [
[User, :manage, ->(u) { { account_id: u.account_id } }]
],
'settings' => [
[EncryptedConfig, :manage, ->(u) { { account_id: u.account_id } }],
[AccountConfig, :manage, ->(u) { { account_id: u.account_id } }],
[Account, :manage, ->(u) { { id: u.account_id } }],
[WebhookUrl, :manage, ->(u) { { account_id: u.account_id } }]
]
}.freeze
def initialize(user)
can %i[read create update], Template, Abilities::TemplateConditions.collection(user) do |template|
Abilities::TemplateConditions.entity(template, user:, ability: 'manage')
end
return unless user
always_allowed(user)
apply_role_permissions(user)
end
private
can :destroy, Template, account_id: user.account_id
can :manage, TemplateFolder, account_id: user.account_id
can :manage, TemplateSharing, template: { account_id: user.account_id }
can :manage, Submission, account_id: user.account_id
can :manage, Submitter, account_id: user.account_id
can :manage, User, account_id: user.account_id
can :manage, EncryptedConfig, account_id: user.account_id
# Personal resources — always available regardless of role.
def always_allowed(user)
can :manage, EncryptedUserConfig, user_id: user.id
can :manage, AccountConfig, account_id: user.account_id
can :manage, UserConfig, user_id: user.id
can :manage, Account, id: user.account_id
can :manage, User, id: user.id
can :read, Account, id: user.account_id
can :manage, AccessToken, user_id: user.id
can :manage, WebhookUrl, account_id: user.account_id
end
def apply_role_permissions(user)
role = user.role.to_s
RESOURCE_MAP.each do |resource_key, model_rules|
config_actions = Whitelabel.role_permissions(role, resource_key)
model_rules.each do |model, cancan_action, condition_proc|
grant_if_allowed(user, model, cancan_action, condition_proc, config_actions)
end
end
end
def grant_if_allowed(user, model, cancan_action, condition_proc, config_actions)
needed = action_to_config(cancan_action)
return unless (needed & config_actions).any?
conditions = condition_proc.call(user)
granted = map_cancan_actions(cancan_action, config_actions)
return if granted.empty?
# Hash-only conditions. Shared-template / linked-account filtering
# is handled at the controller level (TemplateConditions.collection,
# filter_templates, etc.) — CanCanCan forbids hash + block together.
can granted, model, conditions
end
# Map a CanCan :manage action to the individual config actions that are allowed.
def map_cancan_actions(cancan_action, config_actions)
if cancan_action == :manage
mapped = []
mapped << :read if config_actions.include?('read')
mapped << :create if config_actions.include?('create')
mapped << :update if config_actions.include?('update')
mapped << :destroy if config_actions.include?('delete')
mapped
elsif cancan_action == :destroy
config_actions.include?('delete') ? [:destroy] : []
else
config_actions.include?(cancan_action.to_s) ? [cancan_action] : []
end
end
def action_to_config(cancan_action)
case cancan_action
when :manage then %w[read create update delete]
when :read then %w[read]
when :create then %w[create]
when :update then %w[update]
when :destroy then %w[delete]
else [cancan_action.to_s]
end
end
end

@ -13,7 +13,7 @@ module Accounts
new_user.uuid = SecureRandom.uuid
new_user.account = new_account
new_user.encrypted_password = SecureRandom.hex
new_user.email = "#{SecureRandom.hex}@docuseal.com"
new_user.email = "#{SecureRandom.hex}@#{Whitelabel.temp_email_domain}"
account.templates.each do |template|
new_template = template.dup

@ -2,6 +2,9 @@
module Docuseal
URL_CACHE = ActiveSupport::Cache::MemoryStore.new
# NOTE: These constants are kept for backward compatibility with upstream
# DocuSeal code. User-visible values are overridden at runtime by the
# Whitelabel module (see config/config.example.yml + config/initializers/whitelabel.rb).
PRODUCT_URL = 'https://www.docuseal.com'
PRODUCT_EMAIL_URL = ENV.fetch('PRODUCT_EMAIL_URL', PRODUCT_URL)
NEWSLETTER_URL = "#{PRODUCT_URL}/newsletters".freeze

@ -1,7 +1,7 @@
# frozen_string_literal: true
module SendWebhookRequest
USER_AGENT = 'DocuSeal.com Webhook'
USER_AGENT = Whitelabel.webhook_user_agent
LOCALHOSTS = DownloadUtils::LOCALHOSTS

@ -43,7 +43,7 @@ module Submissions
io = StringIO.new
document.trailer.info[:Creator] = "#{Docuseal.product_name} (#{Docuseal::PRODUCT_URL})"
document.trailer.info[:Creator] = Whitelabel.pdf_creator
if pkcs
sign_params = {
@ -506,7 +506,7 @@ module Submissions
end
def sign_reason
'Signed with DocuSeal.com'
Whitelabel.audit_trail_footer
end
def select_attachments(submitter)
@ -528,8 +528,8 @@ module Submissions
def add_logo(column, _submission = nil)
column.image(PdfIcons.logo_io, width: 40, height: 40, position: :float)
column.formatted_text([{ text: 'DocuSeal',
link: Docuseal::PRODUCT_EMAIL_URL }],
column.formatted_text([{ text: Whitelabel.brand_name,
link: Whitelabel.website_url }],
font_size: 20,
font: [FONT_NAME, { variant: :bold }],
width: 100,

@ -37,7 +37,7 @@ module Submissions
bold_italic: FONT_BOLD_NAME
}.freeze
SIGN_REASON = 'Signed by %<name>s with DocuSeal.com'
SIGN_REASON = "Signed by %<name>s with #{Whitelabel.brand_name}"
RTL_REGEXP = TextUtils::RTL_REGEXP
@ -952,7 +952,7 @@ module Submissions
end
def info_creator
"#{Docuseal.product_name} (#{Docuseal::PRODUCT_URL})"
Whitelabel.pdf_creator
end
def detached_signature?(_submitter)

@ -2,27 +2,29 @@
module Submitters
module FormConfigs
DEFAULT_KEYS = [AccountConfig::FORM_COMPLETED_BUTTON_KEY,
AccountConfig::FORM_COMPLETED_MESSAGE_KEY,
AccountConfig::FORM_WITH_CONFETTI_KEY,
AccountConfig::FORM_PREFILL_SIGNATURE_KEY,
AccountConfig::WITH_SIGNATURE_ID,
AccountConfig::ALLOW_TO_DECLINE_KEY,
AccountConfig::ENFORCE_SIGNING_ORDER_KEY,
AccountConfig::REQUIRE_SIGNING_REASON_KEY,
AccountConfig::REUSE_SIGNATURE_KEY,
AccountConfig::WITH_FIELD_LABELS_KEY,
AccountConfig::ALLOW_TO_PARTIAL_DOWNLOAD_KEY,
AccountConfig::ALLOW_TYPED_SIGNATURE,
AccountConfig::WITH_SUBMITTER_TIMEZONE_KEY,
AccountConfig::WITH_TIMESTAMP_SECONDS_KEY,
AccountConfig::WITH_SIGNATURE_ID_REASON_KEY,
*(Docuseal.multitenant? ? [] : [AccountConfig::POLICY_LINKS_KEY])].freeze
module_function
def default_keys
[AccountConfig::FORM_COMPLETED_BUTTON_KEY,
AccountConfig::FORM_COMPLETED_MESSAGE_KEY,
AccountConfig::FORM_WITH_CONFETTI_KEY,
AccountConfig::FORM_PREFILL_SIGNATURE_KEY,
AccountConfig::WITH_SIGNATURE_ID,
AccountConfig::ALLOW_TO_DECLINE_KEY,
AccountConfig::ENFORCE_SIGNING_ORDER_KEY,
AccountConfig::REQUIRE_SIGNING_REASON_KEY,
AccountConfig::REUSE_SIGNATURE_KEY,
AccountConfig::WITH_FIELD_LABELS_KEY,
AccountConfig::ALLOW_TO_PARTIAL_DOWNLOAD_KEY,
AccountConfig::ALLOW_TYPED_SIGNATURE,
AccountConfig::WITH_SUBMITTER_TIMEZONE_KEY,
AccountConfig::WITH_TIMESTAMP_SECONDS_KEY,
AccountConfig::WITH_SIGNATURE_ID_REASON_KEY,
*(Docuseal.multitenant? ? [] : [AccountConfig::POLICY_LINKS_KEY])]
end
def call(submitter, keys = [])
configs = submitter.submission.account.account_configs.where(key: DEFAULT_KEYS + keys)
configs = submitter.submission.account.account_configs.where(key: default_keys + keys)
completed_button = find_safe_value(configs, AccountConfig::FORM_COMPLETED_BUTTON_KEY) || {}
completed_message = find_safe_value(configs, AccountConfig::FORM_COMPLETED_MESSAGE_KEY) || {}

@ -0,0 +1,737 @@
# frozen_string_literal: true
# =============================================================================
# Whitelabel — Centralised brand config + licence enforcement
# =============================================================================
#
# Config loading priority:
# 1. Local YAML file → if present, used as-is (dev / custom deploys)
# 2. Remote API fetch → if no file, calls Intebec Dashboard (production)
# 3. Empty defaults → test environment only
#
# All accessors use dig() with safe fallbacks so the app never crashes on
# missing keys. Without a valid config source the fallbacks return plain
# upstream DocuSeal values — your branding only appears with YOUR config.
#
# Env vars:
# INTEBEC_CONFIG_PATH — override local file path (default: /run/secrets/config.yml)
# INTEBEC_LICENCE_KEY — licence UUID (required for API mode)
# INTEBEC_SECRET_KEY — HMAC shared secret (required for API mode)
# INTEBEC_DASHBOARD_URL — override Dashboard URL (default: https://dashboard.intebec.ca)
# =============================================================================
require 'yaml'
require 'uri'
require 'json'
require 'openssl'
require 'net/http'
require 'securerandom'
module Whitelabel
class ConfigError < StandardError; end
class LicenceRevokedError < ConfigError; end
CONFIG_PATH = Pathname.new(
ENV.fetch('INTEBEC_CONFIG_PATH', '/run/secrets/config.yml')
).freeze
DASHBOARD_URL = ENV.fetch('INTEBEC_DASHBOARD_URL', 'https://dashboard.intebec.ca').freeze
CONFIG_ENDPOINT = '/api/licences/config'
API_TIMEOUT = 10
API_MAX_RETRIES = 3
API_RETRY_DELAY = 2 # seconds, doubles each retry
REFRESH_INTERVAL = 24 * 3600 # 24 h
REFRESH_ON_ERROR = 5 * 60 # 5 min retry on transient failure
THEME_DEFAULTS = {
'primary' => '216 77% 52%',
'primary_focus' => '216 77% 44%',
'primary_content' => '0 0% 100%',
'secondary' => '220 12% 45%',
'secondary_focus' => '220 14% 36%',
'secondary_content' => '0 0% 100%',
'accent' => '160 50% 40%',
'accent_focus' => '160 50% 34%',
'accent_content' => '0 0% 100%',
'neutral' => '220 16% 12%',
'neutral_focus' => '220 16% 8%',
'neutral_content' => '0 0% 100%',
'base_100' => '0 0% 100%',
'base_200' => '220 14% 96%',
'base_300' => '220 12% 93%',
'base_content' => '220 14% 10%',
'info' => '205 80% 50%',
'success' => '154 55% 38%',
'warning' => '38 88% 48%',
'error' => '0 72% 50%',
'rounded_btn' => '1.9rem',
'tab_border' => '2px',
'tab_radius' => '.5rem'
}.freeze
DEFAULT_STYLING_VARIABLES = {
'ib-bg' => '220 14% 98%',
'ib-surface' => '0 0% 100%',
'ib-surface-2' => '220 14% 96%',
'ib-border' => '220 10% 88%',
'ib-text' => '220 14% 10%',
'ib-text-secondary' => '220 8% 40%',
'ib-muted' => '220 6% 55%'
}.freeze
# ── Mutable state (thread-safe) ─────────────────────────────────────────
@mutex = Mutex.new
@config = nil
@api_sourced = false
@next_refresh = Time.at(0).utc
class << self
# =====================================================================
# Core
# =====================================================================
def config
@config || load_config!
end
def reload!
@mutex.synchronize { @config = nil }
load_config!
end
def config_source
return :api if @api_sourced
return :test if @config && !CONFIG_PATH.file?
:file
end
# Called per-request from ApplicationController.
# For API-sourced configs, periodically re-fetches to confirm the
# licence is still active and pick up any Dashboard changes.
def ensure_valid!
return true unless @api_sourced
return true unless Time.now.utc >= @next_refresh
@mutex.synchronize do
return true unless Time.now.utc >= @next_refresh
@config = fetch_remote_config
@next_refresh = Time.now.utc + REFRESH_INTERVAL
rescue LicenceRevokedError
# Licence actively revoked → propagate, controller returns 503
@config = {}
raise
rescue ConfigError => e
# Transient error (network, timeout) → keep existing config, retry sooner
Rails.logger.error("[Whitelabel] Revalidation failed: #{e.message}")
@next_refresh = Time.now.utc + REFRESH_ON_ERROR
end
true
end
# =====================================================================
# Brand
# =====================================================================
def brand_name
config.dig('brand', 'name') || 'DocuSeal'
end
def brand_short_name
config.dig('brand', 'short_name') || brand_name
end
def tagline
config.dig('brand', 'tagline') || ''
end
def description
config.dig('brand', 'description') || ''
end
def page_title(signed_in: false)
key = signed_in ? 'page_title_signed_in' : 'page_title_signed_out'
config.dig('brand', key) || brand_name
end
# =====================================================================
# URLs
# =====================================================================
def website_url
config.dig('urls', 'website') || 'https://www.docuseal.com'
end
def support_email
config.dig('urls', 'support_email') || 'support@docuseal.com'
end
def privacy_policy_url
config.dig('urls', 'privacy_policy')
end
def terms_url
config.dig('urls', 'terms_of_service')
end
def twitter_url
config.dig('urls', 'twitter_url')
end
def twitter_handle
config.dig('urls', 'twitter_handle')
end
def github_url
config.dig('urls', 'github_url')
end
def discord_url
config.dig('urls', 'discord_url')
end
# =====================================================================
# Email
# =====================================================================
def email_from
name = config.dig('email', 'from_name') || brand_name
addr = config.dig('email', 'from_address') || support_email
"#{name} <#{addr}>"
end
def email_attribution_html
raw = config.dig('email', 'attribution_html') ||
'Sent with <a href="%{website}">%{brand}</a>.'
raw.gsub('%{brand}', brand_name).gsub('%{website}', website_url)
end
# =====================================================================
# Assets
# =====================================================================
def logo_path
config.dig('assets', 'logo_path') || '/logo.svg'
end
def logo_width
config.dig('assets', 'logo_width') || 37
end
def logo_height
config.dig('assets', 'logo_height') || 37
end
def favicon_svg
config.dig('assets', 'favicon_svg') || '/favicon.svg'
end
def favicon_ico
config.dig('assets', 'favicon_ico') || '/favicon.ico'
end
def favicon_16
config.dig('assets', 'favicon_16') || '/favicon-16x16.png'
end
def favicon_32
config.dig('assets', 'favicon_32') || '/favicon-32x32.png'
end
def favicon_96
config.dig('assets', 'favicon_96') || '/favicon-96x96.png'
end
def apple_touch_icon
config.dig('assets', 'apple_touch_icon') || '/apple-icon-180x180.png'
end
def preview_image
config.dig('assets', 'preview_image') || '/preview.png'
end
# =====================================================================
# Theme — HSL triplets for DaisyUI / CSS custom properties
# =====================================================================
def theme(key)
config.dig('theme', key.to_s) || THEME_DEFAULTS[key.to_s] || '0 0% 50%'
end
# =====================================================================
# PDF / Audit trail
# =====================================================================
def sign_reason(name)
template = config.dig('pdf', 'sign_reason') || 'Signed by %{name}'
template.gsub('%{name}', name.to_s)
end
def audit_trail_footer
config.dig('pdf', 'audit_trail_footer') || "Signed with #{brand_name}"
end
def pdf_creator
creator = config.dig('pdf', 'creator') || brand_name
"#{creator} (#{website_url})"
end
def cert_name
config.dig('pdf', 'cert_name') || 'docuseal_aatl'
end
# =====================================================================
# PWA
# =====================================================================
def pwa_description
config.dig('pwa', 'description') || description
end
def pwa_theme_color
config.dig('pwa', 'theme_color') || '#FFFFFF'
end
def pwa_background_color
config.dig('pwa', 'background_color') || '#FFFFFF'
end
# =====================================================================
# Webhooks
# =====================================================================
def webhook_user_agent
config.dig('webhooks', 'user_agent') || "#{brand_name} Webhook"
end
# =====================================================================
# Feature flags
# =====================================================================
def show_github_button?
dig_bool('features', 'show_github_button', false)
end
def show_powered_by?
dig_bool('features', 'show_powered_by', false)
end
def powered_by_text
config.dig('features', 'powered_by_text') || brand_name
end
def show_ai_link?
dig_bool('features', 'show_ai_link', false)
end
def show_discord_link?
dig_bool('features', 'show_discord_link', false)
end
def show_pro_upsells?
dig_bool('features', 'show_pro_upsells', false)
end
# =====================================================================
# Roles & Permissions (config-driven)
# =====================================================================
#
# Config format:
# roles:
# admin:
# permissions:
# templates: [read, create, update, delete]
# submissions: [read, create, update, delete]
# users: [read, create, update, delete]
# settings: [read, create, update, delete]
# gestionnaire:
# permissions:
# templates: [read, create, update, delete]
# submissions: [read, create, update, delete]
# users: [read]
# settings: [read]
# user:
# permissions:
# templates: [read]
# submissions: [read]
#
# Default permission matrix — used when no roles section in config.
DEFAULT_ROLES = {
'admin' => {
'permissions' => {
'templates' => %w[read create update delete],
'submissions' => %w[read create update delete],
'users' => %w[read create update delete],
'settings' => %w[read create update delete]
}
},
'gestionnaire' => {
'permissions' => {
'templates' => %w[read create update delete],
'submissions' => %w[read create update delete],
'users' => %w[read],
'settings' => %w[read]
}
},
'user' => {
'permissions' => {
'templates' => %w[read],
'submissions' => %w[read],
'users' => [],
'settings' => []
}
}
}.freeze
# All available roles (keys). Order matters — first is the default.
def roles
(config.dig('roles') || DEFAULT_ROLES).keys
end
# The default role assigned to new users.
def default_role
roles.first
end
# Full role definition hash for a given role slug.
def role_definition(role_slug)
all = config.dig('roles') || DEFAULT_ROLES
all[role_slug.to_s] || {}
end
# Permission list for a role + resource.
# Returns e.g. ["read", "create", "update"] or [].
def role_permissions(role_slug, resource)
perms = role_definition(role_slug).dig('permissions', resource.to_s)
perms.is_a?(Array) ? perms : []
end
# Check if a role has a specific action on a resource.
def role_can?(role_slug, resource, action)
role_permissions(role_slug, resource).include?(action.to_s)
end
# Check if a role is an admin (first role in the list is always the admin).
def admin_role?(role_slug)
role_slug.to_s == roles.first
end
# Validate that a role slug exists in config.
def role_valid?(role_slug)
roles.include?(role_slug.to_s)
end
# Returns the rank index of a role (0 = highest privilege = admin).
# Unknown roles return roles.size (treated as lowest).
def role_rank(role_slug)
roles.index(role_slug.to_s) || roles.size
end
# Returns only roles that the given actor_role can assign/manage.
# An actor can only work with roles at their own rank or lower (higher index).
def manageable_roles(actor_role)
rank = role_rank(actor_role.to_s)
roles[rank..]
end
# All known settings sections in display order.
ALL_SETTINGS_SECTIONS = %w[account email storage notifications esign personalization users api webhooks].freeze
# Returns true if the role is allowed to see the given settings section.
# Falls back to ALL_SETTINGS_SECTIONS for roles that have settings read
# permission but no explicit sections list (backward-compatible).
def setting_section_visible?(role_slug, section)
defn = role_definition(role_slug)
sections = defn['settings_sections']
if sections.is_a?(Array)
sections.map(&:to_s).include?(section.to_s)
else
# No explicit list → grant all sections to roles that can read settings.
role_permissions(role_slug, 'settings').include?('read')
end
end
# =====================================================================
# Internal
# =====================================================================
def temp_email_domain
config.dig('internal', 'temp_email_domain') || 'docuseal.com'
end
# =====================================================================
# Locale / Translations
# =====================================================================
def default_locale
config.dig('locale', 'default') || 'en'
end
def available_locales
config.dig('locale', 'available') || %w[en]
end
def fallback_locale
config.dig('locale', 'fallback') || 'en'
end
def translation_overrides
config.dig('text', 'translations') || {}
end
# =====================================================================
# Styling
# =====================================================================
def styling_variables
DEFAULT_STYLING_VARIABLES.merge(config.dig('styling', 'css_variables') || {})
end
def inline_css_variables
vars = {
'wl-ib-primary' => theme(:primary),
'wl-ib-primary-strong' => theme(:primary_focus),
'wl-ib-primary-soft' => "#{theme(:primary)} / 0.12",
'wl-ib-neutral' => theme(:neutral),
'wl-ib-neutral-soft' => theme(:base_200),
'wl-p' => theme(:primary),
'wl-pf' => theme(:primary_focus),
'wl-pc' => theme(:primary_content),
'wl-s' => theme(:secondary),
'wl-sf' => theme(:secondary_focus),
'wl-sc' => theme(:secondary_content),
'wl-a' => theme(:accent),
'wl-af' => theme(:accent_focus),
'wl-ac' => theme(:accent_content),
'wl-n' => theme(:neutral),
'wl-nf' => theme(:neutral_focus),
'wl-nc' => theme(:neutral_content),
'wl-b1' => theme(:base_100),
'wl-b2' => theme(:base_200),
'wl-b3' => theme(:base_300),
'wl-bc' => theme(:base_content),
'wl-in' => theme(:info),
'wl-su' => theme(:success),
'wl-wa' => theme(:warning),
'wl-er' => theme(:error),
'wl-rounded-btn' => theme(:rounded_btn)
}
styling_variables.each { |key, value| vars["wl-#{key}"] = value }
declarations = vars.map { |k, v| "--#{k}: #{v};" }.join(' ')
":root { #{declarations} }"
end
# =====================================================================
# Config signature (file-based only)
# =====================================================================
def enforce_config_signature?
dig_bool('security', 'enforce_config_signature', false)
end
def config_signature
config.dig('security', 'config_signature') || ''
end
def signature_payload
canonical_payload(config)
end
def generate_config_signature(secret)
raise ConfigError, 'Secret required' if secret.to_s.empty?
OpenSSL::HMAC.hexdigest('SHA256', secret, signature_payload).downcase
end
private
# =====================================================================
# Config loading
# =====================================================================
def load_config!
@mutex.synchronize do
return @config if @config # another thread beat us
if CONFIG_PATH.file?
load_from_file!
elsif Rails.env.test?
load_test_defaults!
else
load_from_api!
end
end
@config
end
def load_from_file!
raw = YAML.safe_load_file(
CONFIG_PATH,
permitted_classes: [], permitted_symbols: [], aliases: false
)
raise ConfigError, '[Whitelabel] Config must be a YAML mapping' unless raw.is_a?(Hash)
verify_file_signature!(raw)
@config = raw
@api_sourced = false
Rails.logger.info("[Whitelabel] Loaded config from file: #{CONFIG_PATH}")
rescue Psych::SyntaxError => e
raise ConfigError, "[Whitelabel] YAML parse error in #{CONFIG_PATH}: #{e.message}"
rescue Errno::EISDIR
raise ConfigError, "[Whitelabel] #{CONFIG_PATH} is a directory, not a file."
end
def load_from_api!
licence_key = ENV['INTEBEC_LICENCE_KEY'].to_s
secret_key = ENV['INTEBEC_SECRET_KEY'].to_s
if licence_key.empty? || secret_key.empty?
raise ConfigError,
'[Whitelabel] No config file found and INTEBEC_LICENCE_KEY / INTEBEC_SECRET_KEY ' \
'env vars are missing. Cannot start without a config source.'
end
@config = fetch_remote_config
@api_sourced = true
@next_refresh = Time.now.utc + REFRESH_INTERVAL
Rails.logger.info('[Whitelabel] Loaded config from Intebec Dashboard API')
end
def load_test_defaults!
@config = {}
@api_sourced = false
Rails.logger.info('[Whitelabel] Test mode — all accessors return safe fallbacks')
end
# =====================================================================
# Remote config fetch (with retry)
# =====================================================================
def fetch_remote_config
licence_key = ENV.fetch('INTEBEC_LICENCE_KEY')
secret_key = ENV.fetch('INTEBEC_SECRET_KEY')
last_error = nil
API_MAX_RETRIES.times do |attempt|
uri = URI.join(DASHBOARD_URL, CONFIG_ENDPOINT)
timestamp = Time.now.utc.to_i.to_s
nonce = SecureRandom.hex(12)
instance_id = stable_instance_id
payload = [licence_key, timestamp, nonce, instance_id].join('.')
signature = OpenSSL::HMAC.hexdigest('SHA256', secret_key, payload)
uri.query = URI.encode_www_form(licence_key: licence_key, instance_id: instance_id)
req = Net::HTTP::Get.new(uri)
req['Accept'] = 'application/json'
req['X-Licence-Key'] = licence_key
req['X-Licence-Timestamp'] = timestamp
req['X-Licence-Nonce'] = nonce
req['X-Licence-Signature'] = signature
req['X-Licence-Instance'] = instance_id
req['User-Agent'] = 'Intebec-DocuSeal'
resp = Net::HTTP.start(
uri.host, uri.port,
use_ssl: uri.scheme == 'https',
open_timeout: API_TIMEOUT,
read_timeout: API_TIMEOUT
) { |http| http.request(req) }
unless [200, 201].include?(resp.code.to_i)
raise ConfigError, "HTTP #{resp.code}"
end
parsed = JSON.parse(resp.body)
status = parsed['status'].to_s
unless %w[active trial].include?(status)
raise LicenceRevokedError, "Licence status: #{status}"
end
remote_cfg = parsed['config']
raise ConfigError, 'API returned no config payload' unless remote_cfg.is_a?(Hash)
return remote_cfg
rescue LicenceRevokedError
raise # don't retry revocations
rescue StandardError => e
last_error = e.message
delay = API_RETRY_DELAY * (2**attempt)
if attempt < API_MAX_RETRIES - 1
Rails.logger.warn(
"[Whitelabel] API attempt #{attempt + 1}/#{API_MAX_RETRIES} " \
"failed: #{e.message}, retry in #{delay}s"
)
sleep(delay)
end
end
raise ConfigError,
"[Whitelabel] Dashboard unreachable after #{API_MAX_RETRIES} attempts: #{last_error}"
end
def stable_instance_id
@stable_instance_id ||= begin
raw = [ENV.fetch('INTEBEC_LICENCE_KEY', ''), ENV.fetch('HOST', 'localhost')].join(':')
OpenSSL::Digest::SHA256.hexdigest(raw)
end
end
# =====================================================================
# File signature verification (optional, for file-based configs)
# =====================================================================
def verify_file_signature!(raw)
return unless raw.dig('security', 'enforce_config_signature') == true
secret = ENV['INTEBEC_SECRET_KEY'].to_s
raise ConfigError, '[Whitelabel] INTEBEC_SECRET_KEY required for config signature verification' if secret.empty?
expected = raw.dig('security', 'config_signature').to_s.downcase
actual = OpenSSL::HMAC.hexdigest('SHA256', secret, canonical_payload(raw)).downcase
unless expected.length == 64 && secure_compare(actual, expected)
raise ConfigError, '[Whitelabel] Config signature mismatch — refusing to boot.'
end
end
def canonical_payload(loaded)
copy = Marshal.load(Marshal.dump(loaded))
copy['security']&.delete('config_signature')
JSON.generate(deep_sort_hash(copy))
end
def deep_sort_hash(value)
case value
when Hash
value.keys.sort.each_with_object({}) { |k, h| h[k] = deep_sort_hash(value[k]) }
when Array
value.map { |v| deep_sort_hash(v) }
else
value
end
end
def secure_compare(a, b)
return false unless a.bytesize == b.bytesize
ActiveSupport::SecurityUtils.secure_compare(a, b)
end
# =====================================================================
# Helpers
# =====================================================================
def dig_bool(section, key, default = false)
value = config.dig(section, key)
value.nil? ? default : value
end
end
end

Binary file not shown.

Before

Width:  |  Height:  |  Size: 807 B

After

Width:  |  Height:  |  Size: 934 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.5 KiB

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 14 KiB

@ -0,0 +1,271 @@
/* Intébec theme overrides for DocuSeal (DaisyUI/Tailwind)
Scope: only when docuseal theme is active.
v2 Professional, restrained palette. Blue reserved for primary actions only. */
[data-theme="docuseal"] {
/* Brand used sparingly */
--ib-primary: var(--wl-ib-primary, 216 77% 52%);
--ib-primary-strong: var(--wl-ib-primary-strong, 216 77% 44%);
--ib-primary-soft: var(--wl-ib-primary-soft, 216 60% 95%);
/* Neutrals the backbone of the UI */
--ib-neutral: var(--wl-ib-neutral, 220 16% 12%);
--ib-neutral-soft: var(--wl-ib-neutral-soft, 220 12% 96%);
--ib-bg: var(--wl-ib-bg, 220 14% 98%);
--ib-surface: var(--wl-ib-surface, 0 0% 100%);
--ib-surface-2: var(--wl-ib-surface-2, 220 14% 96%);
--ib-border: var(--wl-ib-border, 220 10% 88%);
--ib-text: var(--wl-ib-text, 220 14% 10%);
--ib-text-secondary: var(--wl-ib-text-secondary, 220 8% 40%);
--ib-muted: var(--wl-ib-muted, 220 6% 55%);
/* DaisyUI theme tokens (H S L space-separated) */
--p: var(--wl-p, var(--ib-primary));
--pf: var(--wl-pf, var(--ib-primary-strong));
--pc: var(--wl-pc, 0 0% 100%);
--s: var(--wl-s, 220 12% 45%);
--sf: var(--wl-sf, 220 14% 36%);
--sc: var(--wl-sc, 0 0% 100%);
--a: var(--wl-a, 160 50% 40%);
--af: var(--wl-af, 160 50% 34%);
--ac: var(--wl-ac, 0 0% 100%);
--n: var(--wl-n, var(--ib-neutral));
--nf: var(--wl-nf, 220 16% 8%);
--nc: var(--wl-nc, 0 0% 100%);
--b1: var(--wl-b1, var(--ib-surface));
--b2: var(--wl-b2, var(--ib-surface-2));
--b3: var(--wl-b3, 220 12% 93%);
--bc: var(--wl-bc, var(--ib-text));
--in: var(--wl-in, 205 80% 50%);
--su: var(--wl-su, 154 55% 38%);
--wa: var(--wl-wa, 38 88% 48%);
--er: var(--wl-er, 0 72% 50%);
/* Radii + shadow */
--rounded-box: 0.875rem;
--rounded-btn: var(--wl-rounded-btn, 0.625rem);
--rounded-badge: 9999px;
}
/* ─── Global polish ─── */
html, body {
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
[data-theme="docuseal"] body {
background: hsl(var(--ib-bg));
color: hsl(var(--ib-text));
}
/* ─── Links: dark by default, blue only on hover ─── */
[data-theme="docuseal"] a {
color: hsl(var(--ib-text));
transition: color 0.15s ease;
}
[data-theme="docuseal"] a:hover {
color: hsl(var(--ib-primary));
}
/* Sidebar nav links — neutral dark, not blue */
[data-theme="docuseal"] aside a,
[data-theme="docuseal"] nav a,
[data-theme="docuseal"] .menu a {
color: hsl(var(--ib-text-secondary));
font-weight: 500;
}
[data-theme="docuseal"] aside a:hover,
[data-theme="docuseal"] nav a:hover,
[data-theme="docuseal"] .menu a:hover {
color: hsl(var(--ib-text));
background: hsl(var(--ib-neutral-soft));
}
/* Active sidebar item */
[data-theme="docuseal"] aside a.active,
[data-theme="docuseal"] aside a[aria-current],
[data-theme="docuseal"] .menu a.active {
color: hsl(var(--ib-primary));
font-weight: 600;
background: hsl(var(--ib-primary-soft));
}
/* ─── Navbar / header ─── */
[data-theme="docuseal"] .navbar,
[data-theme="docuseal"] header {
background: hsl(var(--ib-surface));
border-bottom: 1px solid hsl(var(--ib-border));
}
/* ─── Cards / panels ─── */
[data-theme="docuseal"] .card,
[data-theme="docuseal"] .modal-box,
[data-theme="docuseal"] .rounded-box,
[data-theme="docuseal"] .bg-base-100 {
background: hsl(var(--ib-surface));
border: 1px solid hsl(var(--ib-border));
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.04), 0 4px 12px rgba(0, 0, 0, 0.03);
}
[data-theme="docuseal"] .bg-base-200 {
background: hsl(var(--ib-surface-2));
}
/* ─── Headings ─── */
[data-theme="docuseal"] h1,
[data-theme="docuseal"] h2,
[data-theme="docuseal"] h3,
[data-theme="docuseal"] h4 {
color: hsl(var(--ib-text));
font-weight: 700;
}
/* ─── Labels & secondary text ─── */
[data-theme="docuseal"] label,
[data-theme="docuseal"] .label-text {
color: hsl(var(--ib-text-secondary));
font-weight: 500;
}
/* ─── Buttons ─── */
[data-theme="docuseal"] .btn {
border-radius: var(--rounded-btn);
font-weight: 600;
letter-spacing: 0.15px;
border-width: 1px;
transition: all 0.15s ease;
}
/* Primary the only blue button */
[data-theme="docuseal"] .btn-primary,
[data-theme="docuseal"] .base-button {
background: hsl(var(--ib-primary));
border-color: hsl(var(--ib-primary));
color: white;
box-shadow: 0 1px 3px rgba(37, 99, 235, 0.25);
}
[data-theme="docuseal"] .btn-primary:hover,
[data-theme="docuseal"] .base-button:hover {
background: hsl(var(--ib-primary-strong));
border-color: hsl(var(--ib-primary-strong));
box-shadow: 0 2px 6px rgba(37, 99, 235, 0.30);
}
/* Neutral button dark, professional */
[data-theme="docuseal"] .btn-neutral {
background: hsl(var(--ib-neutral));
border-color: hsl(var(--ib-neutral));
color: white;
}
[data-theme="docuseal"] .btn-neutral:hover {
filter: brightness(1.1);
}
/* Outline / ghost */
[data-theme="docuseal"] .btn-outline {
background: transparent;
border-color: hsl(var(--ib-border));
color: hsl(var(--ib-text));
}
[data-theme="docuseal"] .btn-outline:hover {
background: hsl(var(--ib-neutral-soft));
border-color: hsl(220 10% 80%);
color: hsl(var(--ib-text));
}
/* White button */
[data-theme="docuseal"] .white-button {
border-radius: var(--rounded-btn);
background: hsl(var(--ib-surface));
border: 1px solid hsl(var(--ib-border));
color: hsl(var(--ib-text));
font-weight: 600;
}
[data-theme="docuseal"] .white-button:hover {
background: hsl(var(--ib-surface-2));
}
/* ─── Badges ─── */
[data-theme="docuseal"] .badge {
border-radius: var(--rounded-badge);
font-weight: 600;
font-size: 0.75rem;
letter-spacing: 0.2px;
}
[data-theme="docuseal"] .badge-success {
background: hsl(154 50% 93%);
border-color: hsl(154 35% 82%);
color: hsl(154 45% 22%);
}
[data-theme="docuseal"] .badge-info {
background: hsl(205 70% 94%);
border-color: hsl(205 45% 84%);
color: hsl(205 60% 22%);
}
[data-theme="docuseal"] .badge-warning {
background: hsl(38 80% 93%);
border-color: hsl(38 55% 82%);
color: hsl(38 60% 22%);
}
[data-theme="docuseal"] .badge-error {
background: hsl(0 70% 94%);
border-color: hsl(0 45% 84%);
color: hsl(0 55% 24%);
}
/* ─── Inputs ─── */
[data-theme="docuseal"] .input,
[data-theme="docuseal"] .select,
[data-theme="docuseal"] .textarea {
border-radius: 0.625rem;
border-color: hsl(var(--ib-border));
background: hsl(var(--ib-surface));
color: hsl(var(--ib-text));
}
[data-theme="docuseal"] .input:focus,
[data-theme="docuseal"] .select:focus,
[data-theme="docuseal"] .textarea:focus,
[data-theme="docuseal"] .input:focus-visible,
[data-theme="docuseal"] .select:focus-visible,
[data-theme="docuseal"] .textarea:focus-visible {
outline: none;
border-color: hsl(var(--ib-primary));
box-shadow: 0 0 0 3px hsla(216, 77%, 52%, 0.12);
}
/* ─── Tables ─── */
[data-theme="docuseal"] .table :where(th, td) {
border-color: hsl(var(--ib-border));
color: hsl(var(--ib-text));
}
[data-theme="docuseal"] .table thead th {
background: hsl(var(--ib-surface-2));
color: hsl(var(--ib-text-secondary));
font-weight: 600;
text-transform: uppercase;
font-size: 0.75rem;
letter-spacing: 0.5px;
}
/* ─── Separators ─── */
[data-theme="docuseal"] .divider,
[data-theme="docuseal"] hr {
border-color: hsl(var(--ib-border));
}
/* ─── Dropzone ─── */
[data-theme="docuseal"] [class*="border-dashed"] {
border-color: hsl(220 10% 82%);
}
/* ─── Root fallbacks ─── */
:root {
--b1: 0 0% 100% !important;
--b2: 220 14% 96% !important;
--b3: 220 12% 93% !important;
}

@ -1,4 +1,3 @@
<svg height="180" width="180" style="color: #e0753f" viewBox="0 0 180 180" xmlns="http://www.w3.org/2000/svg">
<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"/>
<circle fill="currentColor" cx="71.927" cy="32.004" r="2.829"/>
<svg xmlns="http://www.w3.org/2000/svg" width="819" height="897" viewBox="0 0 819 897">
<path d="M 441.00 718.00 L 441.00 831.00 L 62.07 831.00 L 62.29 772.25 L 62.50 713.50 L 85.25 713.23 L 108.00 712.96 L 108.00 186.00 L 63.26 186.00 L 62.64 182.25 C61.82,177.29 61.82,78.61 62.64,72.64 L 63.27 68.00 L 226.00 68.00 L 226.25 392.75 L 226.50 717.50 L 248.93 717.81 C261.27,717.99 272.40,717.86 273.68,717.54 L 276.00 716.96 L 276.00 68.00 L 441.00 68.00 L 441.00 180.74 L 436.84 181.41 C434.55,181.78 424.53,182.04 414.59,182.00 C404.64,181.95 395.94,181.94 395.25,181.96 C394.26,181.99 394.00,202.90 394.00,283.07 L 394.00 384.15 L 450.25 383.73 C511.08,383.28 511.26,383.26 528.95,377.36 C562.24,366.27 587.29,341.03 595.99,309.83 C602.17,287.70 600.18,258.96 591.08,238.67 C577.56,208.56 547.01,189.15 504.20,183.49 C497.98,182.67 490.90,182.00 488.45,182.00 L 484.00 182.00 L 484.00 68.00 L 497.85 68.00 C529.54,68.00 565.68,72.85 590.87,80.49 C661.51,101.91 703.26,148.71 714.44,219.00 C720.04,254.19 717.89,290.64 708.58,318.48 C700.93,341.33 688.25,361.76 671.00,379.00 C658.27,391.72 650.19,397.88 634.74,406.64 C628.27,410.31 623.09,413.42 623.24,413.57 C623.38,413.71 627.10,415.25 631.50,417.00 C663.91,429.86 696.53,454.01 716.42,479.89 C744.77,516.76 757.99,557.53 757.99,608.00 C757.99,635.30 753.16,668.04 746.04,689.00 C740.09,706.53 735.88,715.82 727.50,729.91 C701.08,774.35 654.97,805.82 594.20,820.90 C567.64,827.49 547.18,829.92 511.75,830.68 L 484.00 831.29 L 484.00 718.27 L 500.75 717.58 C509.96,717.20 521.55,716.42 526.50,715.86 C592.96,708.22 632.14,672.78 637.21,615.70 C640.68,576.72 626.22,544.52 596.05,524.03 C577.26,511.27 550.10,502.25 520.93,499.07 C513.92,498.31 428.28,497.57 395.25,497.98 C394.26,498.00 394.00,520.68 394.00,608.00 L 394.00 718.00 Z" fill="rgb(77,138,225)"/>
</svg>

Before

Width:  |  Height:  |  Size: 4.7 KiB

After

Width:  |  Height:  |  Size: 1.8 KiB

@ -1,9 +1,9 @@
self.addEventListener('install', () => {
console.log('DocuSeal App installed')
console.log('App installed')
})
self.addEventListener('activate', () => {
console.log('DocuSeal App activated')
console.log('App activated')
})
self.addEventListener('fetch', (event) => {

Loading…
Cancel
Save