mirror of
https://github.com/docusealco/docuseal.git
synced 2026-06-23 04:10:11 +00:00
update signature validation
This commit is contained in:
committed by
Pete Matsyburka
parent
7e70f4fb14
commit
b0110d9340
+20
-17
@@ -1,5 +1,6 @@
|
||||
import SignaturePad from 'signature_pad'
|
||||
import { cropCanvasAndExportToPNG } from './submission_form/crop_canvas'
|
||||
import { isValidSignatureCanvas } from './submission_form/validate_signature'
|
||||
|
||||
window.customElements.define('draw-signature', class extends HTMLElement {
|
||||
connectedCallback () {
|
||||
@@ -43,6 +44,8 @@ window.customElements.define('draw-signature', class extends HTMLElement {
|
||||
|
||||
return response
|
||||
})
|
||||
}).catch(error => {
|
||||
console.log(error)
|
||||
}).finally(() => {
|
||||
this.submitButton.disabled = false
|
||||
})
|
||||
@@ -65,26 +68,26 @@ window.customElements.define('draw-signature', class extends HTMLElement {
|
||||
}
|
||||
|
||||
async submitImage () {
|
||||
return new Promise((resolve, reject) => {
|
||||
cropCanvasAndExportToPNG(this.canvas, { errorOnTooSmall: true }).then(async (blob) => {
|
||||
const file = new File([blob], 'signature.png', { type: 'image/png' })
|
||||
if (!isValidSignatureCanvas(this.pad.toData())) {
|
||||
alert('Signature is too small or simple. Please redraw.')
|
||||
|
||||
const formData = new FormData()
|
||||
return Promise.reject(new Error('Image too small or simple'))
|
||||
}
|
||||
|
||||
formData.append('file', file)
|
||||
formData.append('submitter_slug', this.dataset.slug)
|
||||
formData.append('name', 'attachments')
|
||||
formData.append('remember_signature', 'true')
|
||||
return cropCanvasAndExportToPNG(this.canvas).then(async (blob) => {
|
||||
const file = new File([blob], 'signature.png', { type: 'image/png' })
|
||||
|
||||
return fetch('/api/attachments', {
|
||||
method: 'POST',
|
||||
body: formData
|
||||
}).then((resp) => resp.json()).then((attachment) => {
|
||||
return resolve(attachment)
|
||||
})
|
||||
}).catch((error) => {
|
||||
return reject(error)
|
||||
})
|
||||
const formData = new FormData()
|
||||
|
||||
formData.append('file', file)
|
||||
formData.append('submitter_slug', this.dataset.slug)
|
||||
formData.append('name', 'attachments')
|
||||
formData.append('remember_signature', 'true')
|
||||
|
||||
return fetch('/api/attachments', {
|
||||
method: 'POST',
|
||||
body: formData
|
||||
}).then(resp => resp.json())
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
function cropCanvasAndExportToPNG (canvas, { errorOnTooSmall } = { errorOnTooSmall: false }) {
|
||||
function cropCanvasAndExportToPNG (canvas) {
|
||||
const ctx = canvas.getContext('2d')
|
||||
|
||||
const width = canvas.width
|
||||
@@ -33,10 +33,6 @@ function cropCanvasAndExportToPNG (canvas, { errorOnTooSmall } = { errorOnTooSma
|
||||
croppedCanvas.height = croppedHeight
|
||||
const croppedCtx = croppedCanvas.getContext('2d')
|
||||
|
||||
if (errorOnTooSmall && (croppedWidth < 20 || croppedHeight < 20)) {
|
||||
return Promise.reject(new Error('Image too small'))
|
||||
}
|
||||
|
||||
croppedCtx.drawImage(canvas, leftmost, topmost, croppedWidth, croppedHeight, 0, 0, croppedWidth, croppedHeight)
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
|
||||
@@ -1445,11 +1445,7 @@ export default {
|
||||
this.isSubmittingComplete = false
|
||||
})
|
||||
}).catch(error => {
|
||||
if (error?.message === 'Image too small') {
|
||||
alert(this.t('signature_is_too_small_please_redraw'))
|
||||
} else {
|
||||
console.log(error)
|
||||
}
|
||||
console.log(error)
|
||||
}).finally(() => {
|
||||
this.isSubmitting = false
|
||||
this.isSubmittingComplete = false
|
||||
|
||||
@@ -93,7 +93,7 @@ const en = {
|
||||
reupload: 'Reupload',
|
||||
upload: 'Upload',
|
||||
files: 'Files',
|
||||
signature_is_too_small_please_redraw: 'Signature is too small. Please redraw.',
|
||||
signature_is_too_small_or_simple_please_redraw: 'Signature is too small or simple. Please redraw.',
|
||||
wait_countdown_seconds: 'Wait {countdown} seconds'
|
||||
}
|
||||
|
||||
@@ -191,7 +191,7 @@ const es = {
|
||||
reupload: 'Volver a subir',
|
||||
upload: 'Subir',
|
||||
files: 'Archivos',
|
||||
signature_is_too_small_please_redraw: 'La firma es demasiado pequeña. Por favor, dibújala de nuevo.',
|
||||
signature_is_too_small_or_simple_please_redraw: 'La firma es demasiado pequeña o simple. Por favor, vuelve a dibujarla.',
|
||||
wait_countdown_seconds: 'Espera {countdown} segundos'
|
||||
}
|
||||
|
||||
@@ -289,7 +289,7 @@ const it = {
|
||||
reupload: 'Ricarica',
|
||||
upload: 'Carica',
|
||||
files: 'File',
|
||||
signature_is_too_small_please_redraw: 'La firma è troppo piccola. Ridisegnala per favore.',
|
||||
signature_is_too_small_or_simple_please_redraw: 'La firma è troppo piccola o semplice. Ridisegnala, per favore.',
|
||||
wait_countdown_seconds: 'Attendi {countdown} secondi'
|
||||
}
|
||||
|
||||
@@ -387,7 +387,7 @@ const de = {
|
||||
reupload: 'Erneut hochladen',
|
||||
upload: 'Hochladen',
|
||||
files: 'Dateien',
|
||||
signature_is_too_small_please_redraw: 'Die Unterschrift ist zu klein. Bitte erneut zeichnen.',
|
||||
signature_is_too_small_or_simple_please_redraw: 'Die Unterschrift ist zu klein oder zu einfach. Bitte erneut zeichnen.',
|
||||
wait_countdown_seconds: 'Warte {countdown} Sekunden'
|
||||
}
|
||||
|
||||
@@ -485,7 +485,7 @@ const fr = {
|
||||
reupload: 'Recharger',
|
||||
upload: 'Télécharger',
|
||||
files: 'Fichiers',
|
||||
signature_is_too_small_please_redraw: 'La signature est trop petite. Veuillez la redessiner.',
|
||||
signature_is_too_small_or_simple_please_redraw: 'La signature est trop petite ou trop simple. Veuillez la redessiner.',
|
||||
wait_countdown_seconds: 'Attendez {countdown} secondes'
|
||||
}
|
||||
|
||||
@@ -583,7 +583,7 @@ const pl = {
|
||||
reupload: 'Ponowne przesłanie',
|
||||
upload: 'Przesyłanie',
|
||||
files: 'Pliki',
|
||||
signature_is_too_small_please_redraw: 'Podpis jest zbyt mały. Proszę narysować go ponownie.'
|
||||
signature_is_too_small_or_simple_please_redraw: 'Podpis jest zbyt mały lub zbyt prosty. Proszę narysować go ponownie.'
|
||||
}
|
||||
|
||||
const uk = {
|
||||
@@ -680,7 +680,7 @@ const uk = {
|
||||
reupload: 'Перезавантажити',
|
||||
upload: 'Завантажити',
|
||||
files: 'Файли',
|
||||
signature_is_too_small_please_redraw: 'Підпис занадто малий. Будь ласка, перемалюйте його.',
|
||||
signature_is_too_small_or_simple_please_redraw: 'Підпис занадто маленький або надто простий. Будь ласка, перемалюйте.',
|
||||
wait_countdown_seconds: 'Зачекайте {countdown} секунд'
|
||||
}
|
||||
|
||||
@@ -778,7 +778,7 @@ const cs = {
|
||||
reupload: 'Znovu nahrát',
|
||||
upload: 'Nahrát',
|
||||
files: 'Soubory',
|
||||
signature_is_too_small_please_redraw: 'Podpis je příliš malý. Prosím, překreslete ho.',
|
||||
signature_is_too_small_or_simple_please_redraw: 'Podpis je příliš malý nebo jednoduchý. Nakreslete jej prosím znovu.',
|
||||
wait_countdown_seconds: 'Počkejte {countdown} sekund'
|
||||
}
|
||||
|
||||
@@ -876,7 +876,7 @@ const pt = {
|
||||
reupload: 'Reenviar',
|
||||
upload: 'Carregar',
|
||||
files: 'Arquivos',
|
||||
signature_is_too_small_please_redraw: 'A assinatura é muito pequena. Por favor, redesenhe-a.',
|
||||
signature_is_too_small_or_simple_please_redraw: 'A assinatura é muito pequena ou simples. Por favor, redesenhe.',
|
||||
wait_countdown_seconds: 'Aguarde {countdown} segundos'
|
||||
}
|
||||
|
||||
@@ -975,7 +975,7 @@ const he = {
|
||||
reupload: 'העלה שוב',
|
||||
upload: 'העלאה',
|
||||
files: 'קבצים',
|
||||
signature_is_too_small_please_redraw: 'החתימה קטנה מדי. אנא צייר מחדש.',
|
||||
signature_is_too_small_or_simple_please_redraw: 'החתימה קטנה או פשוטה מדי. אנא חתום מחדש.',
|
||||
wait_countdown_seconds: 'המתן {countdown} שניות'
|
||||
}
|
||||
|
||||
@@ -1074,7 +1074,7 @@ const nl = {
|
||||
reupload: 'Opnieuw uploaden',
|
||||
upload: 'Uploaden',
|
||||
files: 'Bestanden',
|
||||
signature_is_too_small_please_redraw: 'De handtekening is te klein. Teken deze opnieuw, alstublieft.',
|
||||
signature_is_too_small_or_simple_please_redraw: 'De handtekening is te klein of te eenvoudig. Teken opnieuw.',
|
||||
wait_countdown_seconds: 'Wacht {countdown} seconden'
|
||||
}
|
||||
|
||||
@@ -1172,7 +1172,7 @@ const ar = {
|
||||
reupload: 'إعادة التحميل',
|
||||
upload: 'تحميل',
|
||||
files: 'الملفات',
|
||||
signature_is_too_small_please_redraw: 'التوقيع صغير جدًا. يرجى إعادة الرسم.',
|
||||
signature_is_too_small_or_simple_please_redraw: 'التوقيع صغير جدًا أو بسيط جدًا. يرجى إعادة رسمه.',
|
||||
wait_countdown_seconds: 'انتظر {countdown} ثانية'
|
||||
}
|
||||
|
||||
@@ -1269,7 +1269,7 @@ const ko = {
|
||||
reupload: '다시 업로드',
|
||||
upload: '업로드',
|
||||
files: '파일',
|
||||
signature_is_too_small_please_redraw: '서명이 너무 작습니다. 다시 그려주세요.',
|
||||
signature_is_too_small_or_simple_please_redraw: '서명이 너무 작거나 단순합니다. 다시 그려주세요.',
|
||||
wait_countdown_seconds: '{countdown}초 기다리세요'
|
||||
}
|
||||
|
||||
|
||||
@@ -118,11 +118,17 @@
|
||||
:src="attachmentsIndex[modelValue || computedPreviousValue].url"
|
||||
class="mx-auto bg-white border border-base-300 rounded max-h-44"
|
||||
>
|
||||
<canvas
|
||||
v-show="!modelValue && !computedPreviousValue"
|
||||
ref="canvas"
|
||||
class="bg-white border border-base-300 rounded-2xl w-full draw-canvas"
|
||||
/>
|
||||
<div class="relative">
|
||||
<div
|
||||
v-if="!isDrawInitials"
|
||||
class="absolute top-0 right-0 left-0 bottom-0"
|
||||
/>
|
||||
<canvas
|
||||
v-show="!modelValue && !computedPreviousValue"
|
||||
ref="canvas"
|
||||
class="bg-white border border-base-300 rounded-2xl w-full draw-canvas"
|
||||
/>
|
||||
</div>
|
||||
<input
|
||||
v-if="!isDrawInitials && !modelValue && !computedPreviousValue"
|
||||
id="initials_text_input"
|
||||
|
||||
@@ -292,6 +292,7 @@
|
||||
<script>
|
||||
import { IconReload, IconCamera, IconSignature, IconTextSize, IconArrowsDiagonalMinimize2, IconQrcode, IconX } from '@tabler/icons-vue'
|
||||
import { cropCanvasAndExportToPNG } from './crop_canvas'
|
||||
import { isValidSignatureCanvas } from './validate_signature'
|
||||
import SignaturePad from 'signature_pad'
|
||||
import AppearsOn from './appears_on'
|
||||
import FileDropzone from './dropzone'
|
||||
@@ -680,8 +681,14 @@ export default {
|
||||
return Promise.resolve({})
|
||||
}
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
cropCanvasAndExportToPNG(this.$refs.canvas, { errorOnTooSmall: true }).then(async (blob) => {
|
||||
if (this.isSignatureStarted && this.pad.toData().length > 0 && !isValidSignatureCanvas(this.pad.toData())) {
|
||||
alert(this.t('signature_is_too_small_or_simple_please_redraw'))
|
||||
|
||||
return Promise.reject(new Error('Image too small or simple'))
|
||||
}
|
||||
|
||||
return new Promise((resolve) => {
|
||||
cropCanvasAndExportToPNG(this.$refs.canvas).then(async (blob) => {
|
||||
const file = new File([blob], 'signature.png', { type: 'image/png' })
|
||||
|
||||
if (this.dryRun) {
|
||||
@@ -717,12 +724,6 @@ export default {
|
||||
return resolve(attachment)
|
||||
})
|
||||
}
|
||||
}).catch((error) => {
|
||||
if (error.message === 'Image too small' && this.field.required === false) {
|
||||
return resolve({})
|
||||
} else {
|
||||
return reject(error)
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
function isValidSignatureCanvas (data) {
|
||||
if (data.length === 0) return false
|
||||
|
||||
const strokes = data.filter(stroke => Array.isArray(stroke.points) && stroke.points.length > 2)
|
||||
|
||||
if (strokes.length === 0) return false
|
||||
|
||||
let skippedStraightLine = 0
|
||||
|
||||
const validStrokes = strokes.filter(stroke => {
|
||||
const points = stroke.points
|
||||
const first = points[0]
|
||||
const last = points[points.length - 1]
|
||||
const A = last.y - first.y
|
||||
const B = first.x - last.x
|
||||
const C = last.x * first.y - first.x * last.y
|
||||
const lineLength = Math.sqrt(A * A + B * B)
|
||||
|
||||
const totalDeviation = points.reduce((sum, p) => {
|
||||
const distanceToLine = Math.abs(A * p.x + B * p.y + C) / lineLength
|
||||
return sum + distanceToLine
|
||||
}, 0)
|
||||
|
||||
const avgDeviation = totalDeviation / points.length
|
||||
|
||||
if (avgDeviation < 3 && skippedStraightLine < 2) {
|
||||
skippedStraightLine++
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
})
|
||||
|
||||
return validStrokes.length > 0
|
||||
}
|
||||
|
||||
export { isValidSignatureCanvas }
|
||||
+38
-10
@@ -4,21 +4,49 @@ module SigningFormHelper
|
||||
module_function
|
||||
|
||||
def draw_canvas
|
||||
page.find('canvas').click([], { x: 150, y: 100 })
|
||||
page.execute_script <<~JS
|
||||
const canvas = document.getElementsByTagName('canvas')[0];
|
||||
const ctx = canvas.getContext('2d');
|
||||
const rect = canvas.getBoundingClientRect();
|
||||
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(150, 100);
|
||||
ctx.lineTo(450, 100);
|
||||
ctx.stroke();
|
||||
const startX = rect.left + 50;
|
||||
const startY = rect.top + 100;
|
||||
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(150, 100);
|
||||
ctx.lineTo(150, 150);
|
||||
ctx.stroke();
|
||||
const amplitude = 20;
|
||||
const wavelength = 30;
|
||||
const length = 300;
|
||||
|
||||
function dispatchPointerEvent(type, x, y) {
|
||||
const event = new PointerEvent(type, {
|
||||
pointerId: 1,
|
||||
pointerType: 'pen',
|
||||
isPrimary: true,
|
||||
clientX: x,
|
||||
clientY: y,
|
||||
bubbles: true,
|
||||
pressure: 0.5
|
||||
});
|
||||
|
||||
canvas.dispatchEvent(event);
|
||||
}
|
||||
|
||||
dispatchPointerEvent('pointerdown', startX, startY);
|
||||
|
||||
let x = 0;
|
||||
function drawStep() {
|
||||
if (x > length) {
|
||||
dispatchPointerEvent('pointerup', startX + x, startY);
|
||||
return;
|
||||
}
|
||||
|
||||
const y = startY + amplitude * Math.sin((x / wavelength) * 2 * Math.PI);
|
||||
dispatchPointerEvent('pointermove', startX + x, y);
|
||||
x += 5;
|
||||
requestAnimationFrame(drawStep);
|
||||
}
|
||||
|
||||
drawStep();
|
||||
JS
|
||||
|
||||
sleep 0.5
|
||||
end
|
||||
|
||||
|
||||
@@ -360,6 +360,19 @@ RSpec.describe 'Signing Form', type: :system do
|
||||
expect(field_value(submitter, 'Signature')).to be_present
|
||||
end
|
||||
|
||||
it 'shows an error message if the canvas is not drawn or too simple' do
|
||||
visit submit_form_path(slug: submitter.slug)
|
||||
|
||||
find('#expand_form_button').click
|
||||
page.find('canvas').click([], { x: 150, y: 100 })
|
||||
|
||||
alert_text = page.accept_alert do
|
||||
click_button 'Sign and Complete'
|
||||
end
|
||||
|
||||
expect(alert_text).to eq 'Signature is too small or simple. Please redraw.'
|
||||
end
|
||||
|
||||
it 'completes the form if the canvas is typed' do
|
||||
visit submit_form_path(slug: submitter.slug)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user