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 cambiePDF_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
| Ruta | Componente | Contenido |
|---|---|---|
/doc/[data] | PdfDocument | Nombre, 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] | PdfStatusDocument | Los mismos datos más el estado de la devolución y de cada línea; sin QR |
Ambas rutas siguen el mismo esquema:
- Verifican la firma del segmento
[data]. - Con
returnId+emailpiden el caso a robo-api (fetchReturn→/return/info). - 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 (SKUS00233).returnStatusName: estado de la devolución traducido, con fallback al idioma por defecto (en) si no existe traducción.returnMethodNameyreturnMethodInstructionsParsed.
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:
| HTML | Resultado en el PDF |
|---|---|
p | View con margen inferior |
ul / li | Lista con viñeta • |
a | Link con href y subrayado |
strong / b | Texto en negrita |
em / i | Texto en cursiva |
br | Salto de línea |
span y cualquier otra | Text plano |
4. Idioma del PDF
resolvePdfLocale(preferred) establece la prioridad:
- El
languagefirmado en la URL, normalizado a dos letras minúsculas y solo si está entre los idiomas soportados. - En su defecto, el locale ambiente de la petición (
getLocale()denext-intl: cookieHW_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 conFont.register(familiaNotoSans). 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 localeel. - Logo:
getPngBase64('images/logo-hawkers-black.png')lee el fichero confsen el arranque del módulo y lo embute como data URI (@react-pdf/rendererno descarga imágenes remotas de forma fiable). - QR:
qrcode.toDataURL(orderId, { margin: 2 })en servidor para el PDF, ynext-qrcodeen cliente para la pantalla de confirmación. Ambos codifican el número de pedido (desde el commitf6c33d7; antes se usaba el identificador interno).