Feat: sécurisation formulaire white paper + stockage leads
- Honeypot, timing check, rate limiting IP, validation serveur - Déduplication par email : enrichissement des champs vides si contact existant - Blueprint white-paper : onglet "Contacts intéressés" (champ structure contactDatabase) - Blueprint site.yml : ajout onglet "Données d'usage" pour vue globale des leads - Route externalisée dans site/config/routes/download-white-paper.php - isDownloadable côté client (prénom, nom, email valide, consentement) - Cursor : pas de hover sur boutons disabled - Buttons : hover désactivé si disabled Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
974067d986
commit
67d8159787
8 changed files with 312 additions and 112 deletions
|
|
@ -12,7 +12,22 @@
|
|||
let consent = $state(false)
|
||||
let submitting = $state(false)
|
||||
let status = $state(null) // null | 'success' | 'error'
|
||||
let showForm = $state(false)
|
||||
let showForm = $state(false)
|
||||
let honeypot = $state('')
|
||||
let formOpenedAt = $state(0)
|
||||
|
||||
$effect(() => {
|
||||
if (showForm && formOpenedAt === 0) formOpenedAt = Date.now()
|
||||
})
|
||||
|
||||
let isEmailValid = $derived.by(() => {
|
||||
const emailValidator = /^[\w\-\.]+@([\w-]+\.)+[\w-]{2,}$/gm
|
||||
return emailValidator.test(email)
|
||||
})
|
||||
|
||||
let isDownloadable = $derived.by(() => {
|
||||
return firstName.length > 0 && lastName.length > 0 && email.length > 0 && isEmailValid && consent
|
||||
})
|
||||
|
||||
async function handleSubmit(e) {
|
||||
e.preventDefault()
|
||||
|
|
@ -24,7 +39,7 @@
|
|||
const res = await fetch(`${prefix}/${data.uri}/download`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ firstName, lastName, company, role, email })
|
||||
body: JSON.stringify({ firstName, lastName, company, role, email, _hp: honeypot, _t: formOpenedAt })
|
||||
})
|
||||
const result = await res.json()
|
||||
if (result.fileUrl) {
|
||||
|
|
@ -77,10 +92,15 @@
|
|||
<input class="input" type="text" placeholder={t('wp_firstname')} bind:value={firstName} required />
|
||||
<input class="input" type="text" placeholder={t('wp_lastname')} bind:value={lastName} required />
|
||||
</div>
|
||||
<input class="input" type="text" placeholder={t('wp_company')} bind:value={company} required />
|
||||
<input class="input" type="text" placeholder={t('wp_role')} bind:value={role} required />
|
||||
<input class="input" type="text" placeholder={t('wp_company')} bind:value={company} />
|
||||
<input class="input" type="text" placeholder={t('wp_role')} bind:value={role} />
|
||||
<input class="input" type="email" placeholder={t('wp_email')} bind:value={email} required />
|
||||
|
||||
<div class="hp" aria-hidden="true">
|
||||
<label for="website">Website</label>
|
||||
<input id="website" type="text" name="website" tabindex="-1" autocomplete="off" bind:value={honeypot} />
|
||||
</div>
|
||||
|
||||
<label class="consent">
|
||||
<input type="checkbox" bind:checked={consent} required />
|
||||
<span>{t('wp_consent')}</span>
|
||||
|
|
@ -92,7 +112,7 @@
|
|||
<p class="status status--error">{t('wp_error')}</p>
|
||||
{/if}
|
||||
|
||||
<button type="submit" class="submit button" disabled={submitting || !consent}>
|
||||
<button type="submit" class="submit button" disabled={submitting || !isDownloadable}>
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" aria-hidden="true">
|
||||
<path d="M12 16L7 11H10V4H14V11H17L12 16Z" fill="currentColor"/>
|
||||
<path d="M5 20H19V18H5V20Z" fill="currentColor"/>
|
||||
|
|
@ -228,6 +248,16 @@
|
|||
color: rgba(255, 255, 255, 0.5);
|
||||
}
|
||||
|
||||
.hp {
|
||||
position: absolute;
|
||||
left: -9999px;
|
||||
width: 1px;
|
||||
height: 1px;
|
||||
overflow: hidden;
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.consent {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue