mirror of https://github.com/docusealco/docuseal
Lets the EnWella EHR mount <docuseal-builder> (and -form) without a
paid Pro license. Upstream's `EmbedScriptsController` returns an
"Upgrade to Pro" stub; we override it via the static-asset path so the
real shim wins without touching the controller.
JS shim (public/js/builder.js)
- Web component that decodes the JWT minted by the embedder (`name`,
`document_urls`, `external_id`, optional `template_id`).
- Iframes /templates/:id/edit for existing templates, /new?... for the
upload-from-URL flow.
- Relays a `save` postMessage from the inner builder up as a DOM
`CustomEvent('save')` on the outer element so @docuseal/react's
onSave callback fires.
- Discovers host from its own <script src> so embedders don't have to
set data-host. JWT signature not re-verified (session cookies +
shared HMAC at the calling app are authoritative).
Builder (app/javascript/template_builder/builder.vue)
- isEmbedded computed (window.parent !== window).
- Hide #title_container in embed (sheet already shows the consent name).
- Fire the `save` postMessage at the end of the manual SAVE handler so
the outer shim can dispatch the DOM event.
Rails glue
- application_controller.rb / config/application.rb: when
EMBED_ALLOWED_ORIGIN is set, add `frame-ancestors 'self' <origin>`
to the per-request CSP and drop the default `X-Frame-Options:
SAMEORIGIN` header (different scheme/host/port = different origin,
so SAMEORIGIN blocks the EHR's iframe even in same-domain prod).
- templates_uploads_controller.rb: persist `external_id` from the
upload params so the host app can later look the template up via
`GET /api/templates?external_id=...` (deterministic fallback link
path when the `template.created` webhook doesn't reach us).
- templates_uploads_controller.rb: skip the SSRF guard when fetching
documents in development — it rejects http / non-443 / localhost,
which is exactly the Active Storage URLs the EHR sends from
https://localhost:3000.
- templates_uploads/show.html.erb: carry `external_id` through the
resubmit form (used on the encrypted-PDF prompt path).
Local HTTPS dev (puma.rb, Procfile.dev, .env.example)
- Bind ssl://0.0.0.0:PORT when LOCAL_HTTPS_CERT/KEY are set (wired by
../bin/docuseal-dev to ../.certs/lvh.me.pem). EHR runs HTTPS too,
so HTTPS iframe avoids mixed-content blocking.
- Procfile.dev runs `puma -C config/puma.rb` directly so the ssl bind
applies.
- SSL_CERT_FILE example for trusting the mkcert root so DocuSeal's
outbound HTTPS to Rails (Active Storage downloads) verifies.
pull/697/head
parent
4f50df8f76
commit
f4324122cc
@ -1,2 +1,2 @@
|
||||
web: PORT=${PORT:-3000} bundle exec rails s -p ${PORT:-3000}
|
||||
web: PORT=${PORT:-3000} bundle exec puma -C config/puma.rb
|
||||
webpacker: bundle exec ./bin/shakapacker-dev-server
|
||||
|
||||
@ -0,0 +1,122 @@
|
||||
// Self-hosted embed shim for the <docuseal-builder> web component.
|
||||
// Upstream `EmbedScriptsController#show` returns an "Upgrade to Pro" stub;
|
||||
// the Rails static middleware serves this file ahead of that route, so the
|
||||
// real shim wins without touching the controller.
|
||||
//
|
||||
// What this does:
|
||||
// 1. Reads the JWT minted by the embedding app from `data-token`.
|
||||
// 2. Decodes its payload to discover `document_urls`, `name`,
|
||||
// `external_id`, and `template_id`.
|
||||
// 3. Iframes either `/templates/:id/edit` (existing template) or
|
||||
// `/new?url=...&filename=...&external_id=...` (fresh upload flow).
|
||||
// 4. Relays the inner builder's `save` postMessage up as a DOM
|
||||
// `CustomEvent('save')` on the outer element so `@docuseal/react`'s
|
||||
// `onSave` callback fires.
|
||||
//
|
||||
// JWT signature is NOT verified here — host DocuSeal session cookies carry
|
||||
// auth, and the calling app signs with the shared secret it controls.
|
||||
(function () {
|
||||
function decodeJwtPayload(token) {
|
||||
try {
|
||||
var part = token.split('.')[1]
|
||||
if (!part) return null
|
||||
var b64 = part.replace(/-/g, '+').replace(/_/g, '/')
|
||||
var pad = b64.length % 4
|
||||
if (pad) b64 += '='.repeat(4 - pad)
|
||||
return JSON.parse(decodeURIComponent(escape(atob(b64))))
|
||||
} catch (e) {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
// Discover the DocuSeal host from this script's own src so embedders
|
||||
// don't have to set data-host on every element.
|
||||
var SCRIPT_HOST = (function () {
|
||||
try {
|
||||
var current = document.currentScript
|
||||
var src = current ? current.src : ''
|
||||
if (!src) {
|
||||
var all = document.getElementsByTagName('script')
|
||||
for (var i = 0; i < all.length; i++) {
|
||||
if (/\/js\/(builder|form)\.js/.test(all[i].src)) {
|
||||
src = all[i].src
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
return src ? new URL(src).host : ''
|
||||
} catch (e) {
|
||||
return ''
|
||||
}
|
||||
})()
|
||||
|
||||
// Relay `save` postMessage from the iframe (templates_builder fires it
|
||||
// after a successful manual SAVE) up as a DOM `save` CustomEvent on the
|
||||
// outer <docuseal-builder> element so React listeners can close the
|
||||
// embedding sheet.
|
||||
if (!window.__docusealEmbedListenerInstalled) {
|
||||
window.__docusealEmbedListenerInstalled = true
|
||||
window.addEventListener('message', function (ev) {
|
||||
var d = ev && ev.data
|
||||
if (!d || d.source !== 'docuseal-embed' || d.type !== 'save') return
|
||||
document.querySelectorAll('docuseal-builder, docuseal-form').forEach(function (el) {
|
||||
el.dispatchEvent(new CustomEvent('save', { detail: { template_id: d.template_id } }))
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
var EmbedBuilder = class extends HTMLElement {
|
||||
static get observedAttributes() {
|
||||
return ['data-token', 'data-host']
|
||||
}
|
||||
connectedCallback() {
|
||||
this._maybeMount()
|
||||
}
|
||||
attributeChangedCallback() {
|
||||
this._maybeMount()
|
||||
}
|
||||
_maybeMount() {
|
||||
if (this._mounted) return
|
||||
var token = this.getAttribute('data-token') || ''
|
||||
if (!token) return
|
||||
this._mounted = true
|
||||
var host = this.getAttribute('data-host') || SCRIPT_HOST || window.location.host
|
||||
var payload = decodeJwtPayload(token) || {}
|
||||
var docUrl = (payload.document_urls && payload.document_urls[0]) || ''
|
||||
var name = payload.name || 'Untitled'
|
||||
var filename = name.replace(/[^A-Za-z0-9_\-]+/g, '_') + '.pdf'
|
||||
|
||||
// Match parent page scheme. Both ends run TLS locally (mkcert), so
|
||||
// https parent -> https iframe with no mixed-content blocking.
|
||||
var origin = window.location.protocol + '//' + host
|
||||
var src
|
||||
if (payload.template_id) {
|
||||
// Existing template: open the editor directly so prior field
|
||||
// edits are preserved. Posting to /new would create another
|
||||
// template and orphan the original.
|
||||
src = origin + '/templates/' + encodeURIComponent(payload.template_id) + '/edit'
|
||||
} else {
|
||||
var qs = new URLSearchParams()
|
||||
if (docUrl) qs.set('url', docUrl)
|
||||
qs.set('filename', filename)
|
||||
if (payload.external_id) qs.set('external_id', payload.external_id)
|
||||
src = origin + '/new?' + qs.toString()
|
||||
}
|
||||
var iframe = document.createElement('iframe')
|
||||
iframe.src = src
|
||||
iframe.style.cssText = 'width:100%;height:100%;min-height:600px;border:0;display:block'
|
||||
iframe.setAttribute('allow', 'clipboard-write')
|
||||
this.style.display = 'block'
|
||||
this.style.height = this.style.height || '100%'
|
||||
this.appendChild(iframe)
|
||||
}
|
||||
}
|
||||
|
||||
if (!window.customElements.get('docuseal-builder')) {
|
||||
window.customElements.define('docuseal-builder', EmbedBuilder)
|
||||
}
|
||||
|
||||
if (!window.customElements.get('docuseal-form')) {
|
||||
window.customElements.define('docuseal-form', class extends EmbedBuilder {})
|
||||
}
|
||||
})()
|
||||
Loading…
Reference in new issue