add signature verification
@@ -12,7 +12,7 @@ module Api
|
|||||||
attachment = ActiveStorage::Attachment.create!(
|
attachment = ActiveStorage::Attachment.create!(
|
||||||
blob:,
|
blob:,
|
||||||
name: params[:name],
|
name: params[:name],
|
||||||
record: submitter || current_account
|
record: submitter
|
||||||
)
|
)
|
||||||
|
|
||||||
render json: attachment.as_json(only: %i[uuid], methods: %i[url filename content_type])
|
render json: attachment.as_json(only: %i[uuid], methods: %i[url filename content_type])
|
||||||
|
|||||||
@@ -1,26 +1,25 @@
|
|||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
class EsignSettingsController < ApplicationController
|
class EsignSettingsController < ApplicationController
|
||||||
before_action :load_encrypted_config
|
|
||||||
|
|
||||||
def create
|
def create
|
||||||
attachment = ActiveStorage::Attachment.find_by!(uuid: params[:attachment_uuid])
|
blobs =
|
||||||
|
params[:blob_signed_ids].map do |sid|
|
||||||
|
ActiveStorage::Blob.find_signed(sid)
|
||||||
|
end
|
||||||
|
|
||||||
pdf = HexaPDF::Document.new(io: StringIO.new(attachment.download))
|
pdfs =
|
||||||
|
blobs.map do |blob|
|
||||||
|
HexaPDF::Document.new(io: StringIO.new(blob.download))
|
||||||
|
end
|
||||||
|
|
||||||
pdf.signatures
|
cert = EncryptedConfig.find_by(account: current_account, key: EncryptedConfig::ESIGN_CERTS_KEY).value
|
||||||
end
|
|
||||||
|
|
||||||
private
|
trusted_certs = [OpenSSL::X509::Certificate.new(cert['cert']),
|
||||||
|
OpenSSL::X509::Certificate.new(cert['sub_ca']),
|
||||||
|
OpenSSL::X509::Certificate.new(cert['root_ca'])]
|
||||||
|
|
||||||
def load_encrypted_config
|
render turbo_stream: turbo_stream.replace('result', partial: 'result', locals: { pdfs:, blobs:, trusted_certs: })
|
||||||
@encrypted_config =
|
rescue HexaPDF::MalformedPDFError
|
||||||
EncryptedConfig.find_or_initialize_by(account: current_account, key: EncryptedConfig::ESIGN_CERTS_KEY)
|
render turbo_stream: turbo_stream.replace('result', html: helpers.tag.div('Invalid PDF', id: 'result'))
|
||||||
end
|
|
||||||
|
|
||||||
def storage_configs
|
|
||||||
params.require(:encrypted_config).permit(value: {}).tap do |e|
|
|
||||||
e[:value].compact_blank!
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -10,6 +10,8 @@ class SubmitFormController < ApplicationController
|
|||||||
Submitter.preload(submission: { template: { documents_attachments: { preview_images_attachments: :blob } } })
|
Submitter.preload(submission: { template: { documents_attachments: { preview_images_attachments: :blob } } })
|
||||||
.find_by!(slug: params[:slug])
|
.find_by!(slug: params[:slug])
|
||||||
|
|
||||||
|
cookies.signed[:submitter_sid] = @submitter.signed_id
|
||||||
|
|
||||||
redirect_to submit_form_completed_path(@submitter.slug) if @submitter.completed_at?
|
redirect_to submit_form_completed_path(@submitter.slug) if @submitter.completed_at?
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -18,7 +20,7 @@ class SubmitFormController < ApplicationController
|
|||||||
submitter.values.merge!(normalized_values)
|
submitter.values.merge!(normalized_values)
|
||||||
submitter.completed_at = Time.current if params[:completed] == 'true'
|
submitter.completed_at = Time.current if params[:completed] == 'true'
|
||||||
|
|
||||||
submitter.save
|
submitter.save!
|
||||||
|
|
||||||
head :ok
|
head :ok
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -6,8 +6,8 @@ import { target, targetable } from '@github/catalyst/lib/targetable'
|
|||||||
export default actionable(targetable(class extends HTMLElement {
|
export default actionable(targetable(class extends HTMLElement {
|
||||||
static [target.static] = [
|
static [target.static] = [
|
||||||
'loading',
|
'loading',
|
||||||
'input',
|
'icon',
|
||||||
'valueField'
|
'input'
|
||||||
]
|
]
|
||||||
|
|
||||||
connectedCallback () {
|
connectedCallback () {
|
||||||
@@ -30,8 +30,16 @@ export default actionable(targetable(class extends HTMLElement {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
toggleLoading () {
|
||||||
|
this.loading.classList.toggle('hidden')
|
||||||
|
this.icon.classList.toggle('hidden')
|
||||||
|
this.classList.toggle('opacity-50')
|
||||||
|
}
|
||||||
|
|
||||||
async uploadFiles (files) {
|
async uploadFiles (files) {
|
||||||
const blobs = await Promise.all(
|
this.toggleLoading()
|
||||||
|
|
||||||
|
await Promise.all(
|
||||||
Array.from(files).map(async (file) => {
|
Array.from(files).map(async (file) => {
|
||||||
const upload = new DirectUpload(
|
const upload = new DirectUpload(
|
||||||
file,
|
file,
|
||||||
@@ -53,29 +61,26 @@ export default actionable(targetable(class extends HTMLElement {
|
|||||||
console.error(error)
|
console.error(error)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
)
|
).then((blobs) => {
|
||||||
|
if (this.dataset.submitOnUpload) {
|
||||||
|
this.querySelectorAll('[name="blob_signed_ids[]"]').forEach((e) => e.remove())
|
||||||
|
}
|
||||||
|
|
||||||
await Promise.all(
|
blobs.forEach((blob) => {
|
||||||
blobs.map((blob) => {
|
const input = document.createElement('input')
|
||||||
return fetch('/api/attachments', {
|
|
||||||
method: 'POST',
|
|
||||||
body: JSON.stringify({
|
|
||||||
name: this.dataset.name,
|
|
||||||
blob_signed_id: blob.signed_id,
|
|
||||||
submitter_slug: this.dataset.submitterSlug
|
|
||||||
}),
|
|
||||||
headers: { 'Content-Type': 'application/json' }
|
|
||||||
}).then(resp => resp.json()).then((data) => {
|
|
||||||
return data
|
|
||||||
})
|
|
||||||
})).then((result) => {
|
|
||||||
result.forEach((attachment) => {
|
|
||||||
if (this.valueField) {
|
|
||||||
this.valueField.value = attachment.uuid
|
|
||||||
}
|
|
||||||
|
|
||||||
this.dispatchEvent(new CustomEvent('upload', { detail: attachment }))
|
input.type = 'hidden'
|
||||||
|
input.name = 'blob_signed_ids[]'
|
||||||
|
input.value = blob.signed_id
|
||||||
|
|
||||||
|
this.append(input)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
if (this.dataset.submitOnUpload) {
|
||||||
|
this.closest('form').querySelector('button[type="submit"]').click()
|
||||||
|
}
|
||||||
|
}).finally(() => {
|
||||||
|
this.toggleLoading()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}))
|
}))
|
||||||
|
|||||||
@@ -7,10 +7,18 @@
|
|||||||
<label
|
<label
|
||||||
:for="inputId"
|
:for="inputId"
|
||||||
class="w-full relative bg-base-300 hover:bg-base-200 rounded-md border border-base-content border-dashed"
|
class="w-full relative bg-base-300 hover:bg-base-200 rounded-md border border-base-content border-dashed"
|
||||||
|
:class="{ 'opacity-50': isLoading }"
|
||||||
>
|
>
|
||||||
<div class="absolute top-0 right-0 left-0 bottom-0 flex items-center justify-center">
|
<div class="absolute top-0 right-0 left-0 bottom-0 flex items-center justify-center">
|
||||||
<div class="flex flex-col items-center">
|
<div class="flex flex-col items-center">
|
||||||
|
<IconInnerShadowTop
|
||||||
|
v-if="isLoading"
|
||||||
|
class="animate-spin"
|
||||||
|
:width="30"
|
||||||
|
:height="30"
|
||||||
|
/>
|
||||||
<IconCloudUpload
|
<IconCloudUpload
|
||||||
|
v-else
|
||||||
:width="30"
|
:width="30"
|
||||||
:height="30"
|
:height="30"
|
||||||
/>
|
/>
|
||||||
@@ -40,12 +48,13 @@
|
|||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { DirectUpload } from '@rails/activestorage'
|
import { DirectUpload } from '@rails/activestorage'
|
||||||
import { IconCloudUpload } from '@tabler/icons-vue'
|
import { IconCloudUpload, IconInnerShadowTop } from '@tabler/icons-vue'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'FileDropzone',
|
name: 'FileDropzone',
|
||||||
components: {
|
components: {
|
||||||
IconCloudUpload
|
IconCloudUpload,
|
||||||
|
IconInnerShadowTop
|
||||||
},
|
},
|
||||||
props: {
|
props: {
|
||||||
message: {
|
message: {
|
||||||
@@ -68,6 +77,11 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
emits: ['upload'],
|
emits: ['upload'],
|
||||||
|
data () {
|
||||||
|
return {
|
||||||
|
isLoading: false
|
||||||
|
}
|
||||||
|
},
|
||||||
computed: {
|
computed: {
|
||||||
inputId () {
|
inputId () {
|
||||||
return 'el' + Math.random().toString(32).split('.')[1]
|
return 'el' + Math.random().toString(32).split('.')[1]
|
||||||
@@ -87,6 +101,8 @@ export default {
|
|||||||
})
|
})
|
||||||
},
|
},
|
||||||
async uploadFiles (files) {
|
async uploadFiles (files) {
|
||||||
|
this.isLoading = true
|
||||||
|
|
||||||
const blobs = await Promise.all(
|
const blobs = await Promise.all(
|
||||||
Array.from(files).map(async (file) => {
|
Array.from(files).map(async (file) => {
|
||||||
const upload = new DirectUpload(
|
const upload = new DirectUpload(
|
||||||
@@ -126,6 +142,8 @@ export default {
|
|||||||
})
|
})
|
||||||
})).then((result) => {
|
})).then((result) => {
|
||||||
this.$emit('upload', result)
|
this.$emit('upload', result)
|
||||||
|
}).finally(() => {
|
||||||
|
this.isLoading = false
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,68 @@
|
|||||||
|
<div id="result">
|
||||||
|
<% blobs.zip(pdfs).each do |blob, pdf| %>
|
||||||
|
<div class="mb-4 border border-base-300 rounded-md py-2 px-3">
|
||||||
|
<% if pdf.signatures.to_a.size == 0 %>
|
||||||
|
<div class="text-sm">
|
||||||
|
<%= blob.filename %>
|
||||||
|
</div>
|
||||||
|
<p class="text-xl font-medium">
|
||||||
|
There are no signatures...
|
||||||
|
</p>
|
||||||
|
<% else %>
|
||||||
|
<div class="flex items-center space-x-1 border-b border-dashed border-base-300 pb-2">
|
||||||
|
<%= svg_icon('file_text', class: 'w-5 h-5 inline') %>
|
||||||
|
<span><%= blob.filename %> - <%= pluralize(pdf.signatures.to_a.size, 'Signature') %></span>
|
||||||
|
</div>
|
||||||
|
<% pdf.signatures.to_a.each do |signature| %>
|
||||||
|
<div class="mt-3">
|
||||||
|
<div class="space-y-1 font-medium pb-2 text-xl">
|
||||||
|
<% signature.verify(trusted_certs:).messages.map(&:content).each do |message| %>
|
||||||
|
<p class="flex space-x-1 items-center">
|
||||||
|
<% if message == 'Signature verification failed' %>
|
||||||
|
<%= svg_icon('x_circle', class: 'w-6 h-6 text-red-500') %>
|
||||||
|
<% elsif message == 'Signature valid' %>
|
||||||
|
<%= svg_icon('circle_check', class: 'w-6 h-6 text-green-500') %>
|
||||||
|
<% end %>
|
||||||
|
<span>
|
||||||
|
<%= message %>
|
||||||
|
</span>
|
||||||
|
</p>
|
||||||
|
<% if message == 'Signature valid' %>
|
||||||
|
<p class="flex space-x-1 items-center">
|
||||||
|
<% if signature.signature_handler.signer_certificate.public_key.to_der == trusted_certs.first.public_key.to_der %>
|
||||||
|
<%= svg_icon('circle_check', class: 'w-6 h-6 text-green-500') %>
|
||||||
|
<span>
|
||||||
|
Signed with DocuSeal Certificate
|
||||||
|
</span>
|
||||||
|
<% else %>
|
||||||
|
<%= svg_icon('x_circle', class: 'w-6 h-6 text-red-500') %>
|
||||||
|
<span>
|
||||||
|
Signed with External Certificate
|
||||||
|
</span>
|
||||||
|
<% end %>
|
||||||
|
</p>
|
||||||
|
<% end %>
|
||||||
|
<% end %>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center space-x-1">
|
||||||
|
<%= svg_icon('user', class: 'w-5 h-5 inline') %>
|
||||||
|
<span><%= signature.signing_reason %></span>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center space-x-1">
|
||||||
|
<%= svg_icon('calendar', class: 'w-5 h-5 inline') %>
|
||||||
|
<span><%= l(signature.signing_time, format: :long) %></span>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center space-x-1">
|
||||||
|
<%= svg_icon('certificate', class: 'w-5 h-5 inline') %>
|
||||||
|
<span><%= signature.signer_name %></span>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center space-x-1">
|
||||||
|
<%= svg_icon('lock_access', class: 'w-5 h-5 inline') %>
|
||||||
|
<span><%= signature.signature_type %></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<% end %>
|
||||||
|
<% end %>
|
||||||
|
</div>
|
||||||
|
<% end %>
|
||||||
|
</div>
|
||||||
@@ -1,19 +1,40 @@
|
|||||||
<div class="flex flex-wrap space-y-4 md:flex-nowrap md:space-y-0">
|
<div class="flex flex-wrap space-y-4 md:flex-nowrap md:space-y-0">
|
||||||
<%= render 'shared/settings_nav' %>
|
<%= render 'shared/settings_nav' %>
|
||||||
<div class="flex-grow max-w-xl mx-auto">
|
<div class="flex-grow max-w-xl mx-auto">
|
||||||
<h1 class="text-4xl font-bold mb-4">eSign</h1>
|
<h1 class="text-4xl font-bold mb-4">Verify PDF Signature</h1>
|
||||||
<p>
|
<div id="result">
|
||||||
Upload your electronic signature
|
<p class="mb-2">
|
||||||
</p>
|
Upload signed PDF file to validate its signature:
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
<%= form_for '', url: settings_esign_index_path, method: :post do |f| %>
|
<%= form_for '', url: settings_esign_index_path, method: :post do |f| %>
|
||||||
<file-dropzone data-name="verify_attachments" class="">
|
<%= f.button type: 'submit', class: 'flex' do %>
|
||||||
<label for="file">
|
<div class="disabled mb-3">
|
||||||
<input id="attachment_uuid" name="attachment_uuid" class="hidden" data-target="file-dropzone.valueField" type="text" autocomplete="off">
|
<%= svg_icon('loader', class: 'w-5 h-5 animate-spin inline') %>
|
||||||
<input id="file" class="hidden" data-action="change:file-dropzone#onSelectFiles" data-target="file-dropzone.input" type="file">
|
Analyzing...
|
||||||
Cick to upload
|
</div>
|
||||||
|
<% end %>
|
||||||
|
<file-dropzone data-name="verify_attachments" data-submit-on-upload="true" class="w-full">
|
||||||
|
<label for="file" class="w-full block h-32 relative bg-base-300 hover:bg-base-200 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
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<input id="file" class="hidden" data-action="change:file-dropzone#onSelectFiles" data-target="file-dropzone.input" type="file" accept="application/pdf" multiple>
|
||||||
|
</div>
|
||||||
</label>
|
</label>
|
||||||
</file-dropzone>
|
</file-dropzone>
|
||||||
<%= f.button button_title(title: 'Save', disabled_with: 'Saving') %>
|
|
||||||
<% end %>
|
<% end %>
|
||||||
</div>
|
</div>
|
||||||
<div class="w-0 md:w-52"></div>
|
<div class="w-0 md:w-52"></div>
|
||||||
|
|||||||
@@ -0,0 +1,9 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" class="<%= local_assigns[:class] %>" width="44" height="44" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
|
||||||
|
<path d="M15 15m-3 0a3 3 0 1 0 6 0a3 3 0 1 0 -6 0"></path>
|
||||||
|
<path d="M13 17.5v4.5l2 -1.5l2 1.5v-4.5"></path>
|
||||||
|
<path d="M10 19h-5a2 2 0 0 1 -2 -2v-10c0 -1.1 .9 -2 2 -2h14a2 2 0 0 1 2 2v10a2 2 0 0 1 -1 1.73"></path>
|
||||||
|
<path d="M6 9l12 0"></path>
|
||||||
|
<path d="M6 12l3 0"></path>
|
||||||
|
<path d="M6 15l2 0"></path>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 603 B |
@@ -0,0 +1,5 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" class="<%= local_assigns[:class] %>" width="44" height="44" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
|
||||||
|
<path d="M12 12m-9 0a9 9 0 1 0 18 0a9 9 0 1 0 -18 0"></path>
|
||||||
|
<path d="M9 12l2 2l4 -4"></path>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 389 B |
@@ -0,0 +1,6 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" class="<%= local_assigns[:class] %>" width="44" height="44" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
|
||||||
|
<path d="M7 18a4.6 4.4 0 0 1 0 -9a5 4.5 0 0 1 11 2h1a3.5 3.5 0 0 1 0 7h-1"></path>
|
||||||
|
<path d="M9 15l3 -3l3 3"></path>
|
||||||
|
<path d="M12 12l0 9"></path>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 443 B |
@@ -0,0 +1,8 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" class="<%= local_assigns[:class] %>" width="44" height="44" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
|
||||||
|
<path d="M14 3v4a1 1 0 0 0 1 1h4"></path>
|
||||||
|
<path d="M17 21h-10a2 2 0 0 1 -2 -2v-14a2 2 0 0 1 2 -2h7l5 5v11a2 2 0 0 1 -2 2z"></path>
|
||||||
|
<path d="M9 9l1 0"></path>
|
||||||
|
<path d="M9 13l6 0"></path>
|
||||||
|
<path d="M9 17l6 0"></path>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 518 B |
@@ -0,0 +1,9 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" class="<%= local_assigns[:class] %>" width="44" height="44" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
|
||||||
|
<path d="M4 8v-2a2 2 0 0 1 2 -2h2"></path>
|
||||||
|
<path d="M4 16v2a2 2 0 0 0 2 2h2"></path>
|
||||||
|
<path d="M16 4h2a2 2 0 0 1 2 2v2"></path>
|
||||||
|
<path d="M16 20h2a2 2 0 0 0 2 -2v-2"></path>
|
||||||
|
<path d="M8 11m0 1a1 1 0 0 1 1 -1h6a1 1 0 0 1 1 1v3a1 1 0 0 1 -1 1h-6a1 1 0 0 1 -1 -1z"></path>
|
||||||
|
<path d="M10 11v-2a2 2 0 1 1 4 0v2"></path>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 619 B |
@@ -0,0 +1,5 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" class="<%= local_assigns[:class] %>" width="44" height="44" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
|
||||||
|
<path d="M8 7a4 4 0 1 0 8 0a4 4 0 0 0 -8 0"></path>
|
||||||
|
<path d="M6 21v-2a4 4 0 0 1 4 -4h4a4 4 0 0 1 4 4v2"></path>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 407 B |
@@ -1 +1,5 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" class="<%= local_assigns[:class] %>" fill="none" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z" /></svg>
|
<svg xmlns="http://www.w3.org/2000/svg" class="<%= local_assigns[:class] %>" width="44" height="44" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
|
||||||
|
<path d="M12 12m-9 0a9 9 0 1 0 18 0a9 9 0 1 0 -18 0"></path>
|
||||||
|
<path d="M10 10l4 4m0 -4l-4 4"></path>
|
||||||
|
</svg>
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 261 B After Width: | Height: | Size: 395 B |
@@ -11,6 +11,13 @@ Rails.configuration.to_prepare do
|
|||||||
response.set_header('Cache-Control', 'public, max-age=31536000') if action_name == 'show'
|
response.set_header('Cache-Control', 'public, max-age=31536000') if action_name == 'show'
|
||||||
end
|
end
|
||||||
|
|
||||||
|
ActiveStorage::DirectUploadsController.before_action do
|
||||||
|
next if current_user
|
||||||
|
next if Submitter.find_signed(cookies.signed[:submitter_sid])
|
||||||
|
|
||||||
|
head :forbidden
|
||||||
|
end
|
||||||
|
|
||||||
LoadActiveStorageConfigs.call
|
LoadActiveStorageConfigs.call
|
||||||
rescue StandardError => e
|
rescue StandardError => e
|
||||||
Rails.logger.error(e)
|
Rails.logger.error(e)
|
||||||
|
|||||||
@@ -70,7 +70,7 @@ module GenerateCertificate
|
|||||||
key = OpenSSL::PKey::RSA.new(SIZE)
|
key = OpenSSL::PKey::RSA.new(SIZE)
|
||||||
|
|
||||||
cert = OpenSSL::X509::Certificate.new
|
cert = OpenSSL::X509::Certificate.new
|
||||||
cert.subject = OpenSSL::X509::Name.parse("/C=AT/O=#{name}/CN=#{name} Certificate")
|
cert.subject = OpenSSL::X509::Name.parse("/C=AT/O=#{name}/CN=#{name}")
|
||||||
cert.issuer = ca_cert.subject
|
cert.issuer = ca_cert.subject
|
||||||
cert.not_before = Time.current
|
cert.not_before = Time.current
|
||||||
cert.not_after = 100.years.from_now
|
cert.not_after = 100.years.from_now
|
||||||
|
|||||||
@@ -5,6 +5,8 @@ module Submissions
|
|||||||
FONT_SIZE = 11
|
FONT_SIZE = 11
|
||||||
FONT_NAME = 'Helvetica'
|
FONT_NAME = 'Helvetica'
|
||||||
|
|
||||||
|
INFO_CREATOR = 'DocuSeal (https://www.docuseal.co)'
|
||||||
|
|
||||||
A4_SIZE = [595, 842].freeze
|
A4_SIZE = [595, 842].freeze
|
||||||
|
|
||||||
module_function
|
module_function
|
||||||
@@ -165,7 +167,9 @@ module Submissions
|
|||||||
def save_signed_pdf(pdf:, submitter:, cert:, uuid:, name:)
|
def save_signed_pdf(pdf:, submitter:, cert:, uuid:, name:)
|
||||||
io = StringIO.new
|
io = StringIO.new
|
||||||
|
|
||||||
pdf.sign(io, reason: "Signed by #{submitter.email}",
|
pdf.trailer[:Info][:Creator] = INFO_CREATOR
|
||||||
|
|
||||||
|
pdf.sign(io, reason: "Signed by #{submitter.email} with docuseal.co",
|
||||||
certificate: OpenSSL::X509::Certificate.new(cert['cert']),
|
certificate: OpenSSL::X509::Certificate.new(cert['cert']),
|
||||||
key: OpenSSL::PKey::RSA.new(cert['key']),
|
key: OpenSSL::PKey::RSA.new(cert['key']),
|
||||||
certificate_chain: [OpenSSL::X509::Certificate.new(cert['sub_ca']),
|
certificate_chain: [OpenSSL::X509::Certificate.new(cert['sub_ca']),
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
"@babel/runtime": "7.21.5",
|
"@babel/runtime": "7.21.5",
|
||||||
"@github/catalyst": "^2.0.0-beta",
|
"@github/catalyst": "^2.0.0-beta",
|
||||||
"@hotwired/turbo-rails": "^7.3.0",
|
"@hotwired/turbo-rails": "^7.3.0",
|
||||||
"@rails/activestorage": "^7.0.4-3",
|
"@rails/activestorage": "^7.0.0",
|
||||||
"@tabler/icons-vue": "^2.20.0",
|
"@tabler/icons-vue": "^2.20.0",
|
||||||
"autoprefixer": "^10.4.14",
|
"autoprefixer": "^10.4.14",
|
||||||
"babel-loader": "9.1.2",
|
"babel-loader": "9.1.2",
|
||||||
|
|||||||
@@ -1119,10 +1119,10 @@
|
|||||||
resolved "https://registry.yarnpkg.com/@rails/actioncable/-/actioncable-7.0.4.tgz#70a3ca56809f7aaabb80af2f9c01ae51e1a8ed41"
|
resolved "https://registry.yarnpkg.com/@rails/actioncable/-/actioncable-7.0.4.tgz#70a3ca56809f7aaabb80af2f9c01ae51e1a8ed41"
|
||||||
integrity sha512-tz4oM+Zn9CYsvtyicsa/AwzKZKL+ITHWkhiu7x+xF77clh2b4Rm+s6xnOgY/sGDWoFWZmtKsE95hxBPkgQQNnQ==
|
integrity sha512-tz4oM+Zn9CYsvtyicsa/AwzKZKL+ITHWkhiu7x+xF77clh2b4Rm+s6xnOgY/sGDWoFWZmtKsE95hxBPkgQQNnQ==
|
||||||
|
|
||||||
"@rails/activestorage@^7.0.4-3":
|
"@rails/activestorage@^7.0.0":
|
||||||
version "7.0.4-3"
|
version "7.0.5"
|
||||||
resolved "https://registry.yarnpkg.com/@rails/activestorage/-/activestorage-7.0.4-3.tgz#ffdc5cb3bfef842c80c42db03dc26d862b2e9e08"
|
resolved "https://registry.yarnpkg.com/@rails/activestorage/-/activestorage-7.0.5.tgz#0e8fe255a422e5cb9f4e673e02fca845f32a50c1"
|
||||||
integrity sha512-elTffC5AbJsnz5SV2UFTUBH9+nO1xWRBe+E7A8x+6kAr/3yv6s/yWOUjbxiPPzVPyiihxJXuZ5zpvCdzwBpksA==
|
integrity sha512-lTOlsVsjz1OYDFRD2SKNH9j2TOA/LTrrIPgRSKyyD3CvJnD1bc65PQt/z6qB1hGqYmtUiklns+SjVWb9xYEqaA==
|
||||||
dependencies:
|
dependencies:
|
||||||
spark-md5 "^3.0.1"
|
spark-md5 "^3.0.1"
|
||||||
|
|
||||||
|
|||||||