16 Commits

Author SHA1 Message Date
770db3067d Mostra il link di disiscrizione dopo la conferma newsletter.
Passa il token unsub alla pagina di conferma e lo include anche nell'email di double opt-in.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-29 14:45:54 +02:00
6ba58c3a86 Merge pull request #16 from javaxman/fix/newsletter-turnstile-optional
Fix Turnstile sul form newsletter
2026-07-29 14:40:21 +02:00
8a99290209 Non richiedere Turnstile contatti sul form newsletter.
Il footer non ha il widget Turnstile: evita il fallimento usando solo NEWSLETTER_TURNSTILE_SECRET_KEY.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-29 14:39:52 +02:00
c04d43d9d7 Merge pull request #15 from javaxman/fix/astro6-cloudflare-workers-env
Fix env Cloudflare per Astro 6 (contatti e newsletter)
2026-07-29 14:08:25 +02:00
c4adc116d6 Corregge l'accesso all'env Worker per Astro 6.
Sostituisce locals.runtime.env con import da cloudflare:workers, ripristinando contatti e newsletter.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-29 14:06:16 +02:00
44007f99e1 Merge pull request #14 from javaxman/feat/cloudflare-email-sending
Sostituisce Resend con Cloudflare Email Service
2026-07-29 09:55:05 +02:00
0743521d48 Sostituisce Resend con Cloudflare Email Service.
Usa il binding EMAIL per contatti e newsletter, eliminando la dipendenza da Resend e dalle relative API key.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-29 09:54:12 +02:00
2b470c778a Merge pull request #13 from javaxman/feat/newsletter-custom
Aggiunge newsletter custom con D1 e double opt-in
2026-07-28 23:27:00 +02:00
7aff1abcfb Aggiunge newsletter custom con D1, double opt-in e invio campagne.
Attiva il form footer, API subscribe/confirm/unsubscribe e endpoint admin protetti, riusando Resend per le email.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-28 23:23:19 +02:00
9a8566a3f5 test: verifica definitiva Workers Builds su main
Conferma che il token cloudflare_nexstudio_build_worker_07_2026 funziona sul percorso di produzione.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-28 23:02:49 +02:00
ed14c7f6a9 Merge pull request #12 from javaxman/docs/legal-texts-update
Aggiorna testi legali cookie, privacy, GDPR e condizioni d'uso
2026-07-28 12:33:10 +02:00
5854401de3 Aggiorna i testi legali del sito alla versione 1.1.
Allinea condizioni d'uso, cookie policy, privacy e GDPR a NexStudio S.r.l. (Italia), mantenendo placeholder per indirizzo, P.IVA e data.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-28 12:31:54 +02:00
c793688762 Merge pull request #11 from javaxman/feat/issue-5-i18n-home-menu-footer
Fix caricamento CSS su route EN e TH
2026-04-27 13:43:55 +02:00
85e470f58a Merge pull request #10 from javaxman/feat/issue-5-i18n-home-menu-footer
Rollback regressione CSS globale su BaseLayout
2026-04-27 11:43:46 +02:00
ffdb6979ff Merge pull request #9 from javaxman/feat/issue-5-i18n-home-menu-footer
Fix CSS globale su route localizzate EN TH
2026-04-27 11:23:22 +02:00
ee610a8853 Merge pull request #8 from javaxman/feat/issue-5-i18n-home-menu-footer
Avvia i18n reale home IT EN TH e corregge rendering locale
2026-04-27 10:20:29 +02:00
36 changed files with 1266 additions and 447 deletions

View File

@ -2,12 +2,20 @@ PUBLIC_CONTACT_FORM_ACTION_URL=/api/contact
PUBLIC_CONTACT_FORM_SUCCESS_REDIRECT=http://localhost:4321/#contatti PUBLIC_CONTACT_FORM_SUCCESS_REDIRECT=http://localhost:4321/#contatti
PUBLIC_CONTACT_FORM_MAILTO= PUBLIC_CONTACT_FORM_MAILTO=
PUBLIC_CONTACT_TURNSTILE_SITE_KEY= PUBLIC_CONTACT_TURNSTILE_SITE_KEY=
PUBLIC_SITE_URL=http://localhost:4321
CONTACT_FORM_MODE=dev CONTACT_FORM_MODE=dev
CONTACT_FORM_RESEND_API_KEY= CONTACT_FORM_FROM_EMAIL=no-reply@nexstudio.ai
CONTACT_FORM_FROM_EMAIL=no-reply@nexstudio.com CONTACT_FORM_TO_EMAIL=info@nexstudio.ai
CONTACT_FORM_TO_EMAIL=info@nexstudio.com CONTACT_FORM_TO_EMAIL_INFO=info@nexstudio.ai
CONTACT_FORM_TO_EMAIL_INFO=info@nexstudio.com
CONTACT_FORM_TO_EMAIL_PRIVACY= CONTACT_FORM_TO_EMAIL_PRIVACY=
CONTACT_FORM_TO_EMAIL_CAREERS= CONTACT_FORM_TO_EMAIL_CAREERS=
CONTACT_TURNSTILE_SECRET_KEY= CONTACT_TURNSTILE_SECRET_KEY=
# Newsletter (custom D1 + Cloudflare Email Service, double opt-in)
# In produzione: NEWSLETTER_MODE=live (oppure CONTACT_FORM_MODE=live come fallback)
NEWSLETTER_MODE=dev
NEWSLETTER_FROM_EMAIL=no-reply@nexstudio.ai
NEWSLETTER_NOTIFY_TO_EMAIL=info@nexstudio.ai
NEWSLETTER_ADMIN_TOKEN=
NEWSLETTER_TURNSTILE_SECRET_KEY=

View File

@ -15,7 +15,7 @@ Sito istituzionale NexStudio basato su Astro, con deploy su Cloudflare e form co
- Adapter Cloudflare (`@astrojs/cloudflare`) - Adapter Cloudflare (`@astrojs/cloudflare`)
- Sitemap (`@astrojs/sitemap`) - Sitemap (`@astrojs/sitemap`)
- Form backend su endpoint `/api/contact` - Form backend su endpoint `/api/contact`
- Turnstile + Resend per anti-spam e invio email - Turnstile + Cloudflare Email Service per anti-spam e invio email
## Struttura essenziale ## Struttura essenziale
@ -29,8 +29,7 @@ Sito istituzionale NexStudio basato su Astro, con deploy su Cloudflare e form co
- Node.js 20+ - Node.js 20+
- npm 10+ - npm 10+
- Account Cloudflare (Pages/Workers + Turnstile) - Account Cloudflare (Workers + Turnstile + Email Service con dominio onboardato, es. `nexstudio.ai`)
- Account Resend (API key + sender verificato)
## Avvio locale ## Avvio locale
@ -63,8 +62,7 @@ Variabili pubbliche (frontend):
Variabili server (Cloudflare/local): Variabili server (Cloudflare/local):
- `CONTACT_FORM_MODE` (`dev` oppure `live`) - `CONTACT_FORM_MODE` (`dev` oppure `live`)
- `CONTACT_FORM_RESEND_API_KEY` - `CONTACT_FORM_FROM_EMAIL` (consigliato: `no-reply@nexstudio.ai`, dominio verificato su Email Service)
- `CONTACT_FORM_FROM_EMAIL` (consigliato: `no-reply@nexstudio.ai`)
- `CONTACT_FORM_TO_EMAIL` (default destinatario, es. `info@nexstudio.ai`) - `CONTACT_FORM_TO_EMAIL` (default destinatario, es. `info@nexstudio.ai`)
- `CONTACT_FORM_TO_EMAIL_INFO` (opzionale, override esplicito per area "info") - `CONTACT_FORM_TO_EMAIL_INFO` (opzionale, override esplicito per area "info")
- `CONTACT_FORM_TO_EMAIL_PRIVACY` - `CONTACT_FORM_TO_EMAIL_PRIVACY`
@ -74,7 +72,7 @@ Variabili server (Cloudflare/local):
Comportamento: Comportamento:
- `dev`: accetta submit e scrive payload nei log (senza invio reale) - `dev`: accetta submit e scrive payload nei log (senza invio reale)
- `live`: valida Turnstile e invia email via Resend - `live`: valida Turnstile e invia email via binding `EMAIL` (Cloudflare Email Service)
Routing pubblico consigliato (portale vetrina): Routing pubblico consigliato (portale vetrina):
@ -89,6 +87,45 @@ Indirizzi consigliati portale:
- `careers@nexstudio.ai` - `careers@nexstudio.ai`
- `no-reply@nexstudio.ai` (solo invio comunicazioni/newsletter, non destinazione inbound) - `no-reply@nexstudio.ai` (solo invio comunicazioni/newsletter, non destinazione inbound)
## Configurazione newsletter
Sistema custom su Cloudflare D1 + Cloudflare Email Service (double opt-in).
Prerequisito: dominio (es. `nexstudio.ai`) onboardato in **Email Service** con SPF/DKIM, e binding `send_email``EMAIL` nel Worker.
Variabili server:
- `NEWSLETTER_MODE` (`dev` oppure `live`; se assente usa `CONTACT_FORM_MODE`)
- `NEWSLETTER_FROM_EMAIL` (fallback: `CONTACT_FORM_FROM_EMAIL`, consigliato `no-reply@nexstudio.ai`)
- `NEWSLETTER_NOTIFY_TO_EMAIL` (notifiche admin; fallback: `CONTACT_FORM_TO_EMAIL`)
- `NEWSLETTER_ADMIN_TOKEN` (Bearer token per endpoint admin)
- `NEWSLETTER_TURNSTILE_SECRET_KEY` (opzionale; fallback: `CONTACT_TURNSTILE_SECRET_KEY`)
- `PUBLIC_SITE_URL` (origine usata nei link di conferma/disiscrizione)
Endpoint pubblici:
- `POST /api/newsletter/subscribe` — iscrizione (form footer)
- `GET /api/newsletter/confirm?token=…` — conferma double opt-in
- `GET|POST /api/newsletter/unsubscribe?token=…` — disiscrizione
Endpoint admin (header `Authorization: Bearer <NEWSLETTER_ADMIN_TOKEN>`):
- `GET /api/newsletter/admin/subscribers?status=active|pending|unsubscribed`
- `POST /api/newsletter/admin/send` — body JSON `{ "subject", "text", "html?", "dryRun?" }`
Nota: Email Service è pensato per email **transazionali** (conferme, contatti, notifiche). Le campagne marketing di massa non sono ancora il target ufficiale del servizio.
Database:
- D1 `nexstudio-newsletter` (binding `NEWSLETTER_DB`)
- Migrazioni in `migrations/`
- `npm run db:migrate:local` / `npm run db:migrate:remote`
Comportamento:
- `dev`: accetta submit e logga (senza D1/Email obbligatori)
- `live`: scrive su D1, invia email di conferma via Cloudflare Email Service, richiede binding D1 + EMAIL
## Deploy Cloudflare ## Deploy Cloudflare
Impostazioni consigliate: Impostazioni consigliate:

View File

@ -0,0 +1,49 @@
-- Newsletter subscribers and campaign send log
CREATE TABLE IF NOT EXISTS newsletter_subscribers (
id INTEGER PRIMARY KEY AUTOINCREMENT,
email TEXT NOT NULL COLLATE NOCASE,
status TEXT NOT NULL CHECK (status IN ('pending', 'active', 'unsubscribed')),
locale TEXT NOT NULL DEFAULT 'it',
confirm_token TEXT,
unsubscribe_token TEXT NOT NULL,
source TEXT NOT NULL DEFAULT 'footer',
created_at TEXT NOT NULL DEFAULT (datetime('now')),
confirmed_at TEXT,
unsubscribed_at TEXT,
UNIQUE (email)
);
CREATE INDEX IF NOT EXISTS idx_newsletter_subscribers_status
ON newsletter_subscribers (status);
CREATE INDEX IF NOT EXISTS idx_newsletter_subscribers_confirm_token
ON newsletter_subscribers (confirm_token);
CREATE INDEX IF NOT EXISTS idx_newsletter_subscribers_unsubscribe_token
ON newsletter_subscribers (unsubscribe_token);
CREATE TABLE IF NOT EXISTS newsletter_campaigns (
id INTEGER PRIMARY KEY AUTOINCREMENT,
subject TEXT NOT NULL,
body_text TEXT NOT NULL,
body_html TEXT,
status TEXT NOT NULL DEFAULT 'draft' CHECK (status IN ('draft', 'sending', 'sent', 'failed')),
created_at TEXT NOT NULL DEFAULT (datetime('now')),
sent_at TEXT,
created_by TEXT
);
CREATE TABLE IF NOT EXISTS newsletter_campaign_sends (
id INTEGER PRIMARY KEY AUTOINCREMENT,
campaign_id INTEGER NOT NULL,
subscriber_id INTEGER NOT NULL,
email TEXT NOT NULL,
status TEXT NOT NULL CHECK (status IN ('queued', 'sent', 'failed', 'skipped')),
error TEXT,
sent_at TEXT,
FOREIGN KEY (campaign_id) REFERENCES newsletter_campaigns (id),
FOREIGN KEY (subscriber_id) REFERENCES newsletter_subscribers (id)
);
CREATE INDEX IF NOT EXISTS idx_newsletter_campaign_sends_campaign
ON newsletter_campaign_sends (campaign_id);

View File

