Da review avversariale indipendente:
- Retry invio email (trySend, 3 tentativi backoff) per ricevuta cliente e notifica merchant.
- Webhook GDPR implementati (erano stub) con idempotenza (skip se gia' processed):
* customers/redact: pseudonimizza PII (nome/email/dichiarazione) nelle
WithdrawalRequest del cliente, mantiene il record legale (ordine+timestamp)
come prova art. 54-bis (base art. 17(3) GDPR).
* shop/redact: purge completa dei dati shop (Settings/Exclusion/Withdrawal/
Session/AuditLog + WebhookEvent stale).
* customers/data_request: registra la richiesta + n. record (merchant li recupera
dalla dashboard Recessi).
- Rate-limit: pruning periodico del bucket in-memory (fix memory leak).
- Ricevuta fallita: messaggio di successo onesto (successMessage riceve receipt.ok)
invece del falso 'ti abbiamo inviato la ricevuta'.
- PII: rimossa dai detail audit persistiti degli errori SMTP (ricevuta/notifica).
Non modificati (verificati): off-by-1 finestra = permissivo, non blocca a torto;
'doppio escape' = falso allarme (template e valori escapati una volta ciascuno).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Mv83a29B4eFv5ixoj6PoE1
65 lines
2.1 KiB
TypeScript
65 lines
2.1 KiB
TypeScript
import type { ActionFunctionArgs } from "@remix-run/node";
|
|
import { createHash } from "node:crypto";
|
|
import { authenticate } from "../shopify.server";
|
|
import db from "../db.server";
|
|
|
|
/**
|
|
* GDPR mandatory compliance webhook: customers/redact.
|
|
* Shopify sends this (typically 10 days after an order is cancelled/deleted, or
|
|
* on merchant request) to require deletion of a customer's personal data.
|
|
* HMAC is verified by authenticate.webhook(request); an invalid request throws.
|
|
*/
|
|
export const action = async ({ request }: ActionFunctionArgs) => {
|
|
const { shop, topic, payload, webhookId } = await authenticate.webhook(request);
|
|
|
|
console.log(`Received ${topic} webhook for ${shop}`);
|
|
|
|
const payloadHash = createHash("sha256")
|
|
.update(JSON.stringify(payload ?? {}))
|
|
.digest("hex");
|
|
|
|
// Idempotency: se gia' lavorato, esci.
|
|
const dedupeKey = webhookId ?? `${topic}:${payloadHash}`;
|
|
const existing = await db.webhookEvent.findUnique({
|
|
where: { webhookId: dedupeKey },
|
|
});
|
|
if (existing?.processed) return new Response();
|
|
await db.webhookEvent.upsert({
|
|
where: { webhookId: dedupeKey },
|
|
create: { shop, topic, webhookId: dedupeKey, processed: false },
|
|
update: {},
|
|
});
|
|
|
|
// Pseudonimizza la PII del cliente nelle richieste di recesso, mantenendo il
|
|
// record legale (ordine, timestamp) come prova ex art. 54-bis. Base di
|
|
// conservazione: obbligo legale / difesa in giudizio (art. 17(3) GDPR).
|
|
const email = ((payload as { customer?: { email?: unknown } } | null)?.customer
|
|
?.email ?? null) as string | null;
|
|
let redacted = 0;
|
|
if (typeof email === "string" && email) {
|
|
const res = await db.withdrawalRequest.updateMany({
|
|
where: { shop, email },
|
|
data: {
|
|
customerName: "[redatto]",
|
|
email: "[redatto]",
|
|
statementText: "[redatto]",
|
|
},
|
|
});
|
|
redacted = res.count;
|
|
}
|
|
|
|
await db.auditLog.create({
|
|
data: {
|
|
shop,
|
|
event: `gdpr.${topic}`,
|
|
payloadHash,
|
|
detail: `customers/redact: ${redacted} record pseudonimizzati`,
|
|
},
|
|
});
|
|
await db.webhookEvent.update({
|
|
where: { webhookId: dedupeKey },
|
|
data: { processed: true },
|
|
});
|
|
return new Response();
|
|
};
|