mirror of https://github.com/docusealco/docuseal
parent
c9f39e7b1a
commit
d3a5f36555
@ -1,19 +1,109 @@
|
|||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
class EsignSettingsController < ApplicationController
|
class EsignSettingsController < ApplicationController
|
||||||
|
DEFAULT_CERT_NAME = 'DocuSeal Self-Host Autogenerated'
|
||||||
|
|
||||||
|
CertFormRecord = Struct.new(:name, :file, :password, keyword_init: true) do
|
||||||
|
include ActiveModel::Validations
|
||||||
|
|
||||||
|
def to_key
|
||||||
|
[]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def show
|
||||||
|
cert_data = EncryptedConfig.find_by(account: current_account,
|
||||||
|
key: EncryptedConfig::ESIGN_CERTS_KEY)&.value || {}
|
||||||
|
|
||||||
|
default_pkcs = GenerateCertificate.load_pkcs(cert_data) if cert_data['cert'].present?
|
||||||
|
|
||||||
|
custom_pkcs_list = (cert_data['custom'] || []).map do |e|
|
||||||
|
{ 'pkcs' => OpenSSL::PKCS12.new(Base64.urlsafe_decode64(e['data']), e['password']),
|
||||||
|
'name' => e['name'],
|
||||||
|
'status' => e['status'] }
|
||||||
|
end
|
||||||
|
|
||||||
|
@pkcs_list = [
|
||||||
|
if default_pkcs
|
||||||
|
{
|
||||||
|
'pkcs' => default_pkcs,
|
||||||
|
'name' => DEFAULT_CERT_NAME,
|
||||||
|
'status' => custom_pkcs_list.any? { |e| e['status'] == 'default' } ? 'validate' : 'default'
|
||||||
|
}
|
||||||
|
end,
|
||||||
|
*custom_pkcs_list
|
||||||
|
].compact.reverse
|
||||||
|
end
|
||||||
|
|
||||||
|
def new
|
||||||
|
@cert_record = CertFormRecord.new
|
||||||
|
end
|
||||||
|
|
||||||
def create
|
def create
|
||||||
pdfs =
|
@cert_record = CertFormRecord.new(**cert_params)
|
||||||
params[:files].map do |file|
|
|
||||||
HexaPDF::Document.new(io: file.open)
|
cert_configs = EncryptedConfig.find_by(account: current_account, key: EncryptedConfig::ESIGN_CERTS_KEY)
|
||||||
|
|
||||||
|
if cert_configs.value['custom']&.any? { |e| e['name'] == @cert_record.name } ||
|
||||||
|
@cert_record.name == DEFAULT_CERT_NAME
|
||||||
|
|
||||||
|
@cert_record.errors.add(:name, 'already exists')
|
||||||
|
|
||||||
|
return render turbo_stream: turbo_stream.replace(:modal, template: 'esign_settings/new'),
|
||||||
|
status: :unprocessable_entity
|
||||||
end
|
end
|
||||||
|
|
||||||
certs = Accounts.load_signing_certs(current_account)
|
save_new_cert!(cert_configs, @cert_record)
|
||||||
|
|
||||||
|
redirect_to settings_esign_path, notice: 'Certificate has been successfully added!'
|
||||||
|
rescue OpenSSL::PKCS12::PKCS12Error
|
||||||
|
@cert_record.errors.add(:password, "is invalid. Make sure you're uploading a valid .p12 file")
|
||||||
|
|
||||||
|
render turbo_stream: turbo_stream.replace(:modal, template: 'esign_settings/new'), status: :unprocessable_entity
|
||||||
|
end
|
||||||
|
|
||||||
|
def update
|
||||||
|
cert_configs = EncryptedConfig.find_by(account: current_account, key: EncryptedConfig::ESIGN_CERTS_KEY)
|
||||||
|
|
||||||
|
cert_configs.value['custom'].each { |e| e['status'] = 'validate' }
|
||||||
|
custom_cert_data = cert_configs.value['custom'].find { |e| e['name'] == params[:name] }
|
||||||
|
custom_cert_data['status'] = 'default' if custom_cert_data
|
||||||
|
|
||||||
|
cert_configs.save!
|
||||||
|
|
||||||
|
redirect_to settings_esign_path, notice: 'Default certificate has been selected'
|
||||||
|
end
|
||||||
|
|
||||||
|
def destroy
|
||||||
|
cert_configs = EncryptedConfig.find_by(account: current_account, key: EncryptedConfig::ESIGN_CERTS_KEY)
|
||||||
|
|
||||||
|
cert_configs.value['custom'].reject! { |e| e['name'] == params[:name] }
|
||||||
|
|
||||||
|
cert_configs.save!
|
||||||
|
|
||||||
|
redirect_to settings_esign_path, notice: 'Certificate has been removed'
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def save_new_cert!(cert_configs, cert_record)
|
||||||
|
pkcs = OpenSSL::PKCS12.new(cert_record.file.read, cert_record.password)
|
||||||
|
|
||||||
|
cert_configs.value['custom'] ||= []
|
||||||
|
cert_configs.value['custom'].each { |e| e['status'] = 'validate' }
|
||||||
|
cert_configs.value['custom'] << {
|
||||||
|
data: Base64.urlsafe_encode64(pkcs.to_der),
|
||||||
|
password: cert_record.password,
|
||||||
|
name: cert_record.name,
|
||||||
|
status: 'default'
|
||||||
|
}
|
||||||
|
|
||||||
|
cert_configs.save!
|
||||||
|
end
|
||||||
|
|
||||||
trusted_certs = [certs[:cert], certs[:sub_ca], certs[:root_ca]]
|
def cert_params
|
||||||
|
return {} if params[:esign_settings_controller_cert_form_record].blank?
|
||||||
|
|
||||||
render turbo_stream: turbo_stream.replace('result', partial: 'result',
|
params.require(:esign_settings_controller_cert_form_record).permit(:name, :file, :password)
|
||||||
locals: { pdfs:, files: params[:files], trusted_certs: })
|
|
||||||
rescue HexaPDF::MalformedPDFError
|
|
||||||
render turbo_stream: turbo_stream.replace('result', html: helpers.tag.div('Invalid PDF', id: 'result'))
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@ -0,0 +1,29 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class VerifyPdfSignatureController < ApplicationController
|
||||||
|
def create
|
||||||
|
pdfs =
|
||||||
|
params[:files].map do |file|
|
||||||
|
HexaPDF::Document.new(io: file.open)
|
||||||
|
end
|
||||||
|
|
||||||
|
cert_data = EncryptedConfig.find_by(account: current_account,
|
||||||
|
key: EncryptedConfig::ESIGN_CERTS_KEY)&.value || {}
|
||||||
|
|
||||||
|
default_pkcs = GenerateCertificate.load_pkcs(cert_data)
|
||||||
|
|
||||||
|
custom_certs = (cert_data['custom'] || []).map do |e|
|
||||||
|
OpenSSL::PKCS12.new(Base64.urlsafe_decode64(e['data']), e['password'])
|
||||||
|
end
|
||||||
|
|
||||||
|
trusted_certs = [default_pkcs.certificate,
|
||||||
|
*default_pkcs.ca_certs,
|
||||||
|
*custom_certs.map(&:certificate),
|
||||||
|
*custom_certs.flat_map(&:ca_certs).compact]
|
||||||
|
|
||||||
|
render turbo_stream: turbo_stream.replace('result', partial: 'result',
|
||||||
|
locals: { pdfs:, files: params[:files], trusted_certs: })
|
||||||
|
rescue HexaPDF::MalformedPDFError
|
||||||
|
render turbo_stream: turbo_stream.replace('result', html: helpers.tag.div('Invalid PDF', id: 'result'))
|
||||||
|
end
|
||||||
|
end
|
||||||
@ -1,41 +0,0 @@
|
|||||||
<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">
|
|
||||||
<h1 class="text-4xl font-bold mb-4">PDF Signature</h1>
|
|
||||||
<div id="result">
|
|
||||||
<p class="mb-2">
|
|
||||||
Upload signed PDF file to validate its signature:
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<%= form_for '', url: settings_esign_index_path, method: :post, html: { enctype: 'multipart/form-data' } do |f| %>
|
|
||||||
<%= f.button type: 'submit', class: 'flex' do %>
|
|
||||||
<div class="disabled mb-3">
|
|
||||||
<%= svg_icon('loader', class: 'w-5 h-5 animate-spin inline') %>
|
|
||||||
Analyzing...
|
|
||||||
</div>
|
|
||||||
<% end %>
|
|
||||||
<file-dropzone data-is-direct-upload="false" data-name="verify_attachments" data-submit-on-upload="true" class="w-full">
|
|
||||||
<label for="file" class="w-full block h-32 relative bg-base-200 hover:bg-base-200/70 rounded-md border border-base-content border-dashed">
|
|
||||||
<div class="absolute top-0 right-0 left-0 bottom-0 flex items-center justify-center">
|
|
||||||
<div class="flex flex-col items-center">
|
|
||||||
<span data-target="file-dropzone.icon">
|
|
||||||
<%= svg_icon('cloud_upload', class: 'w-10 h-10') %>
|
|
||||||
</span>
|
|
||||||
<span data-target="file-dropzone.loading" class="hidden">
|
|
||||||
<%= svg_icon('loader', class: 'w-10 h-10 animate-spin') %>
|
|
||||||
</span>
|
|
||||||
<div class="font-medium mb-1">
|
|
||||||
Verify Signed PDF
|
|
||||||
</div>
|
|
||||||
<div class="text-xs">
|
|
||||||
<span class="font-medium">Click to upload</span> or drag and drop files
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<input id="file" name="files[]" class="hidden" data-action="change:file-dropzone#onSelectFiles" data-target="file-dropzone.input" type="file" accept="application/pdf" multiple>
|
|
||||||
</div>
|
|
||||||
</label>
|
|
||||||
</file-dropzone>
|
|
||||||
<% end %>
|
|
||||||
</div>
|
|
||||||
<div class="w-0 md:w-52"></div>
|
|
||||||
</div>
|
|
||||||
@ -0,0 +1,24 @@
|
|||||||
|
<%= render 'shared/turbo_modal', title: 'Upload Certificate' do %>
|
||||||
|
<%= form_for @cert_record, url: settings_esign_path, html: { class: 'space-y-4', enctype: 'multipart/form-data' }, data: { turbo_frame: :_top } do |f| %>
|
||||||
|
<div class="space-y-2">
|
||||||
|
<div class="form-control">
|
||||||
|
<%= f.label :name, class: 'label' %>
|
||||||
|
<%= f.text_field :name, required: true, class: 'base-input' %>
|
||||||
|
</div>
|
||||||
|
<div class="form-control">
|
||||||
|
<%= f.label :file, class: 'label' %>
|
||||||
|
<%= f.file_field :file, required: true %>
|
||||||
|
<label class="label">
|
||||||
|
<span class="label-text-alt">Use a valid .der, .p12 or .pfx file.</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div class="form-control">
|
||||||
|
<%= f.label :password, 'Password (optional)', class: 'label' %>
|
||||||
|
<%= f.text_field :password, class: 'base-input' %>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="form-control pt-2">
|
||||||
|
<%= f.button button_title, class: 'base-button' %>
|
||||||
|
</div>
|
||||||
|
<% end %>
|
||||||
|
<% end %>
|
||||||
@ -0,0 +1,99 @@
|
|||||||
|
<div class="flex flex-wrap space-y-4 md:flex-nowrap md:space-y-0 md:space-x-10">
|
||||||
|
<%= render 'shared/settings_nav' %>
|
||||||
|
<div class="flex-grow">
|
||||||
|
<div class="max-w-xl">
|
||||||
|
<h1 class="text-4xl font-bold mb-4">PDF Signature</h1>
|
||||||
|
<div id="result">
|
||||||
|
<p class="mb-2">
|
||||||
|
Upload signed PDF file to validate its signature:
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<%= form_for '', url: verify_pdf_signature_index_path, method: :post, html: { enctype: 'multipart/form-data' } do |f| %>
|
||||||
|
<%= f.button type: 'submit', class: 'flex' do %>
|
||||||
|
<div class="disabled mb-3">
|
||||||
|
<%= svg_icon('loader', class: 'w-5 h-5 animate-spin inline') %>
|
||||||
|
Analyzing...
|
||||||
|
</div>
|
||||||
|
<% end %>
|
||||||
|
<file-dropzone data-is-direct-upload="false" data-name="verify_attachments" data-submit-on-upload="true" class="w-full">
|
||||||
|
<label for="file" class="w-full block h-32 relative bg-base-200 hover:bg-base-200/70 rounded-md border border-base-content border-dashed">
|
||||||
|
<div class="absolute top-0 right-0 left-0 bottom-0 flex items-center justify-center">
|
||||||
|
<div class="flex flex-col items-center">
|
||||||
|
<span data-target="file-dropzone.icon">
|
||||||
|
<%= svg_icon('cloud_upload', class: 'w-10 h-10') %>
|
||||||
|
</span>
|
||||||
|
<span data-target="file-dropzone.loading" class="hidden">
|
||||||
|
<%= svg_icon('loader', class: 'w-10 h-10 animate-spin') %>
|
||||||
|
</span>
|
||||||
|
<div class="font-medium mb-1">
|
||||||
|
Verify Signed PDF
|
||||||
|
</div>
|
||||||
|
<div class="text-xs">
|
||||||
|
<span class="font-medium">Click to upload</span> or drag and drop files
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<input id="file" name="files[]" class="hidden" data-action="change:file-dropzone#onSelectFiles" data-target="file-dropzone.input" type="file" accept="application/pdf" multiple>
|
||||||
|
</div>
|
||||||
|
</label>
|
||||||
|
</file-dropzone>
|
||||||
|
<% end %>
|
||||||
|
</div>
|
||||||
|
<div class="flex justify-between items-end mb-4 mt-8">
|
||||||
|
<h2 class="text-3xl font-bold">Signing Certificates</h2>
|
||||||
|
<%= link_to new_settings_esign_path, class: 'btn btn-primary btn-md', data: { turbo_frame: 'modal' } do %>
|
||||||
|
<%= svg_icon('plus', class: 'w-6 h-6') %>
|
||||||
|
<span>Upload Cert</span>
|
||||||
|
<% end %>
|
||||||
|
</div>
|
||||||
|
<%= render 'alert' %>
|
||||||
|
<div class="overflow-x-auto">
|
||||||
|
<table class="table w-full table-lg rounded-b-none overflow-hidden">
|
||||||
|
<thead class="bg-base-200">
|
||||||
|
<tr class="text-neutral uppercase">
|
||||||
|
<th>
|
||||||
|
Name
|
||||||
|
</th>
|
||||||
|
<th>
|
||||||
|
Valid To
|
||||||
|
</th>
|
||||||
|
<th>
|
||||||
|
Status
|
||||||
|
</th>
|
||||||
|
<th class="text-right" width="1px">
|
||||||
|
</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<% @pkcs_list.each do |item| %>
|
||||||
|
<tr scope="row" class="group">
|
||||||
|
<td>
|
||||||
|
<%= item['name'] %>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<%= l(item['pkcs'].certificate.not_after.to_date, format: :long, locale: current_account.locale) %>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<% if item['status'] == 'default' %>
|
||||||
|
<span class="badge badge-lg badge-info badge-outline">
|
||||||
|
<%= item['status'] %>
|
||||||
|
</span>
|
||||||
|
<% else %>
|
||||||
|
<%= button_to settings_esign_path, method: :put, params: { name: item['name'] }, class: 'btn btn-outline btn-neutral btn-xs whitespace-nowrap', title: 'Delete', data: { turbo_confirm: 'Are you sure?' } do %>
|
||||||
|
Make Default
|
||||||
|
<% end %>
|
||||||
|
<% end %>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<% if item['name'] != EsignSettingsController::DEFAULT_CERT_NAME && item['status'] != 'default' %>
|
||||||
|
<%= button_to settings_esign_path, params: { name: item['name'] }, method: :delete, class: 'btn btn-outline btn-error btn-xs', title: 'Delete', data: { turbo_confirm: 'Are you sure?' } do %>
|
||||||
|
Remove
|
||||||
|
<% end %>
|
||||||
|
<% end %>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<% end %>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
Loading…
Reference in new issue