@ -12,7 +12,9 @@
"deploy:cf": "npm run build && wrangler deploy --config dist/server/wrangler.json", "deploy:cf": "npm run build && wrangler deploy --config dist/server/wrangler.json",
"preview": "astro preview", "preview": "astro preview",
"astro": "astro", "astro": "astro",
"generate-types": "wrangler types" "generate-types": "wrangler types",
"db:migrate:local": "wrangler d1 migrations apply nexstudio-newsletter --local",
"db:migrate:remote": "wrangler d1 migrations apply nexstudio-newsletter --remote"
}, },
"dependencies": { "dependencies": {
"@astrojs/alpinejs": "^0.5.0", "@astrojs/alpinejs": "^0.5.0",

View File

@ -166,6 +166,8 @@ const withLocale = (href: string) => {
{notifySubject ? ( {notifySubject ? (
<input type="hidden" name="_subject" value={notifySubject} /> <input type="hidden" name="_subject" value={notifySubject} />
) : null} ) : null}
<input type="hidden" name="locale" value={currentLocale} />
<input type="hidden" name="source" value="footer" />
<input <input
type="text" type="text"
name="_gotcha" name="_gotcha"

View File

@ -1,11 +1,11 @@
/** /**
* Pagina GDPR: diritti degli interessati e modalita' operative. * Pagina GDPR: diritti degli interessati e modalità operative.
* Documento di supporto alla privacy policy quadro del portale NexStudio. * Documento di supporto alla privacy policy quadro del portale NexStudio.
*/ */
export const gdprDocument = { export const gdprDocument = {
version: '1.0', version: '1.1',
inVigoreDal: '22 aprile 2026', inVigoreDal: '[DATA]',
ultimoAggiornamento: '22 aprile 2026', ultimoAggiornamento: '[DATA]',
} as const; } as const;
export const gdprCrossReferences = { export const gdprCrossReferences = {
@ -24,6 +24,8 @@ export type GdprSection = {
title: string; title: string;
paragraphs: readonly string[]; paragraphs: readonly string[];
bullets?: readonly string[]; bullets?: readonly string[];
/** Paragrafi dopo lelenco puntato. */
paragraphsAfterBullets?: readonly string[];
}; };
export const gdprSections: readonly GdprSection[] = [ export const gdprSections: readonly GdprSection[] = [
@ -32,57 +34,57 @@ export const gdprSections: readonly GdprSection[] = [
title: '1. Quadro normativo e ambito', title: '1. Quadro normativo e ambito',
paragraphs: [ paragraphs: [
"Il Regolamento (UE) 2016/679 (GDPR) tutela i diritti degli interessati e disciplina il trattamento dei dati personali. Questa pagina spiega come esercitare tali diritti verso NexStudio nel contesto del portale corporate e dei servizi collegati.", "Il Regolamento (UE) 2016/679 (GDPR) tutela i diritti degli interessati e disciplina il trattamento dei dati personali. Questa pagina spiega come esercitare tali diritti verso NexStudio nel contesto del portale corporate e dei servizi collegati.",
"La presente pagina integra, ma non sostituisce, l'informativa privacy generale: per categorie dati, finalita' e basi giuridiche si rimanda a `/privacy`; per cookie e tecnologie simili si rimanda a `/cookies`.",
], ],
}, },
{ {
id: 'diritti', id: 'diritti',
title: '2. Diritti riconosciuti agli interessati', title: '2. Diritti riconosciuti agli interessati',
paragraphs: [ paragraphs: [
"Nei limiti previsti dalla normativa applicabile, l'interessato puo' esercitare i seguenti diritti:", "Nei limiti previsti dalla normativa applicabile, l'interessato può esercitare i seguenti diritti:",
], ],
bullets: [ bullets: [
"accesso ai dati personali e ottenimento di copia in formato intelligibile;", 'Accesso ai dati personali e ottenimento di copia in formato intelligibile;',
"rettifica dei dati inesatti e integrazione dei dati incompleti;", 'Rettifica dei dati inesatti e integrazione dei dati incompleti;',
"cancellazione dei dati (diritto all'oblio), quando ne ricorrono i presupposti di legge;", 'Cancellazione dei dati (diritto all\'oblio), quando ne ricorrono i presupposti;',
"limitazione del trattamento in presenza delle condizioni previste dal GDPR;", 'Limitazione del trattamento in presenza delle condizioni previste dal GDPR;',
"portabilita' dei dati trattati con mezzi automatizzati sulla base di consenso o contratto;", 'Portabilità dei dati trattati con mezzi automatizzati sulla base di consenso o contratto;',
"opposizione al trattamento fondato su legittimo interesse, inclusa l'opposizione a comunicazioni marketing ove applicabile;", "Opposizione al trattamento fondato su legittimo interesse, inclusa l'opposizione a comunicazioni marketing;",
"revoca del consenso in qualsiasi momento, senza pregiudicare la liceita' del trattamento precedente;", 'Revoca del consenso in qualsiasi momento, senza pregiudicare la liceità del trattamento precedente;',
"reclamo all'autorita' di controllo competente.", "Reclamo all'autorità di controllo competente.",
], ],
}, },
{ {
id: 'chi-contattare', id: 'chi-contattare',
title: '3. Chi contattare per esercitare i diritti', title: '3. Chi contattare per esercitare i diritti',
paragraphs: [ paragraphs: [],
"Per trattamenti in cui NexStudio agisce come titolare (es. contatti dal sito, gestione richieste corporate, sicurezza infrastrutturale di propria competenza), la richiesta puo' essere inviata tramite i canali di contatto privacy indicati nei riferimenti istituzionali.", bullets: [
"Per trattamenti svolti nei tenant SaaS del cliente (LexAura/MediAura), il primo punto di contatto resta di norma il cliente titolare (es. studio legale, struttura sanitaria, organizzazione cliente), che determina finalita' e mezzi del trattamento.", 'Email: privacy@nexstudio.com',
"NexStudio, ove operi come responsabile del trattamento, supporta il cliente titolare nella gestione delle richieste ricevute, secondo DPA e accordi contrattuali applicabili.", 'Form: nexstudio.ai/contatti',
'Per trattamenti nei tenant SaaS (LexAura/MediAura): rivolgersi prima al cliente titolare (studio legale, struttura sanitaria). NexStudio supporta il cliente titolare secondo DPA e accordi contrattuali.',
],
paragraphsAfterBullets: [
"Autorità di controllo competente: Garante per la protezione dei dati personali (www.garanteprivacy.it) per i trattamenti soggetti al GDPR.",
], ],
}, },
{ {
id: 'identificazione', id: 'identificazione',
title: "4. Verifica dell'identita' e gestione della richiesta", title: "4. Verifica dell'identità e gestione della richiesta",
paragraphs: [ paragraphs: [
"Per tutelare riservatezza e sicurezza, NexStudio puo' richiedere informazioni minime aggiuntive necessarie a verificare l'identita' del richiedente e la legittimazione ad agire.", "Per tutelare riservatezza e sicurezza, NexStudio può richiedere informazioni minime aggiuntive necessarie a verificare l'identità del richiedente. Le richieste sono gestite con procedure tracciate e prese in carico nei tempi previsti dal GDPR; in caso di richieste complesse o multiple, i termini possono essere prorogati nei limiti consentiti dalla legge, con comunicazione motivata.",
"Le richieste sono gestite con procedure tracciate e prese in carico nei tempi previsti dal GDPR; in caso di richieste complesse o multiple, i termini possono essere prorogati nei limiti consentiti dalla legge, con comunicazione motivata.",
], ],
}, },
{ {
id: 'limitazioni', id: 'limitazioni',
title: '5. Casi di limitazione o rigetto', title: '5. Casi di limitazione o rigetto',
paragraphs: [ paragraphs: [
"L'esercizio dei diritti puo' essere limitato o non accolto, in tutto o in parte, quando sussistono obblighi legali prevalenti, esigenze di difesa in giudizio, tutela dei diritti di terzi o altre eccezioni previste dalla normativa.", "L'esercizio dei diritti può essere limitato o non accolto quando sussistono obblighi legali prevalenti, esigenze di difesa in giudizio, tutela dei diritti di terzi o altre eccezioni previste dalla normativa. In tali casi NexStudio fornisce un riscontro motivato e indica i rimedi disponibili, incluso il diritto di proporre reclamo all'autorità competente.",
"In tali casi NexStudio fornisce, ove possibile, un riscontro motivato e indica i rimedi disponibili, incluso il diritto di proporre reclamo all'autorita' competente.",
], ],
}, },
{ {
id: 'trasferimenti', id: 'trasferimenti',
title: '6. Trasferimenti internazionali e garanzie', title: '6. Trasferimenti internazionali e garanzie',
paragraphs: [ paragraphs: [
"Qualora il trattamento comporti trasferimenti extra SEE/UE, NexStudio adotta le misure richieste dalla normativa (es. clausole contrattuali standard e misure supplementari tecniche/organizzative) secondo quanto descritto nell'informativa privacy.", "Qualora il trattamento comporti trasferimenti extra SEE/UE, NexStudio adotta le misure richieste dalla normativa (es. clausole contrattuali standard e misure supplementari). I dati risiedono primariamente in UE via infrastruttura Cloudflare.",
"Le informazioni rilevanti sui trasferimenti possono essere fornite all'interessato nei limiti consentiti e in coerenza con obblighi contrattuali e di sicurezza.",
], ],
}, },
{ {
@ -90,7 +92,11 @@ export const gdprSections: readonly GdprSection[] = [
title: '7. Coordinamento con le privacy dei SaaS', title: '7. Coordinamento con le privacy dei SaaS',
paragraphs: [ paragraphs: [
"Le privacy policy dei portali SaaS LexAura e MediAura dettagliano, per ciascun prodotto, flussi dati, tempi di conservazione, ruoli privacy e canali dedicati. In caso di differenze tra documenti, prevalgono i documenti specifici del servizio e gli accordi contrattuali applicabili.", "Le privacy policy dei portali SaaS LexAura e MediAura dettagliano, per ciascun prodotto, flussi dati, tempi di conservazione, ruoli privacy e canali dedicati. In caso di differenze tra documenti, prevalgono i documenti specifici del servizio e gli accordi contrattuali applicabili.",
"Questa pagina mantiene funzione di riferimento generale sul quadro GDPR del gruppo di servizi NexStudio.", 'Per consultare le privacy dei prodotti:',
],
bullets: [
'LexAura: vedere il portale dedicato',
'MediAura: mediaura.doctor/privacy',
], ],
}, },
] as const; ] as const;

View File

@ -45,9 +45,9 @@ const footerByLocale = {
description: 'Aggiornamenti su prodotti e roadmap.', description: 'Aggiornamenti su prodotti e roadmap.',
placeholder: 'La tua email', placeholder: 'La tua email',
buttonLabel: 'Iscriviti', buttonLabel: 'Iscriviti',
actionUrl: '', actionUrl: '/api/newsletter/subscribe',
fieldName: 'email', fieldName: 'email',
successRedirect: '', successRedirect: '/newsletter/grazie',
notifySubject: 'Nuova iscrizione newsletter — NexStudio', notifySubject: 'Nuova iscrizione newsletter — NexStudio',
}, },
labels: { labels: {
@ -101,9 +101,9 @@ const footerByLocale = {
description: 'Updates on products and roadmap.', description: 'Updates on products and roadmap.',
placeholder: 'Your email', placeholder: 'Your email',
buttonLabel: 'Subscribe', buttonLabel: 'Subscribe',
actionUrl: '', actionUrl: '/api/newsletter/subscribe',
fieldName: 'email', fieldName: 'email',
successRedirect: '', successRedirect: '/en/newsletter/grazie',
notifySubject: 'New newsletter signup — NexStudio', notifySubject: 'New newsletter signup — NexStudio',
}, },
labels: { labels: {
@ -157,9 +157,9 @@ const footerByLocale = {
description: 'อัปเดตเกี่ยวกับผลิตภัณฑ์และโรดแมป', description: 'อัปเดตเกี่ยวกับผลิตภัณฑ์และโรดแมป',
placeholder: 'อีเมลของคุณ', placeholder: 'อีเมลของคุณ',
buttonLabel: 'สมัครรับข่าวสาร', buttonLabel: 'สมัครรับข่าวสาร',
actionUrl: '', actionUrl: '/api/newsletter/subscribe',
fieldName: 'email', fieldName: 'email',
successRedirect: '', successRedirect: '/th/newsletter/grazie',
notifySubject: 'มีผู้สมัครจดหมายข่าวใหม่ — NexStudio', notifySubject: 'มีผู้สมัครจดหมายข่าวใหม่ — NexStudio',
}, },
labels: { labels: {

View File

@ -3,14 +3,14 @@
* Questo file centralizza i contenuti della pagina /privacy per facilitare revisioni legali. * Questo file centralizza i contenuti della pagina /privacy per facilitare revisioni legali.
*/ */
export const privacyDocument = { export const privacyDocument = {
version: '1.0', version: '1.1',
inVigoreDal: '22 aprile 2026', inVigoreDal: '[DATA]',
ultimoAggiornamento: '22 aprile 2026', ultimoAggiornamento: '[DATA]',
} as const; } as const;
export const privacyCrossReferences = { export const privacyCrossReferences = {
lead: lead:
"L'informativa distingue il portale istituzionale NexStudio dai servizi SaaS LexAura e MediAura, che rispondono a esigenze operative e livelli di rischio differenti; le informative privacy specifiche dei singoli SaaS, pubblicate nei rispettivi siti/portali applicativi, restano il riferimento di dettaglio per l'uso del prodotto.", "L'informativa distingue il portale istituzionale NexStudio dai servizi SaaS LexAura e MediAura; le informative privacy specifiche dei singoli SaaS, pubblicate nei rispettivi portali applicativi, restano il riferimento di dettaglio per l'uso del prodotto.",
} as const; } as const;
export const privacyRiferimenti: readonly { label: string; href: string }[] = [ export const privacyRiferimenti: readonly { label: string; href: string }[] = [
@ -24,6 +24,8 @@ export type PrivacySection = {
title: string; title: string;
paragraphs: readonly string[]; paragraphs: readonly string[];
bullets?: readonly string[]; bullets?: readonly string[];
/** Paragrafi dopo lelenco puntato (es. nota DPO in §2). */
paragraphsAfterBullets?: readonly string[];
}; };
export const privacySections: readonly PrivacySection[] = [ export const privacySections: readonly PrivacySection[] = [
@ -31,106 +33,112 @@ export const privacySections: readonly PrivacySection[] = [
id: 'ambito', id: 'ambito',
title: '1. Ambito della presente informativa', title: '1. Ambito della presente informativa',
paragraphs: [ paragraphs: [
"La presente informativa descrive il quadro generale con cui NexStudio tratta i dati personali raccolti tramite il sito istituzionale pubblico e i canali corporate (contatti, richieste demo, onboarding commerciale) collegati anche ai prodotti LexAura (Legal Tech) e MediAura (Health Tech).", "La presente informativa descrive il quadro generale con cui NexStudio S.r.l. tratta i dati personali raccolti tramite il sito istituzionale pubblico e i canali corporate (collegati anche ai prodotti LexAura e MediAura).",
"Per l'uso operativo dei SaaS (area riservata, tenant cliente, funzionalita' applicative), il livello di dettaglio e il ruolo privacy possono variare: NexStudio puo' operare come titolare, contitolare o responsabile del trattamento a seconda del servizio concretamente attivato e degli accordi contrattuali sottoscritti con il cliente business.", "Per l'uso operativo dei SaaS, il livello di dettaglio e il ruolo privacy possono variare: NexStudio può operare come titolare, contitolare o responsabile del trattamento a seconda del servizio e degli accordi contrattuali.",
"In caso di conflitto tra questa pagina e un Data Processing Agreement (DPA), un contratto quadro o condizioni speciali firmate, prevalgono i documenti contrattuali specifici per il servizio acquistato.", "In caso di conflitto tra questa pagina e un Data Processing Agreement (DPA), un contratto quadro o condizioni speciali firmate, prevalgono i documenti contrattuali specifici per il servizio acquistato.",
], ],
}, },
{ {
id: 'titolare', id: 'titolare',
title: "2. Identita' del titolare e punti di contatto", title: "2. Identità del titolare e punti di contatto",
paragraphs: [ paragraphs: [
"Titolare del trattamento: NexStudio (sede operativa a Bangkok, Thailandia). I dati anagrafici societari completi e il recapito privacy ufficiale sono indicati nei contatti istituzionali e nei documenti contrattuali applicabili.", "Titolare del trattamento: NexStudio S.r.l., con sede legale in Italia, [INDIRIZZO_COMPLETO], P.IVA [DA_INSERIRE].",
"Canali per richieste privacy: sezione contatti del sito, recapito email privacy dedicato ove attivo, ed eventuali canali autenticati/ticket previsti per i clienti SaaS. Se nominato, il DPO/Privacy Lead e' indicato con i relativi contatti.", "Canali per richieste privacy:",
],
bullets: [
'Email: privacy@nexstudio.com',
'Form di contatto su nexstudio.ai',
'Per i clienti SaaS: canali autenticati/ticket previsti nei rispettivi portali (LexAura, MediAura)',
],
paragraphsAfterBullets: [
'Il Responsabile della Protezione dei Dati (DPO) è contattabile agli stessi recapiti.',
], ],
}, },
{ {
id: 'categorie-dati', id: 'categorie-dati',
title: '3. Categorie di dati trattati', title: '3. Categorie di dati trattati',
paragraphs: [ paragraphs: [
"Le categorie di dati variano in funzione del canale utilizzato e dei moduli/feature attivi. Di seguito il perimetro tipico a livello corporate; i dettagli di prodotto sono disciplinati nelle privacy dedicate dei singoli SaaS.", "Le categorie di dati variano in funzione del canale utilizzato e dei moduli/feature attivi.",
], ],
bullets: [ bullets: [
"Portale NexStudio: dati di navigazione (log tecnici, IP, user agent), dati di contatto inviati volontariamente (nome, email, azienda, contenuto del messaggio), preferenze cookie e metriche aggregate.", "Portale NexStudio: dati di navigazione (log tecnici, IP, user agent), dati di contatto inviati volontariamente (nome, email, azienda, contenuto del messaggio), preferenze cookie e metriche aggregate.",
"LexAura (Legal Tech): dati di account e profilo utente, metadati di workspace, documenti giuridici caricati o generati, richieste testuali ai motori di analisi/sintesi, log di audit su accessi e operazioni.", "LexAura (Legal Tech): dati di account e profilo utente, metadati di workspace, documenti giuridici caricati o generati, richieste testuali ai motori di analisi/sintesi, log di audit su accessi e operazioni.",
"MediAura (Health Tech): dati di account e ruoli, metadati organizzativi di struttura/reparto, documentazione sanitaria o amministrativa trattata nel servizio, input vocali e relative trascrizioni ove la funzione sia attivata, log tecnici e di sicurezza.", "MediAura (Health Tech): dati di account e ruoli, metadati organizzativi, documentazione sanitaria o amministrativa, input vocali e relative trascrizioni, log tecnici e di sicurezza.",
"Categorie particolari ex art. 9 GDPR: possono emergere in MediAura (e, in specifici casi, in LexAura) quando i clienti caricano contenuti che includono dati sanitari o altri dati sensibili; il trattamento avviene solo nei limiti del servizio e delle istruzioni ricevute dal cliente titolare.", "Categorie particolari ex art. 9 GDPR: possono emergere in MediAura (e, in specifici casi, in LexAura) quando i clienti caricano contenuti che includono dati sanitari o altri dati sensibili; il trattamento avviene solo nei limiti del servizio e delle istruzioni ricevute dal cliente titolare.",
], ],
}, },
{ {
id: 'finalita-basi', id: 'finalita-basi',
title: "4. Finalita' e basi giuridiche del trattamento", title: '4. Finalità e basi giuridiche del trattamento',
paragraphs: [ paragraphs: [],
"NexStudio tratta i dati personali per finalita' determinate e legittime, applicando la base giuridica adeguata al singolo trattamento.",
],
bullets: [ bullets: [
"Gestione del sito istituzionale, sicurezza e continuita' operativa (base: legittimo interesse alla sicurezza, prevenzione abusi e mantenimento del servizio).", "Gestione del sito istituzionale, sicurezza e continuità operativa (base: legittimo interesse alla sicurezza, prevenzione abusi e mantenimento del servizio).",
"Gestione richieste pre-contrattuali, demo, contatti commerciali e supporto (base: misure precontrattuali su richiesta dell'interessato; legittimo interesse nella gestione B2B).", "Gestione richieste pre-contrattuali, demo, contatti commerciali e supporto (base: misure precontrattuali su richiesta dell'interessato; legittimo interesse nella gestione B2B).",
"Erogazione dei SaaS LexAura e MediAura, autenticazione utenti, gestione tenant e assistenza tecnica (base: esecuzione del contratto con il cliente business).", "Erogazione dei SaaS LexAura e MediAura, autenticazione utenti, gestione tenant e assistenza tecnica (base: esecuzione del contratto con il cliente business).",
"Conformita' normativa, adempimenti amministrativi/fiscali, gestione contenziosi e difesa di diritti (base: obbligo legale e legittimo interesse alla tutela in giudizio).", "Conformità normativa, adempimenti amministrativi/fiscali, gestione contenziosi e difesa di diritti (base: obbligo legale e legittimo interesse alla tutela in giudizio).",
"Comunicazioni marketing/newsletter, ove attive (base: consenso, salvo eccezioni di soft spam consentite dalla normativa applicabile).", "Comunicazioni marketing/newsletter, ove attive (base: consenso, salvo eccezioni di soft spam consentite dalla normativa applicabile).",
"Attivita' di miglioramento del servizio, quality assurance e sviluppo funzionalita' IA con dati del cliente solo se previsto dal contratto o da istruzioni documentate; in assenza di tale base i dati non sono utilizzati per training generalista.", "Miglioramento del servizio e sviluppo funzionalità IA: solo se previsto dal contratto o da istruzioni documentate; in assenza di tale base i dati non sono utilizzati per training generalista.",
], ],
}, },
{ {
id: 'ruoli-saas', id: 'ruoli-saas',
title: '5. Ruoli privacy specifici per LexAura e MediAura', title: '5. Ruoli privacy specifici per LexAura e MediAura',
paragraphs: [ paragraphs: [
"Per i servizi SaaS B2B, il cliente (studio legale, impresa, struttura sanitaria, professionista) determina normalmente finalita' e mezzi del trattamento relativi ai dati caricati nel proprio ambiente applicativo, agendo come titolare del trattamento.", "Per i servizi SaaS B2B, il cliente determina normalmente finalità e mezzi del trattamento relativi ai dati caricati nel proprio ambiente applicativo, agendo come titolare del trattamento. NexStudio agisce di regola come responsabile del trattamento per conto del cliente, con nomina contrattuale e istruzioni documentate.",
"NexStudio agisce di regola come responsabile del trattamento per conto del cliente, con nomina contrattuale e istruzioni documentate, limitando l'accesso ai dati al personale autorizzato che ne abbia necessita' operativa (supporto, manutenzione, sicurezza).", "Qualora NexStudio determini autonomamente finalità specifiche (es. prevenzione frodi, sicurezza infrastrutturale, metriche aggregate non riconducibili al singolo interessato), agisce come titolare autonomo per tali trattamenti.",
"Qualora NexStudio determini autonomamente finalita' specifiche (es. prevenzione frodi, sicurezza infrastrutturale, metriche aggregate non riconducibili al singolo interessato), agisce come titolare autonomo per tali trattamenti.",
"Le informative privacy dedicate di LexAura e MediAura, pubblicate nei rispettivi siti o ambienti applicativi, approfondiscono categorie dati, flussi, tempi di retention e basi giuridiche specifiche del singolo prodotto.", "Le informative privacy dedicate di LexAura e MediAura, pubblicate nei rispettivi siti o ambienti applicativi, approfondiscono categorie dati, flussi, tempi di retention e basi giuridiche specifiche del singolo prodotto.",
], ],
}, },
{ {
id: 'conservazione', id: 'conservazione',
title: '6. Tempi di conservazione', title: '6. Tempi di conservazione',
paragraphs: [ paragraphs: [],
"I tempi di conservazione sono definiti in base a principio di minimizzazione, obblighi di legge e ciclo di vita del servizio. Durate puntuali possono essere regolate da contratto, configurazioni del cliente e policy interne.",
],
bullets: [ bullets: [
"Dati di contatto dal sito: per il tempo necessario alla gestione della richiesta e, se pertinente, per il successivo follow-up commerciale entro termini ragionevoli.", "Dati di contatto dal sito: 24 mesi dall'ultima interazione.",
"Dati di account SaaS: per tutta la durata del rapporto contrattuale e per il periodo tecnico-amministrativo necessario a chiusura, backup, audit e adempimenti post-contratto.", "Dati di account SaaS: per tutta la durata del rapporto contrattuale + 30 giorni dalla cancellazione.",
"Log tecnici e di sicurezza: retention proporzionata a monitoraggio, incident response, prevenzione abusi e dimostrazione di conformita'.", "Log tecnici e di sicurezza: massimo 30 giorni.",
"Dati contrattuali/amministrativi: conservazione secondo i termini imposti dalla normativa fiscale, civilistica e regolatoria applicabile.", "Dati di fatturazione: 10 anni come richiesto dalla normativa fiscale italiana.",
"Dati sanitari: secondo le istruzioni del titolare (cliente) e gli obblighi di legge.",
"Dati analitici aggregati: indefinitamente in forma anonima.",
], ],
}, },
{ {
id: 'destinatari', id: 'destinatari',
title: '7. Destinatari e categorie di soggetti autorizzati', title: '7. Destinatari e categorie di soggetti autorizzati',
paragraphs: [ paragraphs: [
"I dati possono essere trattati da personale NexStudio autorizzato e istruito, nonche' da fornitori qualificati che operano come responsabili/sub-responsabili (es. hosting cloud, servizi infrastrutturali, monitoraggio sicurezza, strumenti di comunicazione e assistenza).", "I dati possono essere trattati da personale NexStudio autorizzato e istruito, nonché da fornitori qualificati che operano come responsabili/sub-responsabili (es. hosting cloud, servizi infrastrutturali, monitoraggio sicurezza, strumenti di comunicazione).",
"L'elenco aggiornato dei responsabili e sub-responsabili rilevanti per i clienti SaaS e' messo a disposizione nei canali contrattuali o su richiesta, secondo quanto previsto dagli accordi in essere.", "L'elenco aggiornato dei responsabili e sub-responsabili rilevanti per i clienti SaaS è messo a disposizione nei canali contrattuali o su richiesta.",
], ],
}, },
{ {
id: 'trasferimenti', id: 'trasferimenti',
title: '8. Trasferimenti internazionali di dati', title: '8. Trasferimenti internazionali di dati',
paragraphs: [ paragraphs: [
"In ragione della struttura operativa internazionale e dell'uso di fornitori cloud globali, alcuni trattamenti possono comportare trasferimenti verso Paesi extra SEE/UE.", "In ragione dell'uso di fornitori cloud globali, alcuni trattamenti possono comportare trasferimenti verso Paesi extra SEE/UE. Quando richiesto dalla normativa, NexStudio adotta garanzie adeguate (es. clausole contrattuali standard, misure supplementari tecniche/organizzative, valutazioni di impatto sul trasferimento) e limita il trasferimento ai soli dati necessari.",
"Quando richiesto dalla normativa, NexStudio adotta garanzie adeguate (es. clausole contrattuali standard, misure supplementari tecniche/organizzative, valutazioni di impatto sul trasferimento) e limita il trasferimento ai soli dati necessari.", "I dati sono preferibilmente trattati nello Spazio Economico Europeo. Cloudflare Inc. aderisce al Data Privacy Framework UE-USA e adotta Clausole Contrattuali Standard (SCC).",
], ],
}, },
{ {
id: 'sicurezza', id: 'sicurezza',
title: '9. Misure di sicurezza e governance', title: '9. Misure di sicurezza e governance',
paragraphs: [ paragraphs: [
"NexStudio adotta misure tecniche e organizzative proporzionate al rischio, con particolare attenzione ai contesti Legal Tech e Health Tech dove integrita', riservatezza e tracciabilita' sono requisiti essenziali.", "NexStudio adotta misure tecniche e organizzative proporzionate al rischio:",
], ],
bullets: [ bullets: [
"Controlli di accesso basati su ruoli, autenticazione, segregazione logica degli ambienti cliente e principio del minimo privilegio.", "Controlli di accesso basati su ruoli, autenticazione, segregazione logica degli ambienti cliente e principio del minimo privilegio.",
"Cifratura dei dati in transito e, ove previsto dall'architettura, a riposo; gestione sicura delle chiavi e dei segreti applicativi.", "Cifratura dei dati in transito (TLS 1.3) e a riposo; gestione sicura delle chiavi.",
"Logging, monitoraggio, audit trail e procedure di gestione incidenti con escalation interna e tracciamento delle azioni correttive.", "Logging, monitoraggio, audit trail e procedure di gestione incidenti.",
"Procedure di backup, continuita' operativa e disaster recovery in funzione del piano di servizio sottoscritto.", "Procedure di backup, continuità operativa e disaster recovery.",
], ],
}, },
{ {
id: 'diritti', id: 'diritti',
title: "10. Diritti degli interessati e modalita' di esercizio", title: "10. Diritti degli interessati e modalità di esercizio",
paragraphs: [ paragraphs: [
"Gli interessati possono esercitare i diritti previsti dalla normativa applicabile (accesso, rettifica, cancellazione, limitazione, opposizione, portabilita', revoca del consenso ove prestato, reclamo all'autorita' competente).", "Gli interessati possono esercitare i diritti previsti dalla normativa applicabile (accesso, rettifica, cancellazione, limitazione, opposizione, portabilità, revoca del consenso, reclamo all'autorità competente).",
"Per dati trattati nel contesto dei SaaS, la richiesta va di norma indirizzata prima al cliente titolare (es. studio legale o struttura sanitaria) che determina finalita' e mezzi del trattamento. NexStudio fornisce supporto al cliente titolare secondo contratto.", "Per dati trattati nel contesto dei SaaS, la richiesta va di norma indirizzata prima al cliente titolare (es. studio legale o struttura sanitaria). NexStudio fornisce supporto al cliente titolare secondo contratto.",
"Per dati trattati da NexStudio come titolare (es. contatti dal sito, log di sicurezza propri), le richieste possono essere inviate ai recapiti privacy indicati in questa pagina o nella sezione contatti del sito.", "Per dati trattati da NexStudio come titolare (es. contatti dal sito, log di sicurezza propri), le richieste possono essere inviate a privacy@nexstudio.com o tramite il form su nexstudio.ai.",
"Autorità di controllo competente: Garante per la protezione dei dati personali (www.garanteprivacy.it).",
], ],
}, },
{ {
@ -138,23 +146,20 @@ export const privacySections: readonly PrivacySection[] = [
title: '11. Cookie, analytics e tracciamenti', title: '11. Cookie, analytics e tracciamenti',
paragraphs: [ paragraphs: [
"Le informazioni su cookie tecnici, eventuali cookie analitici e strumenti di tracciamento sono descritte nella Cookie Policy, da leggere congiuntamente alla presente informativa.", "Le informazioni su cookie tecnici, eventuali cookie analitici e strumenti di tracciamento sono descritte nella Cookie Policy, da leggere congiuntamente alla presente informativa.",
"La gestione delle preferenze cookie avviene tramite banner/strumenti dedicati, con possibilita' di revisione successiva delle scelte quando tecnicamente disponibile.",
], ],
}, },
{ {
id: 'minori', id: 'minori',
title: '12. Trattamento dati di minori', title: '12. Trattamento dati di minori',
paragraphs: [ paragraphs: [
"I servizi NexStudio sono progettati per un'utenza professionale e business. Non sono rivolti intenzionalmente a minori e non viene perseguita la raccolta volontaria di dati di minori al di fuori dei casi strettamente necessari e leciti nel contesto sanitario gestito dal cliente titolare.", "I servizi NexStudio sono progettati per un'utenza professionale e business. Non sono rivolti intenzionalmente a minori.",
"Se viene rilevata una raccolta non appropriata di dati riferibili a minori al di fuori delle basi giuridiche consentite, NexStudio adotta le misure necessarie per la cancellazione o la regolarizzazione del trattamento, coordinandosi con il cliente titolare ove necessario.",
], ],
}, },
{ {
id: 'aggiornamenti', id: 'aggiornamenti',
title: '13. Aggiornamenti della policy', title: '13. Aggiornamenti della policy',
paragraphs: [ paragraphs: [
"NexStudio puo' aggiornare questa informativa per adeguamenti normativi, evoluzione dei servizi, modifiche organizzative o introduzione di nuove funzionalita'.", "NexStudio può aggiornare questa informativa per adeguamenti normativi, evoluzione dei servizi, modifiche organizzative o introduzione di nuove funzionalità. Le modifiche rilevanti saranno pubblicate su questa pagina con aggiornamento di versione e data.",
"Le modifiche rilevanti saranno pubblicate su questa pagina con aggiornamento di versione e data. Per i clienti SaaS, ulteriori notifiche possono essere fornite tramite canali contrattuali o in-app, quando previsto.",
], ],
}, },
] as const; ] as const;

View File

@ -4,8 +4,8 @@
* Aggiornare versione e data in sync con la pagina (box in testa). * Aggiornare versione e data in sync con la pagina (box in testa).
*/ */
export const termsDocument = { export const termsDocument = {
version: '1.0', version: '1.1',
inVigoreDal: '22 aprile 2026', inVigoreDal: '[DATA]',
} as const; } as const;
export type TermsSection = { export type TermsSection = {
@ -20,7 +20,7 @@ export const termsSections: readonly TermsSection[] = [
id: 'premessa', id: 'premessa',
title: '1. Premessa e ambito', title: '1. Premessa e ambito',
paragraphs: [ paragraphs: [
"Le presenti condizioni duso regolano laccesso e la consultazione del sito web di NexStudio (di seguito «NexStudio», «noi») e dei contenuti informativi resi disponibili online. La sede operativa di riferimento è a Bangkok, Thailandia; i servizi software commerciali (inclusi prodotti e soluzioni per Legal Tech e Health Tech, ad es. le linee LexAura e MediAura) sono regolati da condizioni generali, ordini, offerte o accordi specifici sottoscritti con i clienti: in caso di conflitto tra il presente testo e un contratto B2B sottoscritto, prevalgono i documenti specifici del servizio e gli accordi contrattuali applicabili.", "Le presenti condizioni duso regolano laccesso e la consultazione del sito web di NexStudio S.r.l. (di seguito «NexStudio», «noi»), con sede legale in Italia, [INDIRIZZO_COMPLETO]. I servizi software commerciali (inclusi i prodotti LexAura e MediAura) sono regolati da condizioni generali, ordini, offerte o accordi specifici sottoscritti con i clienti: in caso di conflitto tra il presente testo e un contratto B2B sottoscritto, prevalgono i documenti specifici del servizio e gli accordi contrattuali applicabili.",
"Per il trattamento dei dati personali e i cookie si rimanda rispettivamente allinformativa privacy e alla cookie policy, citate più sotto.", "Per il trattamento dei dati personali e i cookie si rimanda rispettivamente allinformativa privacy e alla cookie policy, citate più sotto.",
], ],
}, },
@ -83,16 +83,16 @@ export const termsSections: readonly TermsSection[] = [
}, },
{ {
id: 'legge', id: 'legge',
title: '9. Legge applicabile e foro (da confermare con il legale)', title: '9. Legge applicabile e foro competente',
paragraphs: [ paragraphs: [
"Salvo disposizioni inderogabili, le presenti condizioni e ogni contestazione inerente al sito informativo sono regolate dalla legge indicata qui sotto, con foro competente ove ammesso (da definire in base alla entità operativa, ai clienti e al mercato: es. Tailandia, o altra scelta con parere legale).", "Salvo disposizioni inderogabili a tutela del consumatore, le presenti condizioni e ogni contestazione inerente al sito informativo sono regolate dalla legge italiana. Il foro competente è Padova.",
], ],
}, },
{ {
id: 'contatti-termini', id: 'contatti-termini',
title: '10. Contatti in merito a queste condizioni', title: '10. Contatti in merito a queste condizioni',
paragraphs: [ paragraphs: [
"Per domande sulle presenti condizioni duso del sito, utilizzare i contatti e i canali indicati nel sito. Per trattamento dei dati personali e diritti, seguire linformativa privacy; per cookie, la cookie policy.", "Per domande sulle presenti condizioni duso del sito, utilizzare i canali indicati nella sezione contatti del sito. Per trattamento dei dati personali e diritti: privacy@nexstudio.com o il form su nexstudio.ai.",
], ],
}, },
] as const; ] as const;
@ -106,5 +106,5 @@ export const termsRiferimenti: readonly { label: string; href: string }[] = [
export const termsCrossReferences = { export const termsCrossReferences = {
leadNote: leadNote:
'I documenti collegati vanno allineati alla stessa data di revisione ove si tratti di un pacchetto unico di pubblicazione; per LexAura e MediAura, le informative privacy dedicate pubblicate sui portali SaaS restano il riferimento di dettaglio del singolo prodotto.', "Per LexAura e MediAura, le informative privacy dedicate pubblicate sui rispettivi portali SaaS restano il riferimento di dettaglio del singolo prodotto.",
} as const; } as const;

47
src/lib/email.ts Normal file
View File

@ -0,0 +1,47 @@
/**
* Outbound email via Cloudflare Email Service (`send_email` binding).
*/
import { env as workerEnv } from 'cloudflare:workers';
export type RuntimeEnv = Record<string, unknown>;
type EmailBinding = {
send: (message: {
to: string;
from: string;
subject: string;
text?: string;
html?: string;
replyTo?: string;
}) => Promise<{ messageId: string }>;
};
export function getEmailBinding(runtimeEnv?: RuntimeEnv): EmailBinding | null {
const env = (runtimeEnv ?? (workerEnv as unknown as RuntimeEnv)) as RuntimeEnv;
const binding = env.EMAIL;
if (binding && typeof binding === 'object' && 'send' in binding) {
return binding as EmailBinding;
}
return null;
}
export async function sendCloudflareEmail(
email: EmailBinding,
options: {
fromEmail: string;
toEmail: string;
subject: string;
text: string;
html?: string;
replyTo?: string;
},
): Promise<void> {
await email.send({
from: options.fromEmail,
to: options.toEmail,
subject: options.subject,
text: options.text,
...(options.html ? { html: options.html } : {}),
...(options.replyTo ? { replyTo: options.replyTo } : {}),
});
}

109
src/lib/newsletter.ts Normal file
View File

@ -0,0 +1,109 @@
/**
* Shared helpers for newsletter API routes (Cloudflare Worker / Astro).
*/
import { env as workerEnv } from 'cloudflare:workers';
export type RuntimeEnv = Record<string, unknown>;
export type SubscriberStatus = 'pending' | 'active' | 'unsubscribed';
export type NewsletterSubscriber = {
id: number;
email: string;
status: SubscriberStatus;
locale: string;
confirm_token: string | null;
unsubscribe_token: string;
source: string;
created_at: string;
confirmed_at: string | null;
unsubscribed_at: string | null;
};
/** Astro v6+: use `cloudflare:workers` env (locals.runtime.env was removed). */
export function getRuntimeEnv(_locals?: unknown): RuntimeEnv {
return workerEnv as unknown as RuntimeEnv;
}
export function readEnv(name: string, runtimeEnv: RuntimeEnv): string {
const runtimeValue = runtimeEnv[name];
if (typeof runtimeValue === 'string' && runtimeValue.trim().length > 0) {
return runtimeValue.trim();
}
const staticValue = import.meta.env[name];
return typeof staticValue === 'string' ? staticValue.trim() : '';
}
export function getNewsletterDb(runtimeEnv: RuntimeEnv): D1Database | null {
const db = runtimeEnv.NEWSLETTER_DB;
if (db && typeof db === 'object' && 'prepare' in db) {
return db as D1Database;
}
return null;
}
export function sanitizeRedirect(candidate: string, requestUrl: URL): string {
if (!candidate) return '';
try {
const u = new URL(candidate, requestUrl);
if (u.origin !== requestUrl.origin) return '';
return u.pathname + u.search + u.hash;
} catch {
return '';
}
}
export function required(value: FormDataEntryValue | null): string {
return typeof value === 'string' ? value.trim() : '';
}
export function isValidEmail(email: string): boolean {
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email) && email.length <= 254;
}
export function normalizeEmail(email: string): string {
return email.trim().toLowerCase();
}
export function createToken(): string {
const bytes = new Uint8Array(32);
crypto.getRandomValues(bytes);
return [...bytes].map((b) => b.toString(16).padStart(2, '0')).join('');
}
export async function verifyTurnstile(
token: string,
secret: string,
ip?: string | null,
): Promise<boolean> {
const body = new URLSearchParams();
body.set('secret', secret);
body.set('response', token);
if (ip) body.set('remoteip', ip);
const response = await fetch('https://challenges.cloudflare.com/turnstile/v0/siteverify', {
method: 'POST',
body,
});
if (!response.ok) return false;
const payload = (await response.json()) as { success?: boolean };
return payload.success === true;
}
export { getEmailBinding, sendCloudflareEmail } from './email';
export function siteOrigin(requestUrl: URL, runtimeEnv: RuntimeEnv): string {
const configured = readEnv('PUBLIC_SITE_URL', runtimeEnv) || readEnv('SITE', runtimeEnv);
if (configured) {
try {
return new URL(configured).origin;
} catch {
/* fall through */
}
}
return requestUrl.origin;
}
export function absoluteUrl(origin: string, path: string): string {
return new URL(path, origin).toString();
}

View File

@ -1,23 +1,13 @@
import type { APIRoute } from 'astro'; import type { APIRoute } from 'astro';
import { env as workerEnv } from 'cloudflare:workers';
import { getEmailBinding, sendCloudflareEmail } from '../../lib/email';
export const prerender = false; export const prerender = false;
type RuntimeEnv = Record<string, unknown>; type RuntimeEnv = Record<string, unknown>;
function getRuntimeEnv(locals: unknown): RuntimeEnv { function getRuntimeEnv(): RuntimeEnv {
if ( return workerEnv as unknown as RuntimeEnv;
locals &&
typeof locals === 'object' &&
'runtime' in locals &&
locals.runtime &&
typeof locals.runtime === 'object' &&
'env' in locals.runtime &&
locals.runtime.env &&
typeof locals.runtime.env === 'object'
) {
return locals.runtime.env as RuntimeEnv;
}
return {};
} }
function readEnv(name: string, runtimeEnv: RuntimeEnv): string { function readEnv(name: string, runtimeEnv: RuntimeEnv): string {
@ -58,27 +48,6 @@ async function verifyTurnstile(token: string, secret: string, ip?: string | null
return payload.success === true; return payload.success === true;
} }
async function sendWithResend(apiKey: string, fromEmail: string, toEmail: string, subject: string, text: string) {
const response = await fetch('https://api.resend.com/emails', {
method: 'POST',
headers: {
Authorization: `Bearer ${apiKey}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
from: fromEmail,
to: [toEmail],
subject,
text,
}),
});
if (!response.ok) {
const detail = await response.text();
throw new Error(`Resend error ${response.status}: ${detail}`);
}
}
function resolveDepartmentRecipient(runtimeEnv: RuntimeEnv, defaultRecipient: string, department: string): string { function resolveDepartmentRecipient(runtimeEnv: RuntimeEnv, defaultRecipient: string, department: string): string {
const normalized = department.trim().toLowerCase(); const normalized = department.trim().toLowerCase();
const map: Record<string, string> = { const map: Record<string, string> = {
@ -89,10 +58,9 @@ function resolveDepartmentRecipient(runtimeEnv: RuntimeEnv, defaultRecipient: st
return map[normalized] || defaultRecipient; return map[normalized] || defaultRecipient;
} }
export const POST: APIRoute = async ({ request, locals, url }) => { export const POST: APIRoute = async ({ request, url }) => {
const runtimeEnv = getRuntimeEnv(locals); const runtimeEnv = getRuntimeEnv();
const mode = readEnv('CONTACT_FORM_MODE', runtimeEnv).toLowerCase() || 'dev'; const mode = readEnv('CONTACT_FORM_MODE', runtimeEnv).toLowerCase() || 'dev';
const resendApiKey = readEnv('CONTACT_FORM_RESEND_API_KEY', runtimeEnv);
const fromEmail = readEnv('CONTACT_FORM_FROM_EMAIL', runtimeEnv); const fromEmail = readEnv('CONTACT_FORM_FROM_EMAIL', runtimeEnv);
const toEmail = readEnv('CONTACT_FORM_TO_EMAIL', runtimeEnv); const toEmail = readEnv('CONTACT_FORM_TO_EMAIL', runtimeEnv);
const turnstileSecret = readEnv('CONTACT_TURNSTILE_SECRET_KEY', runtimeEnv); const turnstileSecret = readEnv('CONTACT_TURNSTILE_SECRET_KEY', runtimeEnv);
@ -141,13 +109,23 @@ export const POST: APIRoute = async ({ request, locals, url }) => {
.join('\n'); .join('\n');
if (mode === 'live') { if (mode === 'live') {
if (!resendApiKey || !fromEmail || !toEmail) { const emailBinding = getEmailBinding(runtimeEnv);
return new Response('Live mode requires CONTACT_FORM_RESEND_API_KEY, CONTACT_FORM_FROM_EMAIL and CONTACT_FORM_TO_EMAIL.', { if (!emailBinding || !fromEmail || !toEmail) {
status: 500, return new Response(
}); 'Live mode requires EMAIL binding (Cloudflare Email Service), CONTACT_FORM_FROM_EMAIL and CONTACT_FORM_TO_EMAIL.',
{
status: 500,
},
);
} }
const destination = resolveDepartmentRecipient(runtimeEnv, toEmail, department); const destination = resolveDepartmentRecipient(runtimeEnv, toEmail, department);
await sendWithResend(resendApiKey, fromEmail, destination, subject, text); await sendCloudflareEmail(emailBinding, {
fromEmail,
toEmail: destination,
subject,
text,
replyTo: email,
});
} else { } else {
console.info('[contact:dev] Contact payload received', { console.info('[contact:dev] Contact payload received', {
name, name,

View File

@ -0,0 +1,176 @@
import type { APIRoute } from 'astro';
import {
absoluteUrl,
getEmailBinding,
getNewsletterDb,
getRuntimeEnv,
readEnv,
sendCloudflareEmail,
siteOrigin,
type NewsletterSubscriber,
} from '../../../../lib/newsletter';
export const prerender = false;
type SendBody = {
subject?: string;
text?: string;
html?: string;
dryRun?: boolean;
};
function unauthorized(): Response {
return new Response('Unauthorized', { status: 401 });
}
function authorize(request: Request, runtimeEnv: ReturnType<typeof getRuntimeEnv>): boolean {
const expected = readEnv('NEWSLETTER_ADMIN_TOKEN', runtimeEnv);
if (!expected) return false;
const header = request.headers.get('authorization') || '';
const bearer = header.toLowerCase().startsWith('bearer ')
? header.slice(7).trim()
: '';
return bearer === expected;
}
export const POST: APIRoute = async ({ request, locals, url }) => {
const runtimeEnv = getRuntimeEnv(locals);
if (!authorize(request, runtimeEnv)) return unauthorized();
const mode = (readEnv('NEWSLETTER_MODE', runtimeEnv) || readEnv('CONTACT_FORM_MODE', runtimeEnv) || 'dev').toLowerCase();
let body: SendBody;
try {
body = (await request.json()) as SendBody;
} catch {
return new Response('Invalid JSON body.', { status: 400 });
}
const subject = (body.subject || '').trim();
const text = (body.text || '').trim();
const html = (body.html || '').trim();
if (!subject || !text) {
return new Response('Fields subject and text are required.', { status: 400 });
}
if (mode !== 'live') {
console.info('[newsletter:dev] Campaign send requested', {
subject,
dryRun: Boolean(body.dryRun),
textPreview: text.slice(0, 120),
});
return Response.json({
mode,
accepted: true,
sent: 0,
failed: 0,
note: 'Dev mode: campaign not sent.',
});
}
const db = getNewsletterDb(runtimeEnv);
if (!db) {
return new Response('Newsletter database binding NEWSLETTER_DB is not configured.', {
status: 500,
});
}
const emailBinding = getEmailBinding(runtimeEnv);
const fromEmail =
readEnv('NEWSLETTER_FROM_EMAIL', runtimeEnv) ||
readEnv('CONTACT_FORM_FROM_EMAIL', runtimeEnv);
if (!emailBinding || !fromEmail) {
return new Response('Missing EMAIL binding or from email for newsletter send.', {
status: 500,
});
}
const subscribersResult = await db
.prepare(
`SELECT * FROM newsletter_subscribers
WHERE status = 'active'
ORDER BY id ASC`,
)
.all<NewsletterSubscriber>();
const subscribers = subscribersResult.results ?? [];
const campaignInsert = await db
.prepare(
`INSERT INTO newsletter_campaigns (subject, body_text, body_html, status, created_by)
VALUES (?, ?, ?, ?, ?)`,
)
.bind(subject, text, html || null, body.dryRun ? 'draft' : 'sending', 'admin-api')
.run();
const campaignId = Number(campaignInsert.meta.last_row_id);
const origin = siteOrigin(url, runtimeEnv);
if (body.dryRun) {
return Response.json({
dryRun: true,
campaignId,
recipients: subscribers.length,
sample: subscribers.slice(0, 5).map((s) => s.email),
});
}
let sent = 0;
let failed = 0;
for (const subscriber of subscribers) {
const unsubUrl = absoluteUrl(
origin,
`/api/newsletter/unsubscribe?token=${subscriber.unsubscribe_token}`,
);
const personalizedText = `${text}\n\n---\nDisiscrizione: ${unsubUrl}\n`;
const personalizedHtml = html
? `${html}<hr /><p style="font-size:12px;color:#666">Disiscrizione: <a href="${unsubUrl}">${unsubUrl}</a></p>`
: undefined;
try {
await sendCloudflareEmail(emailBinding, {
fromEmail,
toEmail: subscriber.email,
subject,
text: personalizedText,
html: personalizedHtml,
});
await db
.prepare(
`INSERT INTO newsletter_campaign_sends
(campaign_id, subscriber_id, email, status, sent_at)
VALUES (?, ?, ?, 'sent', datetime('now'))`,
)
.bind(campaignId, subscriber.id, subscriber.email)
.run();
sent += 1;
} catch (error) {
failed += 1;
const message = error instanceof Error ? error.message : String(error);
await db
.prepare(
`INSERT INTO newsletter_campaign_sends
(campaign_id, subscriber_id, email, status, error, sent_at)
VALUES (?, ?, ?, 'failed', ?, datetime('now'))`,
)
.bind(campaignId, subscriber.id, subscriber.email, message.slice(0, 500))
.run();
}
}
await db
.prepare(
`UPDATE newsletter_campaigns
SET status = ?, sent_at = datetime('now')
WHERE id = ?`,
)
.bind(failed > 0 && sent === 0 ? 'failed' : 'sent', campaignId)
.run();
return Response.json({
campaignId,
recipients: subscribers.length,
sent,
failed,
});
};

View File

@ -0,0 +1,74 @@
import type { APIRoute } from 'astro';
import {
getNewsletterDb,
getRuntimeEnv,
readEnv,
type NewsletterSubscriber,
} from '../../../../lib/newsletter';
export const prerender = false;
function unauthorized(): Response {
return new Response('Unauthorized', { status: 401 });
}
function authorize(request: Request, runtimeEnv: ReturnType<typeof getRuntimeEnv>): boolean {
const expected = readEnv('NEWSLETTER_ADMIN_TOKEN', runtimeEnv);
if (!expected) return false;
const header = request.headers.get('authorization') || '';
const bearer = header.toLowerCase().startsWith('bearer ')
? header.slice(7).trim()
: '';
const queryToken = new URL(request.url).searchParams.get('token') || '';
return bearer === expected || queryToken === expected;
}
export const GET: APIRoute = async ({ request, locals }) => {
const runtimeEnv = getRuntimeEnv(locals);
if (!authorize(request, runtimeEnv)) return unauthorized();
const mode = (readEnv('NEWSLETTER_MODE', runtimeEnv) || readEnv('CONTACT_FORM_MODE', runtimeEnv) || 'dev').toLowerCase();
if (mode !== 'live') {
return Response.json({
mode,
subscribers: [],
note: 'Dev mode: no database query performed.',
});
}
const db = getNewsletterDb(runtimeEnv);
if (!db) {
return new Response('Newsletter database binding NEWSLETTER_DB is not configured.', {
status: 500,
});
}
const status = new URL(request.url).searchParams.get('status');
let rows: NewsletterSubscriber[];
if (status === 'pending' || status === 'active' || status === 'unsubscribed') {
const result = await db
.prepare(
`SELECT id, email, status, locale, source, created_at, confirmed_at, unsubscribed_at
FROM newsletter_subscribers
WHERE status = ?
ORDER BY created_at DESC`,
)
.bind(status)
.all<NewsletterSubscriber>();
rows = result.results ?? [];
} else {
const result = await db
.prepare(
`SELECT id, email, status, locale, source, created_at, confirmed_at, unsubscribed_at
FROM newsletter_subscribers
ORDER BY created_at DESC`,
)
.all<NewsletterSubscriber>();
rows = result.results ?? [];
}
return Response.json({
count: rows.length,
subscribers: rows,
});
};

View File

@ -0,0 +1,86 @@
import type { APIRoute } from 'astro';
import {
getEmailBinding,
getNewsletterDb,
getRuntimeEnv,
readEnv,
required,
sendCloudflareEmail,
type NewsletterSubscriber,
} from '../../../lib/newsletter';
export const prerender = false;
export const GET: APIRoute = async ({ request, locals, url }) => {
const runtimeEnv = getRuntimeEnv(locals);
const mode = (readEnv('NEWSLETTER_MODE', runtimeEnv) || readEnv('CONTACT_FORM_MODE', runtimeEnv) || 'dev').toLowerCase();
const token = required(url.searchParams.get('token'));
const okPath = '/newsletter/confermata';
const failPath = '/newsletter/conferma-errore';
if (!token) {
return Response.redirect(new URL(failPath, url).toString(), 303);
}
if (mode !== 'live') {
console.info('[newsletter:dev] Confirm token received', { token: token.slice(0, 8) + '…' });
return Response.redirect(new URL(okPath, url).toString(), 303);
}
const db = getNewsletterDb(runtimeEnv);
if (!db) {
return new Response('Newsletter database binding NEWSLETTER_DB is not configured.', {
status: 500,
});
}
const row = await db
.prepare(
`SELECT * FROM newsletter_subscribers
WHERE confirm_token = ? AND status = 'pending'
LIMIT 1`,
)
.bind(token)
.first<NewsletterSubscriber>();
if (!row) {
return Response.redirect(new URL(failPath, url).toString(), 303);
}
await db
.prepare(
`UPDATE newsletter_subscribers
SET status = 'active',
confirmed_at = datetime('now'),
confirm_token = NULL
WHERE id = ?`,
)
.bind(row.id)
.run();
const emailBinding = getEmailBinding(runtimeEnv);
const fromEmail =
readEnv('NEWSLETTER_FROM_EMAIL', runtimeEnv) ||
readEnv('CONTACT_FORM_FROM_EMAIL', runtimeEnv);
const notifyTo =
readEnv('NEWSLETTER_NOTIFY_TO_EMAIL', runtimeEnv) ||
readEnv('CONTACT_FORM_TO_EMAIL', runtimeEnv);
if (emailBinding && fromEmail && notifyTo) {
try {
await sendCloudflareEmail(emailBinding, {
fromEmail,
toEmail: notifyTo,
subject: 'Iscrizione newsletter confermata — NexStudio',
text: `Lindirizzo ${row.email} ha confermato liscrizione alla newsletter.`,
});
} catch (error) {
console.warn('[newsletter] confirm notify failed', error);
}
}
return Response.redirect(
new URL(`${okPath}?unsub=${encodeURIComponent(row.unsubscribe_token)}`, request.url).toString(),
303,
);
};

View File

@ -0,0 +1,222 @@
import type { APIRoute } from 'astro';
import {
absoluteUrl,
createToken,
getEmailBinding,
getNewsletterDb,
getRuntimeEnv,
isValidEmail,
normalizeEmail,
readEnv,
required,
sanitizeRedirect,
sendCloudflareEmail,
siteOrigin,
verifyTurnstile,
type NewsletterSubscriber,
} from '../../../lib/newsletter';
export const prerender = false;
function confirmEmailCopy(
locale: string,
confirmUrl: string,
unsubscribeUrl: string,
): { subject: string; text: string } {
if (locale === 'en') {
return {
subject: 'Confirm your NexStudio newsletter subscription',
text: [
'Thanks for subscribing to the NexStudio newsletter.',
'',
'Confirm your email by opening this link:',
confirmUrl,
'',
'If you did not request this, you can ignore this message.',
'',
'Unsubscribe:',
unsubscribeUrl,
].join('\n'),
};
}
if (locale === 'th') {
return {
subject: 'ยืนยันการสมัครรับจดหมายข่าว NexStudio',
text: [
'ขอบคุณที่สมัครรับจดหมายข่าว NexStudio',
'',
'ยืนยันอีเมลของคุณโดยเปิดลิงก์นี้:',
confirmUrl,
'',
'หากคุณไม่ได้ขอสมัคร สามารถเพิกเฉยต่อข้อความนี้ได้',
'',
'ยกเลิกการสมัคร:',
unsubscribeUrl,
].join('\n'),
};
}
return {
subject: 'Conferma iscrizione newsletter NexStudio',
text: [
'Grazie per esserti iscritto alla newsletter di NexStudio.',
'',
'Conferma lindirizzo email aprendo questo link:',
confirmUrl,
'',
'Se non hai richiesto liscrizione, puoi ignorare questo messaggio.',
'',
'Per disiscriverti:',
unsubscribeUrl,
].join('\n'),
};
}
export const POST: APIRoute = async ({ request, locals, url }) => {
const runtimeEnv = getRuntimeEnv(locals);
const mode = (readEnv('NEWSLETTER_MODE', runtimeEnv) || readEnv('CONTACT_FORM_MODE', runtimeEnv) || 'dev').toLowerCase();
const db = getNewsletterDb(runtimeEnv);
const form = await request.formData();
const redirectOk =
sanitizeRedirect(required(form.get('_next')), url) ||
'/newsletter/grazie';
const redirectErr = sanitizeRedirect(required(form.get('_error')), url) || redirectOk;
// Honeypot
if (required(form.get('_gotcha'))) {
return Response.redirect(new URL(redirectOk, url).toString(), 303);
}
const emailRaw = required(form.get('email'));
const locale = required(form.get('locale')) || 'it';
const source = required(form.get('source')) || 'footer';
if (!emailRaw || !isValidEmail(emailRaw)) {
return new Response('Invalid email.', { status: 400 });
}
const email = normalizeEmail(emailRaw);
// Turnstile solo se configurato esplicitamente per la newsletter.
// Non riusare CONTACT_TURNSTILE_SECRET_KEY: il form footer non ha il widget.
const turnstileSecret = readEnv('NEWSLETTER_TURNSTILE_SECRET_KEY', runtimeEnv);
if (turnstileSecret) {
const turnstileToken = required(form.get('cf-turnstile-response'));
const isHuman = await verifyTurnstile(
turnstileToken,
turnstileSecret,
request.headers.get('CF-Connecting-IP'),
);
if (!isHuman) {
return new Response('Turnstile verification failed.', { status: 400 });
}
}
const confirmToken = createToken();
const unsubscribeToken = createToken();
const origin = siteOrigin(url, runtimeEnv);
const confirmUrl = absoluteUrl(origin, `/api/newsletter/confirm?token=${confirmToken}`);
const unsubscribeUrl = absoluteUrl(
origin,
`/api/newsletter/unsubscribe?token=${unsubscribeToken}`,
);
if (mode !== 'live') {
console.info('[newsletter:dev] Subscription request', {
email,
locale,
source,
confirmUrl,
});
return Response.redirect(new URL(redirectOk, url).toString(), 303);
}
if (!db) {
return new Response('Newsletter database binding NEWSLETTER_DB is not configured.', {
status: 500,
});
}
const emailBinding = getEmailBinding(runtimeEnv);
const fromEmail =
readEnv('NEWSLETTER_FROM_EMAIL', runtimeEnv) ||
readEnv('CONTACT_FORM_FROM_EMAIL', runtimeEnv);
const notifyTo =
readEnv('NEWSLETTER_NOTIFY_TO_EMAIL', runtimeEnv) ||
readEnv('CONTACT_FORM_TO_EMAIL', runtimeEnv);
if (!emailBinding || !fromEmail) {
return new Response(
'Live mode requires EMAIL binding (Cloudflare Email Service) and NEWSLETTER_FROM_EMAIL (or CONTACT_FORM_FROM_EMAIL).',
{ status: 500 },
);
}
const existing = await db
.prepare('SELECT * FROM newsletter_subscribers WHERE email = ? LIMIT 1')
.bind(email)
.first<NewsletterSubscriber>();
if (existing?.status === 'active') {
// Already subscribed: redirect without revealing status too loudly.
return Response.redirect(new URL(redirectOk, url).toString(), 303);
}
if (existing?.status === 'unsubscribed' || existing?.status === 'pending') {
await db
.prepare(
`UPDATE newsletter_subscribers
SET status = 'pending',
locale = ?,
source = ?,
confirm_token = ?,
unsubscribe_token = ?,
confirmed_at = NULL,
unsubscribed_at = NULL
WHERE id = ?`,
)
.bind(locale, source, confirmToken, unsubscribeToken, existing.id)
.run();
} else {
await db
.prepare(
`INSERT INTO newsletter_subscribers
(email, status, locale, confirm_token, unsubscribe_token, source)
VALUES (?, 'pending', ?, ?, ?, ?)`,
)
.bind(email, locale, confirmToken, unsubscribeToken, source)
.run();
}
const copy = confirmEmailCopy(locale, confirmUrl, unsubscribeUrl);
try {
await sendCloudflareEmail(emailBinding, {
fromEmail,
toEmail: email,
subject: copy.subject,
text: copy.text,
});
} catch (error) {
console.error('[newsletter] confirm email failed', error);
return Response.redirect(new URL(redirectErr, url).toString(), 303);
}
if (notifyTo) {
try {
await sendCloudflareEmail(emailBinding, {
fromEmail,
toEmail: notifyTo,
subject: required(form.get('_subject')) || 'Nuova richiesta newsletter — NexStudio',
text: [
`Nuova richiesta di iscrizione (pending conferma).`,
`Email: ${email}`,
`Locale: ${locale}`,
`Source: ${source}`,
].join('\n'),
});
} catch (error) {
console.warn('[newsletter] admin notify failed', error);
}
}
return Response.redirect(new URL(redirectOk, url).toString(), 303);
};

View File

@ -0,0 +1,85 @@
import type { APIRoute } from 'astro';
import {
getNewsletterDb,
getRuntimeEnv,
readEnv,
required,
type NewsletterSubscriber,
} from '../../../lib/newsletter';
export const prerender = false;
async function unsubscribeByToken(
locals: unknown,
token: string,
): Promise<'ok' | 'missing' | 'not_found' | 'no_db'> {
if (!token) return 'missing';
const runtimeEnv = getRuntimeEnv(locals);
const mode = (readEnv('NEWSLETTER_MODE', runtimeEnv) || readEnv('CONTACT_FORM_MODE', runtimeEnv) || 'dev').toLowerCase();
if (mode !== 'live') {
console.info('[newsletter:dev] Unsubscribe token received', { token: token.slice(0, 8) + '…' });
return 'ok';
}
const db = getNewsletterDb(runtimeEnv);
if (!db) return 'no_db';
const row = await db
.prepare(
`SELECT * FROM newsletter_subscribers
WHERE unsubscribe_token = ?
LIMIT 1`,
)
.bind(token)
.first<NewsletterSubscriber>();
if (!row) return 'not_found';
if (row.status !== 'unsubscribed') {
await db
.prepare(
`UPDATE newsletter_subscribers
SET status = 'unsubscribed',
unsubscribed_at = datetime('now'),
confirm_token = NULL
WHERE id = ?`,
)
.bind(row.id)
.run();
}
return 'ok';
}
export const GET: APIRoute = async ({ locals, url }) => {
const token = required(url.searchParams.get('token'));
const result = await unsubscribeByToken(locals, token);
if (result === 'no_db') {
return new Response('Newsletter database binding NEWSLETTER_DB is not configured.', {
status: 500,
});
}
const path =
result === 'ok' ? '/newsletter/disiscrizione' : '/newsletter/disiscrizione-errore';
return Response.redirect(new URL(path, url).toString(), 303);
};
export const POST: APIRoute = async ({ request, locals, url }) => {
const form = await request.formData();
const token = required(form.get('token')) || required(url.searchParams.get('token'));
const result = await unsubscribeByToken(locals, token);
if (result === 'no_db') {
return new Response('Newsletter database binding NEWSLETTER_DB is not configured.', {
status: 500,
});
}
const path =
result === 'ok' ? '/newsletter/disiscrizione' : '/newsletter/disiscrizione-errore';
return Response.redirect(new URL(path, url).toString(), 303);
};

View File

@ -1,24 +1,30 @@
--- ---
import SubpageLayout from '../layouts/SubpageLayout.astro'; import SubpageLayout from '../layouts/SubpageLayout.astro';
import { cookieConsentMeta, cookieCategories } from '../data/cookie-consent'; import { cookieConsentMeta } from '../data/cookie-consent';
const link = 'text-sky-400/90 underline-offset-2 hover:text-sky-300 hover:underline'; const link = 'text-sky-400/90 underline-offset-2 hover:text-sky-300 hover:underline';
const tbl = 'mt-4 w-full border-collapse text-left text-sm'; const tbl = 'mt-4 w-full border-collapse text-left text-sm';
const th = 'border border-nx-border bg-nx-surface/60 px-3 py-2 font-semibold text-nx-fg'; const th = 'border border-nx-border bg-nx-surface/60 px-3 py-2 font-semibold text-nx-fg';
const td = 'border border-nx-border px-3 py-2 align-top text-nx-muted'; const td = 'border border-nx-border px-3 py-2 align-top text-nx-muted';
const riferimenti = [
{ label: 'Informativa privacy', href: '/privacy' },
{ label: "Condizioni d'uso", href: '/terms' },
{ label: 'Diritti GDPR', href: '/gdpr' },
] as const;
--- ---
<SubpageLayout <SubpageLayout
title="Cookie Policy — NexStudio" title="Cookie Policy — NexStudio"
description="Cookie Policy NexStudio: categorie, base giuridica, consenso, terze parti, diritti e contatti. Sede Bangkok." description="Cookie Policy NexStudio S.r.l.: categorie, base giuridica, consenso, terze parti, diritti e contatti. Sede legale in Italia."
heading="Cookie Policy" heading="Cookie Policy"
lead="Per NexStudio, con sede a Bangkok, Thailandia. Versione documento 1.0, in vigore dal 21 aprile 2026. Integra l'uso del modulo di consenso first-party descritto sotto; per il trattamento dei dati personali vedi l'informativa privacy." lead="Titolare del trattamento: NexStudio S.r.l., con sede legale in Italia, [INDIRIZZO_COMPLETO]."
> >
<div class="[&_h3]:mt-6 [&_h3]:text-base [&_h3]:font-semibold [&_h3]:text-nx-fg"> <div class="[&_h3]:mt-6 [&_h3]:text-base [&_h3]:font-semibold [&_h3]:text-nx-fg">
<p class="border-l-2 border-sky-500/35 pl-4 text-sm leading-relaxed text-nx-muted"> <p class="border-l-2 border-sky-500/35 pl-4 text-sm leading-relaxed text-nx-muted">
<span class="font-medium text-nx-fg">Versione</span> 1.0 <span class="font-medium text-nx-fg">Versione</span> 1.1
<span class="text-nx-muted/80"> · </span> <span class="text-nx-muted/80"> · </span>
<span class="font-medium text-nx-fg">In vigore dal</span> 21 aprile 2026 <span class="font-medium text-nx-fg">In vigore dal</span> [DATA]
<span class="text-nx-muted/80"> · </span> <span class="text-nx-muted/80"> · </span>
<span class="font-medium text-nx-fg">Versione tecnica consenso</span>{' '} <span class="font-medium text-nx-fg">Versione tecnica consenso</span>{' '}
<code class="rounded bg-nx-elevated px-1.5 py-0.5 font-mono text-xs text-sky-300/90" <code class="rounded bg-nx-elevated px-1.5 py-0.5 font-mono text-xs text-sky-300/90"
@ -26,13 +32,25 @@ const td = 'border border-nx-border px-3 py-2 align-top text-nx-muted';
> >
</p> </p>
<p class="!mt-5 text-sm text-nx-muted sm:text-base">
Documenti connessi:{' '}
{
riferimenti.map((r, i) => (
<span>
{i > 0 && <span class="text-nx-muted/80"> · </span>}
<a class={link} href={r.href}>{r.label}</a>
</span>
))
}
</p>
<h2 id="indice" class="!mt-8">Indice</h2> <h2 id="indice" class="!mt-8">Indice</h2>
<ol class="!mt-3 list-decimal space-y-1.5 pl-5 text-sm leading-relaxed sm:text-base"> <ol class="!mt-3 list-decimal space-y-1.5 pl-5 text-sm leading-relaxed sm:text-base">
<li><a class={link} href="#premessa">Premessa</a></li> <li><a class={link} href="#premessa">Premessa</a></li>
<li><a class={link} href="#definizioni">Definizioni</a></li> <li><a class={link} href="#definizioni">Definizioni</a></li>
<li><a class={link} href="#finalita">Finalità dei cookie e categorie</a></li> <li><a class={link} href="#finalita">Finalità dei cookie e categorie</a></li>
<li><a class={link} href="#elenco">Elenco dei cookie (modello da compilare)</a></li> <li><a class={link} href="#elenco">Elenco dei cookie utilizzati</a></li>
<li><a class={link} href="#sanitario-legale">Base giuridica e ambito sanitario/legale</a></li> <li><a class={link} href="#base-giuridica">Base giuridica</a></li>
<li><a class={link} href="#consenso">Modalità di raccolta del consenso e registro</a></li> <li><a class={link} href="#consenso">Modalità di raccolta del consenso e registro</a></li>
<li><a class={link} href="#revoca">Gestione e revoca del consenso</a></li> <li><a class={link} href="#revoca">Gestione e revoca del consenso</a></li>
<li><a class={link} href="#browser">Disabilitazione via browser e impatto</a></li> <li><a class={link} href="#browser">Disabilitazione via browser e impatto</a></li>
@ -45,23 +63,21 @@ const td = 'border border-nx-border px-3 py-2 align-top text-nx-muted';
<li><a class={link} href="#effetti">Effetti di rifiuto o accettazione</a></li> <li><a class={link} href="#effetti">Effetti di rifiuto o accettazione</a></li>
<li><a class={link} href="#modifiche">Modifiche alla Cookie Policy</a></li> <li><a class={link} href="#modifiche">Modifiche alla Cookie Policy</a></li>
<li><a class={link} href="#diritti">Diritti degli interessati e contatti</a></li> <li><a class={link} href="#diritti">Diritti degli interessati e contatti</a></li>
<li><a class={link} href="#appendice-tabella">Registro dei cookie (appendice)</a></li>
<li><a class={link} href="#implementazione">Implementazione tecnica e strumenti</a></li>
<li><a class={link} href="#test-qc">Test e controllo qualità</a></li>
<li><a class={link} href="#banner-testi">Esempio di testi per il banner (IT / EN)</a></li>
<li><a class={link} href="#note-legali">Note legali</a></li>
<li><a class={link} href="#versioning">Versioning e storico revisioni</a></li>
</ol> </ol>
<h2 id="premessa">1. Premessa</h2> <h2 id="premessa">1. Premessa</h2>
<p> <p>
Questa Cookie Policy descrive le tipologie di cookie e tecnologie simili utilizzate sul sito Questa Cookie Policy descrive le tipologie di cookie e tecnologie simili utilizzate sul sito
web di <strong>NexStudio</strong> (di seguito «Società», «noi»), con sede a Bangkok, web di NexStudio S.r.l. (di seguito «NexStudio», «noi»), con sede legale in Italia,
Thailandia, e spiega come gestire le preferenze. Per il trattamento dei dati personali e per i [INDIRIZZO_COMPLETO], e spiega come gestire le preferenze. Per il trattamento dei dati
diritti degli interessati si rimanda all<a class={link} href="/privacy" personali e per i diritti degli interessati si rimanda all<a class={link} href="/privacy"
>Informativa privacy</a >Informativa privacy</a
>. >.
</p> </p>
<p>
Per le cookie policy dei prodotti SaaS LexAura e MediAura, consultare le rispettive pagine sui
portali dedicati.
</p>
<h2 id="definizioni">2. Definizioni</h2> <h2 id="definizioni">2. Definizioni</h2>
<ul> <ul>
@ -78,89 +94,46 @@ const td = 'border border-nx-border px-3 py-2 align-top text-nx-muted';
</li> </li>
<li> <li>
<strong>Tecnologie simili:</strong> web beacon, pixel, <em>local storage</em>, e in generale <strong>Tecnologie simili:</strong> web beacon, pixel, <em>local storage</em>, e in generale
identificatori lato client. Tecnologie invasive (es. fingerprinting) richiedono base legale identificatori lato client.
adeguata e massima trasparenza.
</li> </li>
</ul> </ul>
<h2 id="finalita">3. Finalità dei cookie e categorie</h2> <h2 id="finalita">3. Finalità dei cookie e categorie</h2>
<p>I cookie sul sito possono essere raggruppati come segue.</p>
<h3>3.1 Cookie strettamente necessari (essenziali)</h3> <h3>3.1 Cookie strettamente necessari (essenziali)</h3>
<ul> <p>
<li> <strong>Finalità:</strong> navigazione, sicurezza, gestione sessione, memorizzazione delle
<strong>Finalità:</strong> navigazione, sicurezza, gestione sessione ove prevista, preferenze di consenso. <strong>Base giuridica:</strong> legittimo interesse del titolare
memorizzazione delle preferenze di consenso. (Art. 6.1.f GDPR) a garantire il funzionamento del servizio richiesto dallutente.
</li> </p>
<li>
<strong>Base giuridica:</strong> esecuzione di misure precontrattuali / necessità tecnica e,
ove applicabile, interesse legittimo limitato al funzionamento del servizio richiesto
dallutente.
</li>
<li>
<strong>Esempi tipici:</strong> token di sessione, cookie anti-CSRF, chiave di consenso in
<em>local storage</em> (se usata al posto di cookie HTTP).
</li>
</ul>
<h3>3.2 Cookie di preferenza / funzionali</h3> <h3>3.2 Cookie di preferenza / funzionali</h3>
<ul> <p>
<li> <strong>Finalità:</strong> lingua, regione, tema o altre impostazioni di interfaccia.{' '}
<strong>Finalità:</strong> lingua, regione, tema o altre impostazioni di interfaccia non <strong>Base giuridica:</strong> consenso (Art. 6.1.a GDPR).
strettamente indispensabili. </p>
</li>
<li><strong>Base giuridica:</strong> consenso, salvo trattamenti minimi e non invasivi.</li>
<li><strong>Esempi:</strong> <code class="font-mono text-xs text-sky-300/90">lang</code>, preferenze UI.</li>
</ul>
<h3>3.3 Cookie di performance / analytics</h3> <h3>3.3 Cookie di performance / analytics</h3>
<ul> <p>
<li> <strong>Finalità:</strong> statistiche aggregate su visite e percorsi per migliorare il sito.{' '}
<strong>Finalità:</strong> statistiche aggregate su visite e percorsi per migliorare il <strong>Base giuridica:</strong> consenso (Art. 6.1.a GDPR).
sito. </p>
</li>
<li>
<strong>Base giuridica:</strong> consenso, salvo misurazione strettamente anonimizzata e
non identificativa secondo orientamenti applicabili.
</li>
<li>
<strong>Esempi:</strong> <code class="font-mono text-xs text-sky-300/90">_ga</code>, <code
class="font-mono text-xs text-sky-300/90">_gid</code
> (Google Analytics), strumenti tipo Hotjar, se integrati.
</li>
</ul>
<h3>3.4 Cookie per advertising e targeting</h3> <h3>3.4 Cookie per advertising e targeting</h3>
<ul> <p>
<li> <strong>Finalità:</strong> pubblicità mirata, remarketing, misura delle campagne.{' '}
<strong>Finalità:</strong> pubblicità mirata, remarketing, misura delle campagne. <strong>Base giuridica:</strong> consenso esplicito.
</li> </p>
<li><strong>Base giuridica:</strong> consenso esplicito.</li>
<li>
<strong>Esempi:</strong> identificatori di reti pubblicitarie, pixel di retargeting, cookie
DSP.
</li>
</ul>
<h3>3.5 Cookie di terze parti e integrazioni social</h3> <h3>3.5 Cookie di terze parti e integrazioni social</h3>
<ul>
<li>
<strong>Finalità:</strong> video incorporati, pulsanti social, mappe, chat di terze parti.
</li>
<li>
<strong>Nota:</strong> il trattamento da parte di terzi è regolato dalle rispettive policy;
NexStudio non controlla cookie impostati senza adeguata configurazione e consenso.
</li>
</ul>
<h2 id="elenco">4. Elenco dei cookie utilizzati (modello da compilare)</h2>
<p> <p>
Per ogni cookie reale indicare: nome, fornitore, categoria, finalità, durata, prima o terza <strong>Finalità:</strong> video incorporati, pulsanti social, mappe, chat di terze parti. Il
parte, base giuridica, link alla privacy del fornitore e, se utile, opt-out. trattamento da parte di terzi è regolato dalle rispettive policy.
</p> </p>
<h2 id="elenco">4. Elenco dei cookie utilizzati</h2>
<p> <p>
La tabella sotto è <strong>esemplificativa</strong>: va sostituita con linventario aggiornato Elenco in appendice tecnica (da mantenere aggiornato con scansione periodica).
(ideale: hash o versione e data di aggiornamento in calce alla tabella pubblicata).
</p> </p>
<table class={tbl}> <table class={tbl}>
<thead> <thead>
@ -175,54 +148,26 @@ const td = 'border border-nx-border px-3 py-2 align-top text-nx-muted';
</thead> </thead>
<tbody> <tbody>
<tr> <tr>
<td class={td}><code class="font-mono text-xs">_ga</code> (es.)</td> <td class={td} colspan="6">
<td class={td}>Google LLC</td> <em>Da compilare dopo scansione del sito in staging e produzione.</em>
<td class={td}>Analytics</td>
<td class={td}>24 mesi (tipico)</td>
<td class={td}>3rd</td>
<td class={td}>Consenso</td>
</tr>
<tr>
<td class={td}>
<code class="font-mono text-xs">{cookieConsentMeta.storageKey}</code> (non cookie HTTP)
</td> </td>
<td class={td}>NexStudio</td>
<td class={td}>Necessario</td>
<td class={td}>Fino a revoca / aggiornamento policy</td>
<td class={td}>1st (local)</td>
<td class={td}>Necessario / consenso informato</td>
</tr>
<tr>
<td class={td}><code class="font-mono text-xs">_fbp</code> (es.)</td>
<td class={td}>Meta</td>
<td class={td}>Advertising</td>
<td class={td}>90 giorni (tipico)</td>
<td class={td}>3rd</td>
<td class={td}>Consenso</td>
</tr> </tr>
</tbody> </tbody>
</table> </table>
<h2 id="sanitario-legale">5. Base giuridica e trattamento in ambito sanitario o legale</h2> <h2 id="base-giuridica">5. Base giuridica</h2>
<ul> <ul>
<li> <li>
Per cookie che comportano trattamento di dati particolari (salute, dati giudiziari o Cookie tecnici essenziali: legittimo interesse del titolare (Art. 6.1.f GDPR).
assimilabili) è richiesto <strong>consenso esplicito</strong> informato e misure tecniche e
organizzative aggiuntive, salvo diversa base prevista dalla legge applicabile.
</li> </li>
<li> <li>
Nei servizi SaaS medico-legali la raccolta tramite cookie sul sito vetrina è da Cookie analitici e di marketing: consenso libero, informato e revocabile (Art. 6.1.a GDPR).
<strong>limitare e segregare</strong>; per test e analytics privilegiare dati sintetici o
anonimizzazione efficace.
</li> </li>
</ul> </ul>
<h2 id="consenso">6. Modalità di raccolta del consenso e registro</h2> <h2 id="consenso">6. Modalità di raccolta del consenso e registro</h2>
<p> <p>
Il sito utilizza una <strong>CMP first-party</strong> sviluppata per NexStudio, orientata ai Il sito utilizza una CMP first-party. Al primo accesso viene mostrato un banner con:
requisiti di trasparenza e granularità (inclusi riferimenti GDPR e PDPA tailandese come quadro
di riferimento per le descrizioni). Al primo accesso, o dopo aumento della versione della
policy tecnica, viene mostrato un banner con:
</p> </p>
<ul> <ul>
<li><strong>Rifiuta non essenziali</strong></li> <li><strong>Rifiuta non essenziali</strong></li>
@ -230,232 +175,78 @@ const td = 'border border-nx-border px-3 py-2 align-top text-nx-muted';
<li><strong>Accetta tutto</strong></li> <li><strong>Accetta tutto</strong></li>
</ul> </ul>
<p> <p>
La prova della scelta è memorizzata nel browser in <em>local storage</em> alla chiave{' '} La prova della scelta è memorizzata nel browser in <em>local storage</em>.
<code class="rounded bg-nx-elevated px-1.5 py-0.5 font-mono text-xs text-sky-300/90"
>{cookieConsentMeta.storageKey}</code
>, con versione <code class="rounded bg-nx-elevated px-1.5 py-0.5 font-mono text-xs text-sky-300/90"
>v{cookieConsentMeta.policyVersion}</code
>, timestamp di aggiornamento e stato per categoria. I cookie strettamente necessari possono
restare attivi compatibilmente con informativa chiara. Per audit avanzati si può integrare in
futuro un log lato server (IP, user-agent, ID sessione) con informativa dedicata.
</p>
<p class="font-medium text-nx-fg">Categorie gestite dal modulo attuale</p>
<ul>
{
cookieCategories.map((c) => (
<li>
<strong class="text-nx-fg">{c.label}</strong>
{c.required ? ' — sempre attiva.' : ' — attiva solo se accettata.'}{' '}
{c.description}
</li>
))
}
</ul>
<p>
Il codice espone <code class="rounded bg-nx-elevated px-1.5 py-0.5 font-mono text-xs text-sky-300/90"
>window.NexStudioCookieConsent</code
> e levento <code class="rounded bg-nx-elevated px-1.5 py-0.5 font-mono text-xs text-sky-300/90"
>nexstudio:cookie-consent</code
> per caricare script non necessari solo dopo consenso.
</p> </p>
<h2 id="revoca">7. Gestione e revoca del consenso</h2> <h2 id="revoca">7. Gestione e revoca del consenso</h2>
<ul> <ul>
<li>Tramite il banner o il pannello <strong>Personalizza</strong> al primo accesso.</li> <li>Tramite il banner o il pannello <strong>Personalizza</strong> al primo accesso.</li>
<li> <li>
In qualsiasi momento tramite <strong>Impostazioni cookie</strong> nel footer del sito (sotto In qualsiasi momento tramite <strong>Impostazioni cookie</strong> nel footer del sito.
le colonne di navigazione).
</li>
<li>
Cancellando i dati locali del sito dalle impostazioni del browser (inclusa la voce di{' '}
<em>local storage</em> citata sopra): al successivo caricamento il banner si ripresenta.
</li> </li>
<li>Cancellando i dati locali del sito dalle impostazioni del browser.</li>
</ul> </ul>
<p>
Dopo la revoca non devono essere impostati cookie non essenziali; alcune funzioni (es.
preferenze memorizzate, statistiche) possono risultare limitate.
</p>
<h2 id="browser">8. Disabilitazione via browser e impatto</h2> <h2 id="browser">8. Disabilitazione via browser e impatto</h2>
<p> <p>
È possibile bloccare o cancellare i cookie dalle impostazioni del browser (Chrome, Firefox, È possibile bloccare o cancellare i cookie dalle impostazioni del browser (Chrome, Firefox,
Safari, Edge e altri). Disabilitando i cookie possono smettere di funzionare login, preferenze Safari, Edge e altri).
o strumenti che ne dipendono.
</p> </p>
<h2 id="terze-parti">9. Cookie di terze parti e responsabilità</h2> <h2 id="terze-parti">9. Cookie di terze parti e responsabilità</h2>
<p> <p>
Il sito può integrare strumenti di terze parti (analytics, CDN, social, gateway di pagamento, Il sito può integrare strumenti di terze parti (analytics, CDN, social). Le terze parti
video). Le terze parti possono impostare cookie propri e trattare dati secondo le loro policy. possono impostare cookie propri e trattare dati secondo le loro policy.
</p>
<p>
<strong>Esempi di fornitori</strong> spesso usati in contesti analoghi (elenco da adattare al
sito reale): Google Analytics, Google Tag Manager, Cloudflare, Stripe, Vimeo, YouTube, Meta,
LinkedIn, Hotjar. Per ciascuno vanno indicati link a privacy/cookie policy e strumenti di
opt-out ove disponibili.
</p> </p>
<h2 id="retention">10. Conservazione e criteri di retention</h2> <h2 id="retention">10. Conservazione e criteri di retention</h2>
<ul> <p>
<li> Le durate dei singoli cookie seguono le tabelle pubblicate e le policy dei fornitori. Cookie
Le durate dei singoli cookie seguono le tabelle pubblicate e le policy dei fornitori (es. di consenso: 365 giorni, rinnovabili. Log di sicurezza: massimo 30 giorni.
finestra indicativa <strong>24 mesi</strong> per dati aggregati di analytics, salvo obblighi </p>
di legge o contratto diversi — da confermare con il legale).
</li>
<li>
Prove di consenso e gestione DSR: periodi di conservazione da definire nella retention
policy (orientamento comune per documentazione di conformità: <strong
>37 anni</strong
>, da calibrare su base legale e rischio — non vincolante in questa pagina).
</li>
</ul>
<h2 id="trasferimenti">11. Trasferimenti internazionali</h2> <h2 id="trasferimenti">11. Trasferimenti internazionali</h2>
<p> <p>
Alcuni fornitori possono trattare dati in Paesi terzi (es. Stati Uniti). Ove applicabile si Alcuni fornitori possono trattare dati in Paesi terzi (es. Stati Uniti). Ove applicabile si
adottano garanzie come clausole contrattuali standard, decisioni di adeguatezza o altre basi adottano garanzie come clausole contrattuali standard, decisioni di adeguatezza o altre basi
previste dalla legge. Per dettagli su trasferimenti specifici contattare il referente privacy previste dalla legge.
indicato sotto.
</p> </p>
<h2 id="minori">12. Minori età</h2> <h2 id="minori">12. Minori età</h2>
<p> <p>Il sito non è rivolto alla raccolta intenzionale di dati di minori.</p>
Il sito non è rivolto alla raccolta intenzionale di dati di minori. Per utenti sotto la soglia
prevista dalla legge locale, i trattamenti non essenziali richiedono consenso del titore
della responsabilità genitoriale ove imposto dalla normativa applicabile.
</p>
<h2 id="sicurezza">13. Misure di sicurezza</h2> <h2 id="sicurezza">13. Misure di sicurezza</h2>
<p> <p>
Si applicano misure tecniche e organizzative appropriate: trasmissione su TLS, accesso Si applicano misure tecniche e organizzative appropriate: trasmissione su TLS, accesso
limitato ai log, principio di minimizzazione, monitoraggio anomalie ove in uso. limitato ai log, principio di minimizzazione.
</p> </p>
<h2 id="advertising">14. Consenso per scopi pubblicitari e profiling</h2> <h2 id="advertising">14. Consenso per scopi pubblicitari e profiling</h2>
<p> <p>
Profilazione e pubblicità comportano <strong>consenso esplicito</strong> e tracciabilità Profilazione e pubblicità comportano consenso esplicito e tracciabilità della scelta.
della scelta. Ove si usino reti pubblicitarie, fornire link agli strumenti di opt-out del
fornitore (es. impostazioni annunci Google).
</p> </p>
<h2 id="effetti">15. Effetti di rifiuto o accettazione</h2> <h2 id="effetti">15. Effetti di rifiuto o accettazione</h2>
<ul> <p>
<li> Accettando le categorie opzionali si abilitano preferenze avanzate, analytics e funzioni di
<strong>Accettando</strong> le categorie opzionali si abilitano preferenze avanzate, marketing. Rifiutando i non essenziali il sito resta navigabile.
analytics e, se configurate, funzioni di marketing. </p>
</li>
<li>
<strong>Rifiutando</strong> i non essenziali il sito resta navigabile con funzioni core; le
altre possono essere limitate.
</li>
</ul>
<h2 id="modifiche">16. Modifiche alla Cookie Policy</h2> <h2 id="modifiche">16. Modifiche alla Cookie Policy</h2>
<p> <p>
La Policy può essere aggiornata quando cambiano cookie, fornitori o norme. Sarà indicata la La Policy può essere aggiornata quando cambiano cookie, fornitori o norme. Sarà indicata la
data di revisione; ove richiesto dalla legge o dallimpatto del cambiamento, si richiederà un data di revisione.
nuovo consenso (incrementando la versione tecnica nel modulo).
</p> </p>
<h2 id="diritti">17. Diritti degli interessati e contatti</h2> <h2 id="diritti">17. Diritti degli interessati e contatti</h2>
<p> <p>
Diritti: accesso, rettifica, cancellazione, limitazione, portabilità, opposizione e revoca del Diritti: accesso, rettifica, cancellazione, limitazione, portabilità, opposizione e revoca del
consenso ove previsti. Per esercitarli: <a class={link} href="mailto:privacy@nexstudio.com" consenso. Per esercitarli:{' '}
>privacy@nexstudio.com</a <a class={link} href="mailto:privacy@nexstudio.com">privacy@nexstudio.com</a>
> (DPO o referente privacy, da formalizzare). {' '}e tramite il form su nexstudio.ai. Autorità di controllo: Garante per la protezione dei
dati personali (<a class={link} href="https://www.garanteprivacy.it" target="_blank" rel="noopener noreferrer"
>www.garanteprivacy.it</a
>).
</p> </p>
<h2 id="appendice-tabella">18. Registro dei cookie e informazioni tecniche (appendice)</h2>
<p>
Pubblicare qui la tabella completa con colonne: nome, provider, categoria, descrizione
tecnica, durata, 1st/3rd, link opt-out, link privacy, note (GTM, embed, CDN, ecc.).
</p>
<table class={tbl}>
<thead>
<tr>
<th class={th}>Nome</th>
<th class={th}>Provider</th>
<th class={th}>Categoria</th>
<th class={th}>Scopo / durata</th>
<th class={th}>Note</th>
</tr>
</thead>
<tbody>
<tr>
<td class={td} colspan="5">
<em>Da compilare dopo scansione del sito in staging e produzione.</em>
</td>
</tr>
</tbody>
</table>
<h2 id="implementazione">19. Implementazione tecnica e strumenti</h2>
<ul>
<li>
<strong>CMP:</strong> modulo first-party NexStudio (nessun OneTrust/Cookiebot obbligatorio
in questa build); estensibile verso CMP commerciali se richiesto da clienti o policy
aziendale.
</li>
<li>
<strong>Log consenso:</strong> attualmente payload JSON in <em>local storage</em> con
versione policy, timestamp ISO e booleani per categoria; valutare integrazione server-side
per audit e correlazione sessione.
</li>
<li>
<strong>Banner:</strong> testo breve, link a questa Cookie Policy, tre azioni come da §6.
</li>
<li>
<strong>Sviluppo:</strong> caricare script di analytics, marketing o embed solo dopo lettura
del consenso tramite API/evento sopra citati.
</li>
</ul>
<h2 id="test-qc">20. Test e controllo qualità</h2>
<ul>
<li>Scansione periodica del sito per elenco cookie (strumenti automatici + revisione manuale).</li>
<li>Allineamento tra tabella pubblicata, CMP e script effettivamente caricati.</li>
<li>Audit trimestrale delle terze parti e aggiornamento categorie/testi.</li>
</ul>
<h2 id="banner-testi">21. Esempio di testi per il banner (IT / EN)</h2>
<ul>
<li>
<strong>Italiano (allineato al sito):</strong> «Utilizziamo cookie e memorizzazione locale.
Seleziona le categorie che preferisci. I necessari restano attivi per registrare la tua
scelta. Dettagli nellinformativa cookie.» — pulsanti: <strong>Rifiuta non essenziali</strong
>, <strong>Personalizza</strong>, <strong>Accetta tutto</strong>.
</li>
<li>
<strong>English (per versione bilingue futura):</strong> «We use cookies and local storage.
Choose the categories you prefer. Necessary ones stay on to record your choice. See our
cookie policy.» — <strong>Reject non-essential</strong>, <strong>Customize</strong>, <strong
>Accept all</strong
>.
</li>
</ul>
<h2 id="note-legali">22. Note legali</h2>
<p>
Questa Cookie Policy <strong>integra ma non sostituisce</strong> lInformativa privacy. Per
aspetti di trattamento dei dati personali collegati ai cookie fare riferimento anche a{' '}
<a class={link} href="/gdpr">GDPR</a> / documenti privacy pubblicati e al counsel locale per
Thailandia, UE o altri mercati serviti.
</p>
<h2 id="versioning">23. Versioning e storico revisioni</h2>
<ul>
<li><strong>Versione 1.0</strong> — 21 aprile 2026 — Prima pubblicazione strutturata.</li>
</ul>
<h2>Appendice: checklist tecnica per il rilascio</h2>
<ul class="list-none space-y-2 pl-0 [&_li]:flex [&_li]:gap-2">
<li><span class="text-nx-muted">☐</span> Scansione cookie completa e tabella aggiornata</li>
<li><span class="text-nx-muted">☐</span> CMP e prova consenso / revoca funzionanti</li>
<li><span class="text-nx-muted">☐</span> Traduzioni banner (es. EN / TH) se richieste dal mercato</li>
<li><span class="text-nx-muted">☐</span> Script non essenziali bloccati fino a consenso</li>
<li><span class="text-nx-muted">☐</span> Tabella cookie con metadati e versioning pubblico</li>
<li><span class="text-nx-muted">☐</span> Procedure DSR e revoca collegate dallinformativa privacy</li>
<li><span class="text-nx-muted">☐</span> Link a privacy e contatti DPO/referente nel footer</li>
</ul>
</div> </div>
</SubpageLayout> </SubpageLayout>

View File

@ -0,0 +1,4 @@
---
import Page from '../../newsletter/conferma-errore.astro';
---
<Page />

View File

@ -0,0 +1,4 @@
---
import Page from '../../newsletter/confermata.astro';
---
<Page />

View File

@ -0,0 +1,4 @@
---
import Page from '../../newsletter/disiscrizione-errore.astro';
---
<Page />

View File

@ -0,0 +1,4 @@
---
import Page from '../../newsletter/disiscrizione.astro';
---
<Page />

View File

@ -9,13 +9,13 @@ import {
const link = 'text-sky-400/90 underline-offset-2 hover:text-sky-300 hover:underline'; const link = 'text-sky-400/90 underline-offset-2 hover:text-sky-300 hover:underline';
const lead = const lead =
"Diritti degli interessati e modalita' di esercizio ai sensi del Regolamento (UE) 2016/679, in coerenza con l'informativa privacy quadro del portale NexStudio. " + "Diritti degli interessati e modalità di esercizio ai sensi del Regolamento (UE) 2016/679, in coerenza con l'informativa privacy quadro di NexStudio S.r.l. (Italia, [INDIRIZZO_COMPLETO]). " +
gdprCrossReferences.lead; gdprCrossReferences.lead;
--- ---
<SubpageLayout <SubpageLayout
title="GDPR — NexStudio" title="GDPR — NexStudio"
description="Pagina GDPR NexStudio: diritti degli interessati, modalita' operative, ruoli tra portale corporate e SaaS LexAura/MediAura, coordinamento con privacy dedicate." description="Pagina GDPR NexStudio S.r.l.: diritti degli interessati, modalità operative, ruoli tra portale corporate e SaaS LexAura/MediAura, coordinamento con privacy dedicate."
heading="GDPR e diritti degli interessati" heading="GDPR e diritti degli interessati"
lead={lead} lead={lead}
> >
@ -67,6 +67,9 @@ const lead =
))} ))}
</ul> </ul>
) : null} ) : null}
{s.paragraphsAfterBullets?.map((p) => (
<p class="mt-3 text-sm leading-relaxed text-nx-muted sm:text-base">{p}</p>
))}
</section> </section>
)) ))
} }

View File

@ -22,7 +22,7 @@ import SubpageLayout from '../layouts/SubpageLayout.astro';
<h2>Canali di distribuzione</h2> <h2>Canali di distribuzione</h2>
<ul> <ul>
<li>Questa pagina resta il punto di riferimento ufficiale per annunci e changelog sintetici.</li> <li>Questa pagina resta il punto di riferimento ufficiale per annunci e changelog sintetici.</li>
<li>La newsletter verra attivata appena disponibile il canale comunicazioni dedicato.</li> <li>La newsletter è attiva dal footer del sito (iscrizione con conferma email).</li>
<li>I contenuti long-form potranno essere pubblicati anche su canali esterni, con rimando qui.</li> <li>I contenuti long-form potranno essere pubblicati anche su canali esterni, con rimando qui.</li>
</ul> </ul>

View File

@ -0,0 +1,18 @@
---
import SubpageLayout from '../../layouts/SubpageLayout.astro';
---
<SubpageLayout
title="Conferma non riuscita — Newsletter NexStudio"
description="Il link di conferma newsletter non è valido o è già stato usato."
heading="Conferma non riuscita"
lead="Il link di conferma non è valido, è scaduto o è già stato utilizzato."
>
<p>
Puoi riprovare iscrivendoti di nuovo dal form newsletter nel footer del
sito: riceverai un nuovo messaggio di conferma.
</p>
<p>
<a class="text-sky-400/90 hover:text-sky-300" href="/">Torna alla home</a>
</p>
</SubpageLayout>

View File

@ -0,0 +1,34 @@
---
import SubpageLayout from '../../layouts/SubpageLayout.astro';
const unsubToken = Astro.url.searchParams.get('unsub')?.trim() ?? '';
const unsubHref = unsubToken
? `/api/newsletter/unsubscribe?token=${encodeURIComponent(unsubToken)}`
: '';
---
<SubpageLayout
title="Iscrizione confermata — Newsletter NexStudio"
description="La tua iscrizione alla newsletter NexStudio è confermata."
heading="Iscrizione confermata"
lead="Il tuo indirizzo email è ora attivo per ricevere aggiornamenti su prodotti e roadmap NexStudio."
>
<p>
Puoi disiscriverti in qualsiasi momento dal link presente in fondo a ogni
messaggio della newsletter.
</p>
{
unsubHref ? (
<p>
Oppure disiscriviti subito:{' '}
<a class="text-sky-400/90 hover:text-sky-300" href={unsubHref}>
Annulla iscrizione
</a>
.
</p>
) : null
}
<p>
<a class="text-sky-400/90 hover:text-sky-300" href="/">Torna alla home</a>
</p>
</SubpageLayout>

View File

@ -0,0 +1,20 @@
---
import SubpageLayout from '../../layouts/SubpageLayout.astro';
---
<SubpageLayout
title="Disiscrizione non riuscita — Newsletter NexStudio"
description="Il link di disiscrizione newsletter non è valido."
heading="Disiscrizione non riuscita"
lead="Il link di disiscrizione non è valido o è incompleto."
>
<p>
Se continui a ricevere messaggi indesiderati, scrivi a
<a class="text-sky-400/90 hover:text-sky-300" href="mailto:privacy@nexstudio.com"
>privacy@nexstudio.com</a
>.
</p>
<p>
<a class="text-sky-400/90 hover:text-sky-300" href="/">Torna alla home</a>
</p>
</SubpageLayout>

View File

@ -0,0 +1,18 @@
---
import SubpageLayout from '../../layouts/SubpageLayout.astro';
---
<SubpageLayout
title="Disiscrizione — Newsletter NexStudio"
description="Disiscrizione dalla newsletter NexStudio completata."
heading="Disiscrizione completata"
lead="Non riceverai più la newsletter di NexStudio su questo indirizzo."
>
<p>
Se ti sei disiscritto per errore, puoi iscriverti di nuovo dal footer del
sito e confermare lemail.
</p>
<p>
<a class="text-sky-400/90 hover:text-sky-300" href="/">Torna alla home</a>
</p>
</SubpageLayout>

View File

@ -4,13 +4,14 @@ import SubpageLayout from '../../layouts/SubpageLayout.astro';
<SubpageLayout <SubpageLayout
title="Grazie — Newsletter NexStudio" title="Grazie — Newsletter NexStudio"
description="Iscrizione alla newsletter ricevuta." description="Richiesta di iscrizione alla newsletter ricevuta."
heading="Grazie" heading="Controlla la tua email"
lead="La richiesta di iscrizione è stata inviata. Controlla la posta se è prevista una conferma (double opt-in)." lead="Abbiamo ricevuto la richiesta di iscrizione. Per completarla, apri il messaggio di conferma e clicca sul link (double opt-in)."
> >
<p> <p>
Se usi un servizio con conferma via email, apri il messaggio e segui il Se non trovi lemail, controlla anche la cartella spam o promozioni. Il link
link per completare liscrizione. di conferma scade solo se viene generata una nuova richiesta per lo stesso
indirizzo.
</p> </p>
<p> <p>
<a class="text-sky-400/90 hover:text-sky-300" href="/">Torna alla home</a> <a class="text-sky-400/90 hover:text-sky-300" href="/">Torna alla home</a>

View File

@ -9,13 +9,13 @@ import {
const link = 'text-sky-400/90 underline-offset-2 hover:text-sky-300 hover:underline'; const link = 'text-sky-400/90 underline-offset-2 hover:text-sky-300 hover:underline';
const lead = const lead =
"Informativa quadro sul trattamento dei dati personali per il portale istituzionale NexStudio (sede operativa: Bangkok, Thailandia) e per le interazioni corporate con i servizi SaaS LexAura e MediAura. " + "Informativa quadro sul trattamento dei dati personali per il portale istituzionale NexStudio S.r.l., con sede legale in Italia, [INDIRIZZO_COMPLETO], e per i servizi SaaS LexAura e MediAura. " +
privacyCrossReferences.lead; privacyCrossReferences.lead;
--- ---
<SubpageLayout <SubpageLayout
title="Privacy — NexStudio" title="Privacy — NexStudio"
description="Informativa privacy quadro NexStudio: sito istituzionale, contatti corporate, rimandi ai dettagli privacy dei SaaS LexAura (Legal Tech) e MediAura (Health Tech)." description="Informativa privacy quadro NexStudio S.r.l.: sito istituzionale, contatti corporate, rimandi ai dettagli privacy dei SaaS LexAura (Legal Tech) e MediAura (Health Tech)."
heading="Informativa sulla privacy" heading="Informativa sulla privacy"
lead={lead} lead={lead}
> >
@ -70,6 +70,9 @@ const lead =
))} ))}
</ul> </ul>
) : null} ) : null}
{s.paragraphsAfterBullets?.map((p) => (
<p class="mt-3 text-sm leading-relaxed text-nx-muted sm:text-base">{p}</p>
))}
</section> </section>
)) ))
} }

View File

@ -4,13 +4,13 @@ import { termsDocument, termsSections, termsRiferimenti, termsCrossReferences }
const link = 'text-sky-400/90 underline-offset-2 hover:text-sky-300 hover:underline'; const link = 'text-sky-400/90 underline-offset-2 hover:text-sky-300 hover:underline';
const lead = const lead =
"Si applicano alla navigazione e all'uso del sito informativo pubblico di NexStudio (sede: Bangkok, Thailandia). I contratti d'abbonamento o d'acquisto sui prodotti software restano retti da offerte, ordini o accordi separati. " + "Si applicano alla navigazione e all'uso del sito informativo pubblico di NexStudio S.r.l., con sede legale in Italia, [INDIRIZZO_COMPLETO]. I contratti d'abbonamento o d'acquisto sui prodotti software restano retti da offerte, ordini o accordi separati. " +
termsCrossReferences.leadNote; termsCrossReferences.leadNote;
--- ---
<SubpageLayout <SubpageLayout
title="Condizioni duso — NexStudio" title="Condizioni duso — NexStudio"
description="Condizioni duso del sito web NexStudio: accettazione, utilizzo lecito, responsabilità, legge applicabile. Bangkok, LexAura e MediAura, servizi B2B per contratto a parte." description="Condizioni duso del sito web NexStudio S.r.l.: accettazione, utilizzo lecito, responsabilità, legge italiana e foro di Padova. LexAura e MediAura, servizi B2B per contratto a parte."
heading="Condizioni duso" heading="Condizioni duso"
lead={lead} lead={lead}
> >

View File

@ -0,0 +1,4 @@
---
import Page from '../../newsletter/conferma-errore.astro';
---
<Page />

View File

@ -0,0 +1,4 @@
---
import Page from '../../newsletter/confermata.astro';
---
<Page />

View File

@ -0,0 +1,4 @@
---
import Page from '../../newsletter/disiscrizione-errore.astro';
---
<Page />

View File

@ -0,0 +1,4 @@
---
import Page from '../../newsletter/disiscrizione.astro';
---
<Page />

View File

@ -9,5 +9,18 @@
}, },
"observability": { "observability": {
"enabled": true "enabled": true
} },
} "d1_databases": [
{
"binding": "NEWSLETTER_DB",
"database_name": "nexstudio-newsletter",
"database_id": "f2b30e34-6e63-4df8-8f92-f0a0dad08cda",
"migrations_dir": "migrations"
}
],
"send_email": [
{
"name": "EMAIL"
}
]
}