* fix(invoices): embed company logo as PNG so it renders on invoice PDFs (#772) @react-pdf/renderer's <Image> only decodes JPG/PNG, but the logo upload route and the `logos` bucket also accept SVG and WebP. For an SVG/WebP logo @react-pdf silently swallows the decode error (console.warn inside a try/catch in its fetchImage step), so the invoice renders with NO logo and nothing surfaces — "Logotyp kommer inte med på fakturor". Fix: prepareInvoicePdfRender now fetches the stored logo and re-encodes it to a PNG data URL via sharp (SVGs rasterized at higher density), handing the template a company whose logo_url is that data URL. Renders regardless of upload format and removes the render-time dependency on a remote fetch inside @react-pdf. Falls back to the original URL unchanged on any failure (network, unreadable image, sharp unavailable), so behaviour is never worse than before. Result is cached per logo URL (5-min TTL, bounded to 50) since the logo is re-rendered on every invoice — twice per send and once per invoice in recurring/batch loops. prepareInvoicePdfRender becomes async and returns the resolved { branding, company }; all 8 call sites updated (6 routes, recurring-schedule-service, pending-operations/commit) to await it and pass the resolved company. Layered cleanly on top of the Swish-QR feature already on main — both coexist at every call site. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(invoices): bound and dedupe the logo fetch (review hardening) Review (PR Agent security): resolveLogoDataUrl fetched logo_url with no timeout or size limit. Add a 5s AbortSignal.timeout and a 5 MB cap (checked on the declared content-length and the read body) so a slow/oversized logo host can't hang or balloon an invoice render. SSRF itself isn't reachable today — logo_url is only ever set to a Supabase logos-bucket URL by the upload route — so an origin allowlist is intentionally skipped (would break self-hosted storage). Also coalesce concurrent renders of the same logo (preflight+final on a send, and recurring/batch loops) onto one in-flight fetch+encode instead of N. New test covers the size-cap fallback; existing SVG test now asserts the timeout signal. 9/9 pass. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
739f18fd1c
commit
10a0b1d8dd
@@ -169,14 +169,16 @@ export async function POST(
|
||||
// is stale and still reads 'draft' — override here so the archived
|
||||
// underlag isn't stamped "UTKAST – inte en giltig faktura".
|
||||
const renderableInvoice = { ...(invoice as Invoice), status: 'sent' as const }
|
||||
const { branding } = prepareInvoicePdfRender(settings as CompanySettings)
|
||||
const { branding, company: renderCompany } = await prepareInvoicePdfRender(
|
||||
settings as CompanySettings,
|
||||
)
|
||||
const swishQrDataUrl = await buildSwishQrDataUrl(settings as CompanySettings, renderableInvoice)
|
||||
const pdfBuffer = await renderToBuffer(
|
||||
InvoicePDF({
|
||||
invoice: renderableInvoice,
|
||||
customer: invoice.customer as Customer,
|
||||
items,
|
||||
company: settings as CompanySettings,
|
||||
company: renderCompany,
|
||||
originalInvoiceNumber,
|
||||
branding,
|
||||
swishQrDataUrl,
|
||||
|
||||
@@ -67,14 +67,16 @@ export async function GET(
|
||||
|
||||
try {
|
||||
// Generate PDF
|
||||
const { branding } = prepareInvoicePdfRender(company as CompanySettings)
|
||||
const { branding, company: renderCompany } = await prepareInvoicePdfRender(
|
||||
company as CompanySettings,
|
||||
)
|
||||
const swishQrDataUrl = await buildSwishQrDataUrl(company as CompanySettings, invoice as Invoice)
|
||||
const pdfBuffer = await renderToBuffer(
|
||||
InvoicePDF({
|
||||
invoice: invoice as Invoice,
|
||||
customer: invoice.customer as Customer,
|
||||
items,
|
||||
company: company as CompanySettings,
|
||||
company: renderCompany,
|
||||
originalInvoiceNumber,
|
||||
branding,
|
||||
swishQrDataUrl,
|
||||
|
||||
@@ -101,13 +101,13 @@ export const POST = withRouteContext(
|
||||
const isFreshAllocation = !invoice.invoice_number
|
||||
if (isFreshAllocation) {
|
||||
try {
|
||||
const preflight = prepareInvoicePdfRender(company as CompanySettings)
|
||||
const preflight = await prepareInvoicePdfRender(company as CompanySettings)
|
||||
await renderToBuffer(
|
||||
InvoicePDF({
|
||||
invoice: { ...(invoice as Invoice), invoice_number: 'F-PREVIEW' },
|
||||
customer,
|
||||
items,
|
||||
company: company as CompanySettings,
|
||||
company: preflight.company,
|
||||
originalInvoiceNumber,
|
||||
branding: preflight.branding,
|
||||
}),
|
||||
@@ -132,14 +132,16 @@ export const POST = withRouteContext(
|
||||
// ~185), but if we render with the stale 'draft' status the customer
|
||||
// receives a PDF stamped "UTKAST – inte en giltig faktura".
|
||||
const renderableInvoice = { ...(invoice as Invoice), status: 'sent' as const }
|
||||
const { branding } = prepareInvoicePdfRender(company as CompanySettings)
|
||||
const { branding, company: renderCompany } = await prepareInvoicePdfRender(
|
||||
company as CompanySettings,
|
||||
)
|
||||
const swishQrDataUrl = await buildSwishQrDataUrl(company as CompanySettings, renderableInvoice)
|
||||
const pdfBuffer = await renderToBuffer(
|
||||
InvoicePDF({
|
||||
invoice: renderableInvoice,
|
||||
customer,
|
||||
items,
|
||||
company: company as CompanySettings,
|
||||
company: renderCompany,
|
||||
originalInvoiceNumber,
|
||||
branding,
|
||||
swishQrDataUrl,
|
||||
|
||||
@@ -172,13 +172,15 @@ export async function POST(request: Request) {
|
||||
} as Invoice
|
||||
|
||||
try {
|
||||
const { branding } = prepareInvoicePdfRender(company as CompanySettings)
|
||||
const { branding, company: renderCompany } = await prepareInvoicePdfRender(
|
||||
company as CompanySettings,
|
||||
)
|
||||
const pdfBuffer = await renderToBuffer(
|
||||
InvoicePDF({
|
||||
invoice: previewInvoice,
|
||||
customer,
|
||||
items: invoiceItems,
|
||||
company: company as CompanySettings,
|
||||
company: renderCompany,
|
||||
isPreview: true,
|
||||
branding,
|
||||
})
|
||||
|
||||
@@ -149,14 +149,16 @@ export const GET = withApiV1<{ params: Promise<{ companyId: string; id: string }
|
||||
|
||||
let pdfBuffer: Buffer
|
||||
try {
|
||||
const { branding } = prepareInvoicePdfRender(company as CompanySettings)
|
||||
const { branding, company: renderCompany } = await prepareInvoicePdfRender(
|
||||
company as CompanySettings,
|
||||
)
|
||||
const swishQrDataUrl = await buildSwishQrDataUrl(company as CompanySettings, typed as Invoice)
|
||||
pdfBuffer = await renderToBuffer(
|
||||
InvoicePDF({
|
||||
invoice: typed as Invoice,
|
||||
customer: typed.customer as Customer,
|
||||
items,
|
||||
company: company as CompanySettings,
|
||||
company: renderCompany,
|
||||
originalInvoiceNumber,
|
||||
branding,
|
||||
swishQrDataUrl,
|
||||
|
||||
@@ -271,13 +271,13 @@ export const POST = withApiV1<{ params: Promise<{ companyId: string; id: string
|
||||
const isFreshAllocation = !typed.invoice_number
|
||||
if (isFreshAllocation) {
|
||||
try {
|
||||
const preflight = prepareInvoicePdfRender(settings)
|
||||
const preflight = await prepareInvoicePdfRender(settings)
|
||||
await renderToBuffer(
|
||||
InvoicePDF({
|
||||
invoice: { ...(typed as Invoice), invoice_number: 'F-PREVIEW' },
|
||||
customer,
|
||||
items,
|
||||
company: settings,
|
||||
company: preflight.company,
|
||||
originalInvoiceNumber,
|
||||
branding: preflight.branding,
|
||||
}),
|
||||
@@ -361,14 +361,14 @@ export const POST = withApiV1<{ params: Promise<{ companyId: string; id: string
|
||||
|
||||
let pdfBuffer: Buffer
|
||||
try {
|
||||
const { branding } = prepareInvoicePdfRender(settings)
|
||||
const { branding, company: renderCompany } = await prepareInvoicePdfRender(settings)
|
||||
const swishQrDataUrl = await buildSwishQrDataUrl(settings, renderableInvoice)
|
||||
pdfBuffer = await renderToBuffer(
|
||||
InvoicePDF({
|
||||
invoice: renderableInvoice,
|
||||
customer,
|
||||
items,
|
||||
company: settings,
|
||||
company: renderCompany,
|
||||
originalInvoiceNumber,
|
||||
branding,
|
||||
swishQrDataUrl,
|
||||
|
||||
Reference in New Issue
Block a user