Skip to main content

PDFs y URLs firmadas

El portal genera dos PDFs en servidor con @react-pdf/renderer. Como no hay sesión de usuario, el acceso se protege mediante URLs firmadas con HMAC: cualquiera con el enlace puede abrir el documento, pero nadie puede fabricar un enlace para otra devolución.

1. Firma y verificación (app/lib/crypto.tsx)

El payload firmado son tres campos: returnId, email y language.

encoded = base64url(JSON.stringify({ returnId, email, language }))
signature = HMAC_SHA256(encoded, PDF_SIGNATURE_SECRET) en hexadecimal
url = /doc/<encoded>.<signature> (o /status/<encoded>.<signature>)

verifySignedPdfUrl() parte el segmento por ., recalcula el HMAC y solo devuelve el payload si la firma coincide; en cualquier otro caso devuelve null y la ruta responde 400 Invalid or tampered URL.

Implicaciones operativas:

  • El email va dentro de la URL (codificado en base64url, no cifrado). Quien tenga el enlace conoce el email del cliente. Los enlaces se distribuyen por email al propio cliente y desde la pantalla de confirmación.
  • No hay caducidad: el payload no incluye exp, así que un enlace es válido de forma indefinida mientras no cambie PDF_SIGNATURE_SECRET. Rotar ese secreto invalida todos los enlaces emitidos, incluidos los ya enviados por email.
  • El idioma va firmado, no se puede alterar sin invalidar la URL.

2. Rutas de renderizado

RutaComponenteContenido
/doc/[data]PdfDocumentNombre, email, nº de devolución, nº de pedido, QR del número de pedido, tabla de artículos, método de devolución e instrucciones
/status/[data]PdfStatusDocumentLos mismos datos más el estado de la devolución y de cada línea; sin QR

Ambas rutas siguen el mismo esquema:

  1. Verifican la firma del segmento [data].
  2. Con returnId + email piden el caso a robo-api (fetchReturn/return/info).
  3. Construyen el documento y lo devuelven con renderToStream.

Cabeceras de respuesta:

Content-Type: application/pdf
Content-Disposition: inline; filename="HWRET12345.pdf"

Con el query param ?download=true el Content-Disposition pasa a attachment, forzando la descarga. Es la variante que usa el botón de descarga de ReturnDetails.

3. Preparación de los datos (lib/returns/getReturnPdf.tsx)

Un único helper alimenta los dos documentos. Hace en paralelo (Promise.all) las llamadas a /return-status/info y /return-line-status/info y devuelve:

  • fullName (first_name + last_name), email, orderId, returnId.
  • lineItems: SKU, nombre y estado traducido de cada línea, filtrando la línea de gastos de envío (SKU S00233).
  • returnStatusName: estado de la devolución traducido, con fallback al idioma por defecto (en) si no existe traducción.
  • returnMethodName y returnMethodInstructionsParsed.

Instrucciones HTML → componentes PDF

Las instrucciones del método llegan de robo-api como HTML. parseHtmlToPdfComponents (en lib/pdf.tsx) las parsea con htmlparser2 y las convierte a primitivas de @react-pdf/renderer. Etiquetas soportadas:

HTMLResultado en el PDF
pView con margen inferior
ul / liLista con viñeta
aLink con href y subrayado
strong / bTexto en negrita
em / iTexto en cursiva
brSalto de línea
span y cualquier otraText plano

4. Idioma del PDF

resolvePdfLocale(preferred) establece la prioridad:

  1. El language firmado en la URL, normalizado a dos letras minúsculas y solo si está entre los idiomas soportados.
  2. En su defecto, el locale ambiente de la petición (getLocale() de next-intl: cookie HW_preferred_locale / Accept-Language).

getPdfTranslators(language) crea traductores con createTranslator sobre messages/<language>.json para los namespaces Form y pdf, saltándose el locale ambiente. Esto es lo que garantiza que un PDF enviado por email se renderice en el idioma en que el cliente hizo la devolución, aunque después lo abra un navegador con otro idioma (commit 0f29652).

5. Fuentes e imágenes

  • Fuentes: NotoSans Regular / SemiBold / Bold desde public/fonts, registradas con Font.register (familia NotoSans). Es la única tipografía de los PDFs — la Roboto del layout web no se usa aquí — y cubre el alfabeto griego que necesita el locale el.
  • Logo: getPngBase64('images/logo-hawkers-black.png') lee el fichero con fs en el arranque del módulo y lo embute como data URI (@react-pdf/renderer no descarga imágenes remotas de forma fiable).
  • QR: qrcode.toDataURL(orderId, { margin: 2 }) en servidor para el PDF, y next-qrcode en cliente para la pantalla de confirmación. Ambos codifican el número de pedido (desde el commit f6c33d7; antes se usaba el identificador interno).