diff --git a/src/components/ContactForm.astro b/src/components/ContactForm.astro index cb7c3ea..5e1d931 100644 --- a/src/components/ContactForm.astro +++ b/src/components/ContactForm.astro @@ -18,7 +18,7 @@ const { variant = 'full' } = Astro.props; - +
Inviando il modulo dichiari di aver letto l'informativa sulla privacy.
diff --git a/src/data/services.ts b/src/data/services.ts index 625dfca..5e2fb34 100644 --- a/src/data/services.ts +++ b/src/data/services.ts @@ -44,7 +44,7 @@ export const services: Service[] = [ }, { slug: "performance-class", - title: "Performance Class", + title: "Class", subtitle: "Gruppo max 8 persone", excerpt: "La Performance Class è un allenamento in piccoli gruppi, basato su una programmazione progressiva che combina forza e condizionamento per ottenere risultati concreti e costanti nel tempo.", longDescription: "La Performance Class è un allenamento in piccoli gruppi con programmazione progressiva e scientifica, pensato per migliorare forza, composizione corporea e performance attraverso sessioni strutturate che combinano forza, ipertrofia e condizionamento metabolico. Non si improvvisa: ogni sessione ha un posto preciso all’interno di un piano più ampio.", diff --git a/src/data/site.ts b/src/data/site.ts index 2ade69f..2bef5e4 100644 --- a/src/data/site.ts +++ b/src/data/site.ts @@ -6,7 +6,7 @@ export const site = { address: "Via Leandro Alberti 76 e Via De Mattiolo 5", cityLine: "40139 Bologna", mapQuery: "IN-SANITY LAB, Via Leandro Alberti 76, 40139 Bologna", - hours: ["Lun – Ven 6.00 – 21.00", "Sab 8.00 – 19.00 · Dom chiuso"], + hours: ["Lun – Ven 6.00 – 21.00", "Sab 8.00 – 17.00 · Dom chiuso"], socials: [ { label: "Instagram", url: "https://www.instagram.com/in_sanity_lab" }, { label: "LinkedIn", url: "https://www.linkedin.com/company/in-sanity-lab-srl/" }, @@ -31,7 +31,7 @@ export const site = { ], footerServices: [ { label: "Personal Training", href: "/services/personal-training" }, - { label: "Performance Class", href: "/services/performance-class" }, + { label: "Class", href: "/services/performance-class" }, { label: "Coaching", href: "/services/coaching" }, { label: "Profilazione", href: "/services/profilazione" }, { label: "Osteopatia", href: "/services/osteopatia" }, diff --git a/src/lib/contact.ts b/src/lib/contact.ts index 1140aba..fd3f8c9 100644 --- a/src/lib/contact.ts +++ b/src/lib/contact.ts @@ -1,6 +1,6 @@ export interface ContactData { firstName: string; lastName: string; phone: string; - email: string; message: string; website: string; + email: string; message: string; hpField: string; } const EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; @@ -20,9 +20,10 @@ export function validateContact(data: unknown): phone: str(d.phone, 40), email: str(d.email, 200), message: str(d.message, 5000), - website: str(d.website, 200), + // Honeypot: nome neutro per non farlo riempire dall'autofill del browser. + hpField: str(d.hp_field, 200), }; - if (value.website) return { ok: false, error: 'Richiesta non valida.' }; + if (value.hpField) return { ok: false, error: 'Richiesta non valida.' }; if (!value.firstName) return { ok: false, error: 'Il nome è obbligatorio.' }; if (!value.lastName) return { ok: false, error: 'Il cognome è obbligatorio.' }; if (!value.phone) return { ok: false, error: 'Il telefono è obbligatorio.' }; diff --git a/src/lib/db.ts b/src/lib/db.ts index 4c1a88b..8fd6b09 100644 --- a/src/lib/db.ts +++ b/src/lib/db.ts @@ -177,11 +177,18 @@ export function createDb(path?: string): Database.Database { const updPromoTx = db.transaction(() => { for (const [tag, oldV, newV] of PROMO_FRECCE) upd02.run(newV, tag, oldV); }); updPromoTx(); - // Fix: il titolo del servizio Performance Class era rimasto troncato a "Class" nei DB esistenti - // (valore vecchio mai allineato dal seed INSERT OR IGNORE), mostrando "CLASS" come H1 della - // pagina /services/performance-class e nelle card di /services e home. Guardato dal vecchio - // valore → idempotente, non tocca eventuali modifiche del pannello. - db.prepare("UPDATE content_blocks SET value = 'Performance Class' WHERE tag = 'service.performance-class.title' AND value = 'Class'").run(); + // Il servizio "Performance Class" torna a chiamarsi solo "Class" (titolo card /services, + // H1 pagina dettaglio, link footer). Inverte la precedente normalizzazione Class→Performance + // Class. Guardato dal vecchio valore → idempotente, non tocca eventuali modifiche del pannello. + db.prepare("UPDATE content_blocks SET value = 'Class' WHERE tag = 'service.performance-class.title' AND value = 'Performance Class'").run(); + db.prepare("UPDATE content_blocks SET value = 'Class' WHERE tag = 'footer.services.2.label' AND value = 'Performance Class'").run(); + + // Titolo del servizio One to One: in prod era stato editato da pannello a "Personal training" + // (t minuscola → la vecchia migrazione, guardata su 'Personal Training', non l'agganciava), + // così l'hero e l'H2 di /services/one-to-one mostravano "PERSONAL TRAINING". Riportiamo il + // nome canonico "One to One" (già nel seed). Guardato sul valore reale corrente → idempotente; + // la descrizione lunga editata da pannello NON viene toccata. + db.prepare("UPDATE content_blocks SET value = 'One to One' WHERE tag = 'service.one-to-one.title' AND value = 'Personal training'").run(); // Pulizia tag orfani: il riquadro training "performance-class" è stato rimosso dal seed // (commit 9966ec3, "Training: 6 riquadri"), ma i suoi tag restano nei DB esistenti — orfani, @@ -194,6 +201,10 @@ export function createDb(path?: string): Database.Database { // già esistente, se non modificato dal pannello. Idempotente. db.prepare("UPDATE content_blocks SET value = 'Via Leandro Alberti 76 e Via De Mattiolo 5' WHERE tag = 'global.contact.address' AND value = 'Via Leandro Alberti, 76'").run(); + // Orario sabato corretto: 8–17, non 8–19. Guardato dal vecchio valore di default → + // idempotente, non tocca eventuali modifiche del pannello. + db.prepare("UPDATE content_blocks SET value = 'Sab 8.00 – 17.00 · Dom chiuso' WHERE tag = 'global.hours.2' AND value = 'Sab 8.00 – 19.00 · Dom chiuso'").run(); + // Modifiche 02 (pagina Insanitylab/about): testi riscritti. Guardia via frammento LIKE // distintivo del vecchio testo → robusto agli apostrofi tipografici, idempotente e // rispettoso delle eventuali riscritture da pannello (se il frammento non c'è più, salta). diff --git a/src/lib/rate-limit.ts b/src/lib/rate-limit.ts index 9f8905c..49b01dc 100644 --- a/src/lib/rate-limit.ts +++ b/src/lib/rate-limit.ts @@ -15,3 +15,10 @@ export function rateLimit(key: string, max: number, windowMs: number): boolean { export function _resetBuckets(): void { buckets.clear(); } + +// Dietro Traefik `clientAddress` è l'IP interno del proxy, uguale per tutti: +// usiamo il primo IP di X-Forwarded-For (client reale) come chiave rate-limit. +export function clientIp(request: Request, fallback: string): string { + const first = request.headers.get('x-forwarded-for')?.split(',')[0].trim(); + return first || fallback; +} diff --git a/src/pages/api/contact.ts b/src/pages/api/contact.ts index 74aba2f..0956575 100644 --- a/src/pages/api/contact.ts +++ b/src/pages/api/contact.ts @@ -1,7 +1,7 @@ import type { APIRoute } from 'astro'; import { validateContact } from '../../lib/contact'; import { sendContactEmail } from '../../lib/mailer'; -import { rateLimit } from '../../lib/rate-limit'; +import { rateLimit, clientIp } from '../../lib/rate-limit'; export const prerender = false; @@ -9,13 +9,15 @@ const json = (status: number, body: object) => new Response(JSON.stringify(body), { status, headers: { 'Content-Type': 'application/json' } }); export const POST: APIRoute = async ({ request, clientAddress }) => { - if (!rateLimit(`contact:${clientAddress}`, 5, 60 * 60 * 1000)) { - return json(429, { error: 'Troppe richieste, riprova più tardi.' }); - } let data: unknown; try { data = await request.json(); } catch { return json(400, { error: 'Dati non validi.' }); } const result = validateContact(data); if (!result.ok) return json(400, { error: result.error }); + // Rate-limit solo sugli invii validi, per IP reale: bot/honeypot non + // consumano la quota di un utente legittimo. + if (!rateLimit(`contact:${clientIp(request, clientAddress)}`, 5, 60 * 60 * 1000)) { + return json(429, { error: 'Troppe richieste, riprova più tardi.' }); + } try { await sendContactEmail(result.value); } catch (err) { diff --git a/tests/contact.test.ts b/tests/contact.test.ts index d848382..4f04afb 100644 --- a/tests/contact.test.ts +++ b/tests/contact.test.ts @@ -26,7 +26,7 @@ describe('validateContact', () => { }); it('rifiuta honeypot compilato', () => { - expect(validateContact({ ...good, website: 'spam.com' }).ok).toBe(false); + expect(validateContact({ ...good, hp_field: 'spam.com' }).ok).toBe(false); }); it('rifiuta messaggi oltre 5000 caratteri', () => { diff --git a/tests/content-db.test.ts b/tests/content-db.test.ts index 1614306..856fe27 100644 --- a/tests/content-db.test.ts +++ b/tests/content-db.test.ts @@ -113,18 +113,18 @@ describe('schema content_blocks e ruoli', () => { } }); - it('corregge il titolo Performance Class rimasto troncato a "Class"', () => { + it('riporta il titolo del servizio a "Class" (inverte Performance Class)', () => { const dir = mkdtempSync(join(tmpdir(), 'il-db-pc-')); const path = join(dir, 'test.db'); try { - // DB con il vecchio valore troncato. + // DB con il vecchio valore normalizzato "Performance Class". const db1 = createDb(path); - db1.prepare("UPDATE content_blocks SET value = 'Class', updated_by = NULL WHERE tag = 'service.performance-class.title'").run(); + db1.prepare("UPDATE content_blocks SET value = 'Performance Class', updated_by = NULL WHERE tag = 'service.performance-class.title'").run(); db1.close(); const db2 = createDb(path); const val = (tag: string) => (db2.prepare('SELECT value FROM content_blocks WHERE tag = ?').get(tag) as { value: string }).value; - expect(val('service.performance-class.title')).toBe('Performance Class'); + expect(val('service.performance-class.title')).toBe('Class'); db2.close(); // Non deve toccare un valore modificato dal pannello.