feat: integrate PDF generation using Puppeteer
- Removed dependencies on html2canvas-pro and jsPDF. - Added puppeteer-core for PDF generation. - Created a new API endpoint for generating PDFs. - Updated SchedaAnnuncioStampabile component to use the new PDF generation API. - Enhanced error handling and user feedback during PDF generation. - Added browserless service to docker-compose for Puppeteer. - Updated environment variables to include BROWSER_WS_URL. - Refactored code to improve readability and maintainability.
This commit is contained in:
parent
217d6c7982
commit
42c15dfe96
12 changed files with 1226 additions and 651 deletions
|
|
@ -85,6 +85,8 @@ ARG STORAGE_URL
|
||||||
ENV STORAGE_URL=$STORAGE_URL
|
ENV STORAGE_URL=$STORAGE_URL
|
||||||
ARG STORAGE_TOKEN
|
ARG STORAGE_TOKEN
|
||||||
ENV STORAGE_TOKEN=$STORAGE_TOKEN
|
ENV STORAGE_TOKEN=$STORAGE_TOKEN
|
||||||
|
ARG BROWSER_WS_URL
|
||||||
|
ENV BROWSER_WS_URL=$BROWSER_WS_URL
|
||||||
|
|
||||||
RUN apk add --no-cache curl
|
RUN apk add --no-cache curl
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -32,6 +32,14 @@ services:
|
||||||
timeout: 5s
|
timeout: 5s
|
||||||
retries: 5
|
retries: 5
|
||||||
start_period: 10s
|
start_period: 10s
|
||||||
|
browserless:
|
||||||
|
image: browserless/chrome:latest
|
||||||
|
ports:
|
||||||
|
- "8081:3000" # Optional: Access the debug dashboard at localhost:8080
|
||||||
|
environment:
|
||||||
|
- MAX_CONCURRENT_SESSIONS=5
|
||||||
|
- CONNECTION_TIMEOUT=30000
|
||||||
|
restart: always
|
||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
tiles-data:
|
tiles-data:
|
||||||
|
|
|
||||||
1005
apps/infoalloggi/package-lock.json
generated
1005
apps/infoalloggi/package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
|
@ -61,10 +61,8 @@
|
||||||
"embla-carousel-react": "^8.6.0",
|
"embla-carousel-react": "^8.6.0",
|
||||||
"framer-motion": "^12.27.5",
|
"framer-motion": "^12.27.5",
|
||||||
"frimousse": "^0.3.0",
|
"frimousse": "^0.3.0",
|
||||||
"html2canvas-pro": "^1.6.5",
|
|
||||||
"ioredis": "^5.9.2",
|
"ioredis": "^5.9.2",
|
||||||
"jose": "^6.0.12",
|
"jose": "^6.0.12",
|
||||||
"jspdf": "^4.0.0",
|
|
||||||
"kysely": "^0.28.10",
|
"kysely": "^0.28.10",
|
||||||
"kysely-plugin-serialize": "^0.8.2",
|
"kysely-plugin-serialize": "^0.8.2",
|
||||||
"leaflet": "^1.9.4",
|
"leaflet": "^1.9.4",
|
||||||
|
|
@ -78,6 +76,7 @@
|
||||||
"nuqs": "^2.8.6",
|
"nuqs": "^2.8.6",
|
||||||
"pg": "^8.17.1",
|
"pg": "^8.17.1",
|
||||||
"postcss": "^8.5.6",
|
"postcss": "^8.5.6",
|
||||||
|
"puppeteer-core": "^24.36.1",
|
||||||
"radix-ui": "^1.4.3",
|
"radix-ui": "^1.4.3",
|
||||||
"react": "^19.2.3",
|
"react": "^19.2.3",
|
||||||
"react-colorful": "^5.6.1",
|
"react-colorful": "^5.6.1",
|
||||||
|
|
@ -134,4 +133,4 @@
|
||||||
"ct3aMetadata": {
|
"ct3aMetadata": {
|
||||||
"initVersion": "7.9.0"
|
"initVersion": "7.9.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,7 @@
|
||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import html2canvas from "html2canvas-pro";
|
import { useEffect, useState } from "react";
|
||||||
import jsPDF from "jspdf";
|
|
||||||
import { useEffect, useRef, useState } from "react";
|
|
||||||
import toast from "react-hot-toast";
|
import toast from "react-hot-toast";
|
||||||
import { useReactToPrint } from "react-to-print";
|
|
||||||
import z from "zod";
|
import z from "zod";
|
||||||
import { Button } from "~/components/ui/button";
|
import { Button } from "~/components/ui/button";
|
||||||
import { Card } from "~/components/ui/card";
|
import { Card } from "~/components/ui/card";
|
||||||
|
|
@ -12,7 +9,7 @@ import { filteredCaratteristiche } from "~/hooks/schedaAnnuncioUtils";
|
||||||
import { it } from "~/i18n/it";
|
import { it } from "~/i18n/it";
|
||||||
import { handleConsegna } from "~/lib/annuncio_details";
|
import { handleConsegna } from "~/lib/annuncio_details";
|
||||||
import { replaceWithBr } from "~/lib/newlineToBr";
|
import { replaceWithBr } from "~/lib/newlineToBr";
|
||||||
import { formatCurrency } from "~/lib/utils";
|
import { cn, formatCurrency } from "~/lib/utils";
|
||||||
import type { Annunci } from "~/schemas/public/Annunci";
|
import type { Annunci } from "~/schemas/public/Annunci";
|
||||||
import { api } from "~/utils/api";
|
import { api } from "~/utils/api";
|
||||||
import { IconMatrix } from "./IconComponents";
|
import { IconMatrix } from "./IconComponents";
|
||||||
|
|
@ -21,14 +18,25 @@ import { Input } from "./ui/input";
|
||||||
import { Label } from "./ui/label";
|
import { Label } from "./ui/label";
|
||||||
import { Popover, PopoverContent, PopoverTrigger } from "./ui/popover";
|
import { Popover, PopoverContent, PopoverTrigger } from "./ui/popover";
|
||||||
|
|
||||||
|
const blobToBase64 = (blob: Blob): Promise<string> => {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const reader = new FileReader();
|
||||||
|
reader.onerror = reject;
|
||||||
|
reader.onload = () => {
|
||||||
|
if (typeof reader.result === "string") {
|
||||||
|
// Remove the Data-URL prefix (e.g. "data:application/pdf;base64,") to get just the base64
|
||||||
|
resolve(reader.result.split(",")[1] || "");
|
||||||
|
} else {
|
||||||
|
reject(new Error("Failed to convert blob to text"));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
reader.readAsDataURL(blob);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
export function SchedaAnnuncioStampabile({ data }: { data: Annunci }) {
|
export function SchedaAnnuncioStampabile({ data }: { data: Annunci }) {
|
||||||
const [inputEmail, setInputEmail] = useState<string>("");
|
const [inputEmail, setInputEmail] = useState<string>("");
|
||||||
const [openPopover, setOpenPopover] = useState<boolean>(false);
|
const [openPopover, setOpenPopover] = useState<boolean>(false);
|
||||||
const contentRef = useRef<HTMLDivElement>(null);
|
|
||||||
const reactToPrintFn = useReactToPrint({
|
|
||||||
contentRef,
|
|
||||||
//print: async (target) => {console.log("Printing...", target.contentDocument);},
|
|
||||||
});
|
|
||||||
|
|
||||||
const { mutate } = api.comunicazioni.sendSchedaAnnuncioEmail.useMutation({
|
const { mutate } = api.comunicazioni.sendSchedaAnnuncioEmail.useMutation({
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
|
|
@ -43,141 +51,81 @@ export function SchedaAnnuncioStampabile({ data }: { data: Annunci }) {
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const sendEmail = useReactToPrint({
|
const sendPdf = async () => {
|
||||||
contentRef,
|
if (!data || !inputEmail || !data.locatore || !data.numero) {
|
||||||
print: async (target) => {
|
toast.error("Dati annuncio mancanti.");
|
||||||
try {
|
return;
|
||||||
const htmlContent = target.contentDocument?.documentElement;
|
}
|
||||||
if (!htmlContent) {
|
try {
|
||||||
toast.error("Impossibile generare il contenuto per la stampa.");
|
toast.loading("Generazione PDF in corso...", {
|
||||||
return;
|
id: "generate-pdf",
|
||||||
}
|
});
|
||||||
if (!data || !inputEmail || !data.locatore || !data.numero) {
|
const response = await fetch(`/api/generate-pdf?id=${data.id}`);
|
||||||
toast.error("Dati annuncio o indirizzo email mancanti.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
toast.loading("Invio email in corso...", {
|
|
||||||
id: "send-scheda-annuncio-email",
|
|
||||||
});
|
|
||||||
setOpenPopover(false);
|
|
||||||
htmlContent.querySelectorAll(".print\\:border-none").forEach((el) => {
|
|
||||||
el.classList.remove("border");
|
|
||||||
});
|
|
||||||
htmlContent.querySelectorAll(".print\\:shadow-none").forEach((el) => {
|
|
||||||
el.classList.remove("shadow-sm");
|
|
||||||
});
|
|
||||||
htmlContent.querySelectorAll("#navigationLink").forEach((el) => {
|
|
||||||
el.classList.remove("flex");
|
|
||||||
el.classList.add("hidden");
|
|
||||||
});
|
|
||||||
htmlContent.querySelectorAll("#mainLogoSvg text").forEach((el) => {
|
|
||||||
el.classList.remove("font-sans");
|
|
||||||
el.classList.add("font-[system-ui]");
|
|
||||||
});
|
|
||||||
const canvas = await html2canvas(htmlContent, {
|
|
||||||
//scale: 2,
|
|
||||||
useCORS: true,
|
|
||||||
logging: false,
|
|
||||||
backgroundColor: "#ffffff",
|
|
||||||
});
|
|
||||||
|
|
||||||
const imgData = canvas.toDataURL("image/png");
|
if (!response.ok) {
|
||||||
const pdf = new jsPDF("p", "mm", "a4");
|
throw new Error(
|
||||||
|
`Errore nella generazione del PDF: ${response.statusText}`,
|
||||||
// A4 dimensions in mm
|
|
||||||
const pdfWidth = pdf.internal.pageSize.getWidth();
|
|
||||||
const pdfHeight = pdf.internal.pageSize.getHeight();
|
|
||||||
|
|
||||||
// Define margins (in mm)
|
|
||||||
const marginTop = 5;
|
|
||||||
const marginRight = 5;
|
|
||||||
const marginBottom = 5;
|
|
||||||
const marginLeft = 5;
|
|
||||||
|
|
||||||
// Calculate available space
|
|
||||||
const availableWidth = pdfWidth - marginLeft - marginRight;
|
|
||||||
const availableHeight = pdfHeight - marginTop - marginBottom;
|
|
||||||
|
|
||||||
// Calculate image dimensions
|
|
||||||
const imgProps = pdf.getImageProperties(imgData);
|
|
||||||
const imgWidth = imgProps.width;
|
|
||||||
const imgHeight = imgProps.height;
|
|
||||||
|
|
||||||
// Calculate how the image should fit with margins
|
|
||||||
const ratio = availableWidth / imgWidth;
|
|
||||||
const scaledHeight = imgHeight * ratio;
|
|
||||||
|
|
||||||
// Check if content needs multiple pages
|
|
||||||
if (scaledHeight <= availableHeight) {
|
|
||||||
// Single page - content fits
|
|
||||||
pdf.addImage(
|
|
||||||
imgData,
|
|
||||||
"PNG",
|
|
||||||
marginLeft,
|
|
||||||
marginTop,
|
|
||||||
availableWidth,
|
|
||||||
scaledHeight,
|
|
||||||
undefined,
|
|
||||||
"FAST",
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
// Multiple pages needed
|
|
||||||
let heightLeft = scaledHeight;
|
|
||||||
let position = marginTop;
|
|
||||||
|
|
||||||
// Add first page
|
|
||||||
pdf.addImage(
|
|
||||||
imgData,
|
|
||||||
"PNG",
|
|
||||||
marginLeft,
|
|
||||||
position,
|
|
||||||
availableWidth,
|
|
||||||
scaledHeight,
|
|
||||||
undefined,
|
|
||||||
"FAST",
|
|
||||||
);
|
|
||||||
heightLeft -= availableHeight;
|
|
||||||
|
|
||||||
// Add remaining pages
|
|
||||||
while (heightLeft > 0) {
|
|
||||||
position = marginTop - (scaledHeight - heightLeft);
|
|
||||||
pdf.addPage();
|
|
||||||
pdf.addImage(
|
|
||||||
imgData,
|
|
||||||
"PNG",
|
|
||||||
marginLeft,
|
|
||||||
position,
|
|
||||||
availableWidth,
|
|
||||||
scaledHeight,
|
|
||||||
undefined,
|
|
||||||
"FAST",
|
|
||||||
);
|
|
||||||
heightLeft -= availableHeight;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const pdfBase64 = pdf.output("dataurlstring");
|
|
||||||
|
|
||||||
mutate({
|
|
||||||
pdf: pdfBase64,
|
|
||||||
to: inputEmail,
|
|
||||||
codice: data.codice,
|
|
||||||
numero: data.numero,
|
|
||||||
nominativo: data.locatore,
|
|
||||||
indirizzo: `${data.indirizzo}, ${data.civico} ${data.comune} ${data.cap} (${data.provincia})`,
|
|
||||||
});
|
|
||||||
setInputEmail("");
|
|
||||||
} catch (e) {
|
|
||||||
toast.error(
|
|
||||||
`Errore durante l'invio dell'email: ${(e as Error).message}`,
|
|
||||||
{
|
|
||||||
id: "send-scheda-annuncio-email",
|
|
||||||
},
|
|
||||||
);
|
);
|
||||||
console.error("Error during print:", e);
|
|
||||||
setInputEmail("");
|
|
||||||
}
|
}
|
||||||
},
|
const blob = await response.blob();
|
||||||
});
|
|
||||||
|
const pdfBase64 = await blobToBase64(blob);
|
||||||
|
|
||||||
|
mutate({
|
||||||
|
pdf: pdfBase64,
|
||||||
|
to: inputEmail,
|
||||||
|
codice: data.codice,
|
||||||
|
numero: data.numero,
|
||||||
|
nominativo: data.locatore,
|
||||||
|
indirizzo: `${data.indirizzo}, ${data.civico} ${data.comune} ${data.cap} (${data.provincia})`,
|
||||||
|
});
|
||||||
|
setInputEmail("");
|
||||||
|
|
||||||
|
toast.success("PDF generato con successo!", {
|
||||||
|
id: "generate-pdf",
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
toast.error(
|
||||||
|
`Errore durante la generazione del PDF: ${(error as Error).message}`,
|
||||||
|
{
|
||||||
|
id: "generate-pdf",
|
||||||
|
},
|
||||||
|
);
|
||||||
|
console.error("Error generating PDF:", error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const generatePdf = async () => {
|
||||||
|
if (!data) {
|
||||||
|
toast.error("Dati annuncio mancanti.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
toast.loading("Generazione PDF in corso...", {
|
||||||
|
id: "generate-pdf",
|
||||||
|
});
|
||||||
|
const response = await fetch(`/api/generate-pdf?id=${data.id}`);
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(
|
||||||
|
`Errore nella generazione del PDF: ${response.statusText}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
const blob = await response.blob();
|
||||||
|
const url = window.URL.createObjectURL(blob);
|
||||||
|
toast.success("PDF generato con successo!", {
|
||||||
|
id: "generate-pdf",
|
||||||
|
});
|
||||||
|
window.open(url, "_blank");
|
||||||
|
} catch (error) {
|
||||||
|
toast.error(
|
||||||
|
`Errore durante la generazione del PDF: ${(error as Error).message}`,
|
||||||
|
{
|
||||||
|
id: "generate-pdf",
|
||||||
|
},
|
||||||
|
);
|
||||||
|
console.error("Error generating PDF:", error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="mx-auto max-w-4xl">
|
<div className="mx-auto max-w-4xl">
|
||||||
|
|
@ -185,9 +133,8 @@ export function SchedaAnnuncioStampabile({ data }: { data: Annunci }) {
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<h1 className="font-bold text-2xl">Scheda Annuncio Stampabile</h1>
|
<h1 className="font-bold text-2xl">Scheda Annuncio Stampabile</h1>
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
<Button className="print:hidden" onClick={() => reactToPrintFn()}>
|
<Button onClick={() => generatePdf()}>Genera Scheda</Button>
|
||||||
Stampa Scheda
|
|
||||||
</Button>
|
|
||||||
<Popover onOpenChange={setOpenPopover} open={openPopover}>
|
<Popover onOpenChange={setOpenPopover} open={openPopover}>
|
||||||
<PopoverTrigger asChild>
|
<PopoverTrigger asChild>
|
||||||
<Button className="print:hidden" variant="outline">
|
<Button className="print:hidden" variant="outline">
|
||||||
|
|
@ -204,7 +151,7 @@ export function SchedaAnnuncioStampabile({ data }: { data: Annunci }) {
|
||||||
e.key === "Enter" &&
|
e.key === "Enter" &&
|
||||||
z.safeParse(z.email(), inputEmail).success
|
z.safeParse(z.email(), inputEmail).success
|
||||||
) {
|
) {
|
||||||
sendEmail();
|
sendPdf();
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
type="email"
|
type="email"
|
||||||
|
|
@ -213,7 +160,7 @@ export function SchedaAnnuncioStampabile({ data }: { data: Annunci }) {
|
||||||
disabled={
|
disabled={
|
||||||
z.safeParse(z.email(), inputEmail).success === false
|
z.safeParse(z.email(), inputEmail).success === false
|
||||||
}
|
}
|
||||||
onClick={sendEmail}
|
onClick={sendPdf}
|
||||||
>
|
>
|
||||||
Invia
|
Invia
|
||||||
</Button>
|
</Button>
|
||||||
|
|
@ -221,15 +168,19 @@ export function SchedaAnnuncioStampabile({ data }: { data: Annunci }) {
|
||||||
</Popover>
|
</Popover>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div ref={contentRef}>
|
<SchedaAnnuncio data={data} />
|
||||||
<SchedaAnnuncio data={data} />
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function SchedaAnnuncio({ data }: { data: Annunci }) {
|
export function SchedaAnnuncio({
|
||||||
|
data,
|
||||||
|
raw,
|
||||||
|
}: {
|
||||||
|
data: Annunci;
|
||||||
|
raw?: boolean;
|
||||||
|
}) {
|
||||||
const companyAddress =
|
const companyAddress =
|
||||||
"Via Beata Giovanna, 1 a Bassano del Grappa (VI)\nTel. +39 0424529869\nEmail: arca@infoalloggi.it";
|
"Via Beata Giovanna, 1 a Bassano del Grappa (VI)\nTel. +39 0424529869\nEmail: arca@infoalloggi.it";
|
||||||
|
|
||||||
|
|
@ -239,7 +190,26 @@ function SchedaAnnuncio({ data }: { data: Annunci }) {
|
||||||
const [navigationUrl, setNavigationUrl] = useState<string | undefined>(
|
const [navigationUrl, setNavigationUrl] = useState<string | undefined>(
|
||||||
undefined,
|
undefined,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const [printDate, setPrintDate] = useState<string>("");
|
||||||
|
const [disponibileDate, setDisponibileDate] = useState<string>("");
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
setPrintDate(new Date().toLocaleDateString("it-IT"));
|
||||||
|
if (data.disponibile_da) {
|
||||||
|
setDisponibileDate(new Date(data.disponibile_da).toLocaleDateString());
|
||||||
|
} else if (data.consegna) {
|
||||||
|
setDisponibileDate(
|
||||||
|
handleConsegna({
|
||||||
|
aggiornamento: it.card.in_aggiornamento,
|
||||||
|
consegna: data.consegna,
|
||||||
|
consegna_da: it.card.consegna_da,
|
||||||
|
mesi: it.parametri.mesi,
|
||||||
|
subito: it.card.consegna_subito,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
setDisponibileDate("Non specificata");
|
||||||
|
}
|
||||||
if (!data) return;
|
if (!data) return;
|
||||||
if (typeof window === "undefined") return;
|
if (typeof window === "undefined") return;
|
||||||
if (
|
if (
|
||||||
|
|
@ -260,7 +230,12 @@ function SchedaAnnuncio({ data }: { data: Annunci }) {
|
||||||
}, [data]);
|
}, [data]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card className="p-4 print:border-none print:shadow-none">
|
<Card
|
||||||
|
className={cn(
|
||||||
|
"p-4 print:border-none print:shadow-none",
|
||||||
|
raw && "border-none shadow-none",
|
||||||
|
)}
|
||||||
|
>
|
||||||
<div className="space-y-8">
|
<div className="space-y-8">
|
||||||
<div className="flex items-start justify-between">
|
<div className="flex items-start justify-between">
|
||||||
<div>
|
<div>
|
||||||
|
|
@ -376,18 +351,7 @@ function SchedaAnnuncio({ data }: { data: Annunci }) {
|
||||||
{(data.disponibile_da || data.consegna) && (
|
{(data.disponibile_da || data.consegna) && (
|
||||||
<div className="rounded bg-gray-50 p-3">
|
<div className="rounded bg-gray-50 p-3">
|
||||||
<p className="font-medium text-primary">Disponibile da</p>
|
<p className="font-medium text-primary">Disponibile da</p>
|
||||||
<p className="font-bold text-xl">
|
<p className="font-bold text-xl">{disponibileDate}</p>
|
||||||
{data.disponibile_da
|
|
||||||
? new Date(data.disponibile_da).toLocaleDateString()
|
|
||||||
: data.consegna &&
|
|
||||||
handleConsegna({
|
|
||||||
aggiornamento: it.card.in_aggiornamento,
|
|
||||||
consegna: data.consegna,
|
|
||||||
consegna_da: it.card.consegna_da,
|
|
||||||
mesi: it.parametri.mesi,
|
|
||||||
subito: it.card.consegna_subito,
|
|
||||||
})}
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -447,9 +411,7 @@ function SchedaAnnuncio({ data }: { data: Annunci }) {
|
||||||
</div>
|
</div>
|
||||||
<div className="pt-2 text-center text-muted-foreground text-xs">
|
<div className="pt-2 text-center text-muted-foreground text-xs">
|
||||||
<p>{footer}</p>
|
<p>{footer}</p>
|
||||||
<p className="mt-1">
|
<p className="mt-1">Documento generato in data: {printDate}</p>
|
||||||
Documento generato in data: {new Date().toLocaleDateString("it-IT")}
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
|
||||||
|
|
@ -30,6 +30,7 @@ export const env = createEnv({
|
||||||
REVALIDATION_SECRET: z.string(),
|
REVALIDATION_SECRET: z.string(),
|
||||||
STORAGE_URL: z.string(),
|
STORAGE_URL: z.string(),
|
||||||
STORAGE_TOKEN: z.string(),
|
STORAGE_TOKEN: z.string(),
|
||||||
|
BROWSER_WS_URL: z.string(),
|
||||||
},
|
},
|
||||||
client: {
|
client: {
|
||||||
NEXT_PUBLIC_BASE_URL: z.string(),
|
NEXT_PUBLIC_BASE_URL: z.string(),
|
||||||
|
|
|
||||||
|
|
@ -511,6 +511,16 @@ const BudgetFilters = () => {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
{/*
|
||||||
|
<div className="flex w-full max-w-md flex-col gap-2">
|
||||||
|
<Label htmlFor="slider">Price Range</Label>
|
||||||
|
<Slider id="slider" max={1000} min={0} onValueChange={setValue} value={value} />
|
||||||
|
<div className="flex items-center justify-between text-muted-foreground text-sm">
|
||||||
|
<span>${value[0]}</span>
|
||||||
|
<span>${value[1]}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
*/}
|
||||||
<div className="flex flex-col gap-2">
|
<div className="flex flex-col gap-2">
|
||||||
<div className="flex h-4 flex-row gap-4 px-1">
|
<div className="flex h-4 flex-row gap-4 px-1">
|
||||||
<Label
|
<Label
|
||||||
|
|
|
||||||
54
apps/infoalloggi/src/pages/api/generate-pdf.ts
Normal file
54
apps/infoalloggi/src/pages/api/generate-pdf.ts
Normal file
|
|
@ -0,0 +1,54 @@
|
||||||
|
import type { NextApiRequest, NextApiResponse } from "next";
|
||||||
|
import puppeteer from "puppeteer-core";
|
||||||
|
import { env } from "~/env";
|
||||||
|
|
||||||
|
export default async function handler(
|
||||||
|
req: NextApiRequest,
|
||||||
|
res: NextApiResponse,
|
||||||
|
) {
|
||||||
|
let browser = null;
|
||||||
|
|
||||||
|
try {
|
||||||
|
browser = await puppeteer.connect({
|
||||||
|
browserWSEndpoint: env.BROWSER_WS_URL,
|
||||||
|
});
|
||||||
|
|
||||||
|
const page = await browser.newPage();
|
||||||
|
|
||||||
|
if (req.cookies && Object.keys(req.cookies).length > 0) {
|
||||||
|
const productionDomain = new URL(env.INTERNAL_BASE_URL).hostname;
|
||||||
|
|
||||||
|
const cookies = Object.entries(req.cookies).map(([name, value]) => ({
|
||||||
|
name,
|
||||||
|
value: value || "",
|
||||||
|
domain:
|
||||||
|
env.NODE_ENV === "production"
|
||||||
|
? productionDomain
|
||||||
|
: "host.docker.internal",
|
||||||
|
path: "/",
|
||||||
|
}));
|
||||||
|
await browser.setCookie(...cookies);
|
||||||
|
}
|
||||||
|
|
||||||
|
const baseUrl =
|
||||||
|
env.NODE_ENV === "production"
|
||||||
|
? env.INTERNAL_BASE_URL
|
||||||
|
: `http://host.docker.internal:3000`;
|
||||||
|
const targetUrl = `${baseUrl}/area-riservata/admin/scheda-annuncio-stampa/${req.query.id}?raw=true`;
|
||||||
|
await page.goto(targetUrl, { waitUntil: "networkidle0" });
|
||||||
|
|
||||||
|
const pdf = await page.pdf({
|
||||||
|
format: "A4",
|
||||||
|
printBackground: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
res.setHeader("Content-Type", "application/pdf");
|
||||||
|
res.send(Buffer.from(pdf));
|
||||||
|
} catch (error) {
|
||||||
|
console.error("PDF Generation Error:", error);
|
||||||
|
res.status(500).json({ error: "Failed to generate PDF" });
|
||||||
|
} finally {
|
||||||
|
// 'disconnect' tells browserless you're done with this tab
|
||||||
|
if (browser) await browser.disconnect();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,45 +1,44 @@
|
||||||
import type { GetServerSideProps } from "next";
|
import type { GetServerSideProps } from "next";
|
||||||
import Head from "next/head";
|
import Head from "next/head";
|
||||||
import { AreaRiservataLayout } from "~/components/Layout";
|
import { AreaRiservataLayout } from "~/components/Layout";
|
||||||
import { LoadingPage } from "~/components/loading";
|
import {
|
||||||
import { SchedaAnnuncioStampabile } from "~/components/schedaAnnuncioStampabile";
|
SchedaAnnuncio,
|
||||||
import { Status500 } from "~/components/status-page";
|
SchedaAnnuncioStampabile,
|
||||||
|
} from "~/components/schedaAnnuncioStampabile";
|
||||||
import type { NextPageWithLayout } from "~/pages/_app";
|
import type { NextPageWithLayout } from "~/pages/_app";
|
||||||
import type { AnnunciId } from "~/schemas/public/Annunci";
|
import type { AnnunciWithMedia } from "~/server/controllers/annunci.controller";
|
||||||
import { TrpcAuthedFetchingIstance } from "~/server/utils/ssgHelper";
|
import { generateSSGHelper } from "~/server/utils/ssgHelper";
|
||||||
import { zAnnuncioId } from "~/server/utils/zod_types";
|
import { zAnnuncioId } from "~/server/utils/zod_types";
|
||||||
import { api } from "~/utils/api";
|
|
||||||
|
|
||||||
type PageProps = {
|
type PageProps = {
|
||||||
id: AnnunciId;
|
raw: boolean;
|
||||||
|
data: StringifiedAnnunciWithMedia;
|
||||||
};
|
};
|
||||||
|
|
||||||
const SchedaAnnuncioPage: NextPageWithLayout<PageProps> = ({
|
const SchedaAnnuncioPage: NextPageWithLayout<PageProps> = ({
|
||||||
id,
|
raw,
|
||||||
|
data,
|
||||||
}: PageProps) => {
|
}: PageProps) => {
|
||||||
const { data, isLoading } = api.annunci.getAnnuncioById_rawImgUrls.useQuery({
|
const annuncioData = StringifiedToAnnuncio(data);
|
||||||
id,
|
if (raw) {
|
||||||
});
|
return <SchedaAnnuncio data={annuncioData} raw />;
|
||||||
|
|
||||||
if (isLoading) {
|
|
||||||
return <LoadingPage />;
|
|
||||||
}
|
|
||||||
if (!data) {
|
|
||||||
return <Status500 />;
|
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Head>
|
<Head>
|
||||||
<title>Scheda Annuncio {data.codice}</title>
|
<title>Scheda Annuncio {annuncioData.codice}</title>
|
||||||
</Head>
|
</Head>
|
||||||
<div className="mx-auto w-full p-4">
|
<div className="mx-auto w-full p-4">
|
||||||
<SchedaAnnuncioStampabile data={data} />
|
<SchedaAnnuncioStampabile data={annuncioData} />
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
SchedaAnnuncioPage.getLayout = function getLayout(page) {
|
SchedaAnnuncioPage.getLayout = function getLayout(page) {
|
||||||
|
if (page.props.raw) {
|
||||||
|
return <>{page}</>;
|
||||||
|
}
|
||||||
return <AreaRiservataLayout noSidebar>{page}</AreaRiservataLayout>;
|
return <AreaRiservataLayout noSidebar>{page}</AreaRiservataLayout>;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -47,6 +46,7 @@ export default SchedaAnnuncioPage;
|
||||||
|
|
||||||
export const getServerSideProps = (async (context) => {
|
export const getServerSideProps = (async (context) => {
|
||||||
const id = context.params?.id;
|
const id = context.params?.id;
|
||||||
|
const raw = context.query?.raw;
|
||||||
|
|
||||||
if (typeof id !== "string") {
|
if (typeof id !== "string") {
|
||||||
console.error("Error: id is not a string");
|
console.error("Error: id is not a string");
|
||||||
|
|
@ -69,25 +69,58 @@ export const getServerSideProps = (async (context) => {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const access_token = context.req.cookies.access_token;
|
const helper = generateSSGHelper();
|
||||||
|
|
||||||
const helper = await TrpcAuthedFetchingIstance({ access_token });
|
|
||||||
if (helper) {
|
|
||||||
await helper.trpc.annunci.getAnnuncioById_rawImgUrls.prefetch({
|
|
||||||
id: parsed.data,
|
|
||||||
});
|
|
||||||
|
|
||||||
|
const data = await helper.annunci.getAnnuncioById_rawImgUrls.fetch({
|
||||||
|
id: parsed.data,
|
||||||
|
});
|
||||||
|
if (!data) {
|
||||||
return {
|
return {
|
||||||
props: {
|
notFound: true,
|
||||||
id: parsed.data,
|
|
||||||
trpcState: helper.trpc.dehydrate(),
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
redirect: {
|
props: {
|
||||||
destination: "/500",
|
raw: raw === "true",
|
||||||
permanent: false,
|
trpcState: helper.dehydrate(),
|
||||||
|
data : AnnuncioToStringified(data),
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}) satisfies GetServerSideProps<PageProps>;
|
}) satisfies GetServerSideProps<PageProps>;
|
||||||
|
|
||||||
|
type StringifiedAnnunciWithMedia = Omit<
|
||||||
|
AnnunciWithMedia,
|
||||||
|
"creato_il" | "disponibile_da" | "media_updated_at" | "modificato_il"
|
||||||
|
> & {
|
||||||
|
creato_il: string | undefined;
|
||||||
|
disponibile_da: string | undefined;
|
||||||
|
media_updated_at: string | undefined;
|
||||||
|
modificato_il: string | undefined;
|
||||||
|
};
|
||||||
|
|
||||||
|
function AnnuncioToStringified(
|
||||||
|
data: AnnunciWithMedia,
|
||||||
|
): StringifiedAnnunciWithMedia {
|
||||||
|
return {
|
||||||
|
...data,
|
||||||
|
creato_il: data?.creato_il?.toISOString(),
|
||||||
|
disponibile_da: data?.disponibile_da?.toISOString(),
|
||||||
|
media_updated_at: data?.media_updated_at?.toISOString(),
|
||||||
|
modificato_il: data?.modificato_il?.toISOString(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function StringifiedToAnnuncio(
|
||||||
|
data: StringifiedAnnunciWithMedia,
|
||||||
|
): AnnunciWithMedia {
|
||||||
|
return {
|
||||||
|
...data,
|
||||||
|
creato_il: data?.creato_il ? new Date(data.creato_il) : null,
|
||||||
|
disponibile_da: data?.disponibile_da ? new Date(data.disponibile_da) : null,
|
||||||
|
media_updated_at: data?.media_updated_at
|
||||||
|
? new Date(data.media_updated_at)
|
||||||
|
: null,
|
||||||
|
modificato_il: data?.modificato_il ? new Date(data.modificato_il) : null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -8,136 +8,193 @@ const STALE_KEY = "stale-resource";
|
||||||
export const apisProxy: ProxyFn = async (req: NextRequest) => {
|
export const apisProxy: ProxyFn = async (req: NextRequest) => {
|
||||||
const { pathname, searchParams } = req.nextUrl;
|
const { pathname, searchParams } = req.nextUrl;
|
||||||
|
|
||||||
if (!pathname.startsWith("/storage-api/")) {
|
// Handle Storage API requests
|
||||||
return;
|
if (pathname.startsWith("/storage-api/")) {
|
||||||
}
|
const params = new URLSearchParams(searchParams);
|
||||||
|
const isImageRequest = params.get("media") === "image";
|
||||||
|
const isVideoRequest = params.get("media") === "video";
|
||||||
|
|
||||||
const params = new URLSearchParams(searchParams);
|
const purpose = req.headers.get("purpose");
|
||||||
const isImageRequest = params.get("media") === "image";
|
const isNextImageRequest =
|
||||||
const isVideoRequest = params.get("media") === "video";
|
purpose === "prefetch" || req.headers.get("x-vercel-id");
|
||||||
|
|
||||||
// Check if this is a Next.js Image Optimization request
|
if (!isNextImageRequest && !isImageRequest && !isVideoRequest) {
|
||||||
const purpose = req.headers.get("purpose");
|
const accessToken = req.cookies.get(
|
||||||
const isNextImageRequest =
|
TOKEN_CONFIG.ACCESS_TOKEN_COOKIE_NAME,
|
||||||
purpose === "prefetch" || req.headers.get("x-vercel-id");
|
)?.value;
|
||||||
|
|
||||||
// For regular requests (not from Next Image Optimization), check auth
|
if (!accessToken) {
|
||||||
if (!isNextImageRequest && !isImageRequest && !isVideoRequest) {
|
console.log(
|
||||||
const accessToken = req.cookies.get(
|
"No access token found, referrer:",
|
||||||
TOKEN_CONFIG.ACCESS_TOKEN_COOKIE_NAME,
|
req.referrer,
|
||||||
)?.value;
|
" url:",
|
||||||
|
req.url,
|
||||||
if (!accessToken) {
|
);
|
||||||
console.log(
|
return new NextResponse("Unauthorized", { status: 401 });
|
||||||
"No access token found, referrer:",
|
|
||||||
req.referrer,
|
|
||||||
" url:",
|
|
||||||
req.url,
|
|
||||||
);
|
|
||||||
return new NextResponse("Unauthorized", { status: 401 });
|
|
||||||
}
|
|
||||||
|
|
||||||
const payload = await verifyToken(accessToken);
|
|
||||||
if (!payload) {
|
|
||||||
console.log("Invalid token, referrer:", req.referrer, " url:", req.url);
|
|
||||||
return new NextResponse("Unauthorized", { status: 401 });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Build the storage API URL correctly
|
|
||||||
const slug = pathname.replace("/storage-api/", "");
|
|
||||||
params.set("token", env.STORAGE_TOKEN);
|
|
||||||
const storageUrl = `${env.STORAGE_URL}/${slug}?${params.toString()}`;
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Forward the request to storage API
|
|
||||||
const headers = new Headers(req.headers);
|
|
||||||
headers.delete("host");
|
|
||||||
headers.delete("cookie");
|
|
||||||
|
|
||||||
// **CRITICAL: Preserve Range header for video streaming**
|
|
||||||
const rangeHeader = req.headers.get("range");
|
|
||||||
|
|
||||||
const response = await fetch(storageUrl, {
|
|
||||||
method: req.method,
|
|
||||||
headers: headers,
|
|
||||||
body:
|
|
||||||
req.method !== "GET" && req.method !== "HEAD" ? req.body : undefined,
|
|
||||||
// @ts-expect-error - duplex needed for request streaming
|
|
||||||
duplex: "half",
|
|
||||||
});
|
|
||||||
|
|
||||||
// Handle failed responses
|
|
||||||
if (!response.ok) {
|
|
||||||
// console.error(
|
|
||||||
// `Storage API error: ${response.status} for ${storageUrl}, referrer: ${req.referrer}, url: ${req.url}`,
|
|
||||||
// );
|
|
||||||
// 404 = File not found (likely stale URL after media update)
|
|
||||||
// 410 = Gone (explicitly removed)
|
|
||||||
const isStaleResource =
|
|
||||||
response.status === 404 || response.status === 410;
|
|
||||||
|
|
||||||
// Return fallback for image requests
|
|
||||||
if (isImageRequest || isNextImageRequest) {
|
|
||||||
const url = req.nextUrl.clone();
|
|
||||||
url.pathname = "/fallback-image.png";
|
|
||||||
url.search = "";
|
|
||||||
const response = NextResponse.rewrite(url);
|
|
||||||
if (isStaleResource) {
|
|
||||||
response.cookies.set(STALE_KEY, "1", {
|
|
||||||
maxAge: 5,
|
|
||||||
path: "/",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return response;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Return fallback for video requests
|
const payload = await verifyToken(accessToken);
|
||||||
|
if (!payload) {
|
||||||
|
console.log("Invalid token, referrer:", req.referrer, " url:", req.url);
|
||||||
|
return new NextResponse("Unauthorized", { status: 401 });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const slug = pathname.replace("/storage-api/", "");
|
||||||
|
params.set("token", env.STORAGE_TOKEN);
|
||||||
|
const storageUrl = `${env.STORAGE_URL}/${slug}?${params.toString()}`;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const headers = new Headers(req.headers);
|
||||||
|
headers.delete("host");
|
||||||
|
headers.delete("cookie");
|
||||||
|
const rangeHeader = req.headers.get("range");
|
||||||
|
|
||||||
|
const response = await fetch(storageUrl, {
|
||||||
|
method: req.method,
|
||||||
|
headers: headers,
|
||||||
|
body:
|
||||||
|
req.method !== "GET" && req.method !== "HEAD" ? req.body : undefined,
|
||||||
|
// @ts-expect-error - duplex needed for request streaming
|
||||||
|
duplex: "half",
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
const isStaleResource =
|
||||||
|
response.status === 404 || response.status === 410;
|
||||||
|
|
||||||
|
if (isImageRequest || isNextImageRequest) {
|
||||||
|
const url = req.nextUrl.clone();
|
||||||
|
url.pathname = "/fallback-image.png";
|
||||||
|
url.search = "";
|
||||||
|
const response = NextResponse.rewrite(url);
|
||||||
|
if (isStaleResource) {
|
||||||
|
response.cookies.set(STALE_KEY, "1", {
|
||||||
|
maxAge: 5,
|
||||||
|
path: "/",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isVideoRequest) {
|
||||||
|
const url = req.nextUrl.clone();
|
||||||
|
url.pathname = "/fallback-video.png";
|
||||||
|
url.search = "";
|
||||||
|
const response = NextResponse.rewrite(url);
|
||||||
|
if (isStaleResource) {
|
||||||
|
response.cookies.set(STALE_KEY, "1", {
|
||||||
|
maxAge: 5,
|
||||||
|
path: "/",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isStaleResource) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ message: "Stale resource, please reload." },
|
||||||
|
{ status: 410 },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return new NextResponse(`Storage API error: ${response.status}`, {
|
||||||
|
status: response.status,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (response.status === 204) {
|
||||||
|
return new NextResponse(null, { status: 200 });
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await response.arrayBuffer();
|
||||||
|
|
||||||
|
if (!data || data.byteLength === 0) {
|
||||||
|
console.error(
|
||||||
|
"Received empty response from storage API, referrer:",
|
||||||
|
req.referrer,
|
||||||
|
" url:",
|
||||||
|
req.url,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (isImageRequest || isNextImageRequest) {
|
||||||
|
const url = req.nextUrl.clone();
|
||||||
|
url.pathname = "/fallback-image.png";
|
||||||
|
url.search = "";
|
||||||
|
return NextResponse.rewrite(url);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isVideoRequest) {
|
||||||
|
const url = req.nextUrl.clone();
|
||||||
|
url.pathname = "/fallback-video.png";
|
||||||
|
url.search = "";
|
||||||
|
return NextResponse.rewrite(url);
|
||||||
|
}
|
||||||
|
|
||||||
|
return new NextResponse("Empty response", { status: 500 });
|
||||||
|
}
|
||||||
|
|
||||||
|
const status =
|
||||||
|
rangeHeader && response.status === 206 ? 206 : response.status;
|
||||||
|
|
||||||
|
const proxyResponse = new NextResponse(data, {
|
||||||
|
status: status,
|
||||||
|
statusText: response.statusText,
|
||||||
|
});
|
||||||
|
|
||||||
|
const contentType = response.headers.get("Content-Type");
|
||||||
|
const contentDisposition = response.headers.get("Content-Disposition");
|
||||||
|
const contentLength = response.headers.get("Content-Length");
|
||||||
|
const acceptRanges = response.headers.get("Accept-Ranges");
|
||||||
|
const contentRange = response.headers.get("Content-Range");
|
||||||
|
|
||||||
|
if (contentType) {
|
||||||
|
proxyResponse.headers.set("Content-Type", contentType);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (contentDisposition) {
|
||||||
|
proxyResponse.headers.set("Content-Disposition", contentDisposition);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (contentLength) {
|
||||||
|
proxyResponse.headers.set("Content-Length", contentLength);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (acceptRanges) {
|
||||||
|
proxyResponse.headers.set("Accept-Ranges", acceptRanges);
|
||||||
|
} else if (isVideoRequest) {
|
||||||
|
proxyResponse.headers.set("Accept-Ranges", "bytes");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (contentRange) {
|
||||||
|
proxyResponse.headers.set("Content-Range", contentRange);
|
||||||
|
}
|
||||||
|
|
||||||
|
proxyResponse.headers.set("Access-Control-Allow-Origin", "*");
|
||||||
|
|
||||||
if (isVideoRequest) {
|
if (isVideoRequest) {
|
||||||
const url = req.nextUrl.clone();
|
proxyResponse.headers.set(
|
||||||
url.pathname = "/fallback-video.png";
|
"Cache-Control",
|
||||||
url.search = "";
|
"public, max-age=31536000, immutable",
|
||||||
const response = NextResponse.rewrite(url);
|
);
|
||||||
if (isStaleResource) {
|
} else {
|
||||||
response.cookies.set(STALE_KEY, "1", {
|
proxyResponse.headers.set(
|
||||||
maxAge: 5,
|
"Cache-Control",
|
||||||
path: "/",
|
"public, max-age=31536000, immutable",
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return response;
|
|
||||||
}
|
|
||||||
|
|
||||||
// For non-media requests (e.g., documents), return JSON with reload hint
|
|
||||||
if (isStaleResource) {
|
|
||||||
return NextResponse.json(
|
|
||||||
{ message: "Stale resource, please reload." },
|
|
||||||
{ status: 410 },
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return new NextResponse(`Storage API error: ${response.status}`, {
|
|
||||||
status: response.status,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
//ignore if 204 no content, is the favicon request
|
proxyResponse.headers.delete("X-Frame-Options");
|
||||||
if (response.status === 204) {
|
proxyResponse.headers.set(
|
||||||
return new NextResponse(null, { status: 200 });
|
"Content-Security-Policy",
|
||||||
}
|
"frame-ancestors 'self'",
|
||||||
|
|
||||||
// Get response data
|
|
||||||
const data = await response.arrayBuffer();
|
|
||||||
|
|
||||||
// Check if we got valid data
|
|
||||||
if (!data || data.byteLength === 0) {
|
|
||||||
console.error(
|
|
||||||
"Received empty response from storage API, referrer:",
|
|
||||||
req.referrer,
|
|
||||||
" url:",
|
|
||||||
req.url,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
|
return proxyResponse;
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Storage proxy error:", error);
|
||||||
|
|
||||||
if (isImageRequest || isNextImageRequest) {
|
if (isImageRequest || isNextImageRequest) {
|
||||||
const url = req.nextUrl.clone();
|
const url = req.nextUrl.clone();
|
||||||
url.pathname = "/fallback-image.png";
|
url.pathname = "/fallback-image.png";
|
||||||
|
|
@ -152,90 +209,8 @@ export const apisProxy: ProxyFn = async (req: NextRequest) => {
|
||||||
return NextResponse.rewrite(url);
|
return NextResponse.rewrite(url);
|
||||||
}
|
}
|
||||||
|
|
||||||
return new NextResponse("Empty response", { status: 500 });
|
return new NextResponse("Failed to proxy request", { status: 500 });
|
||||||
}
|
}
|
||||||
|
|
||||||
// **IMPORTANT: Use correct status code for partial content**
|
|
||||||
const status =
|
|
||||||
rangeHeader && response.status === 206 ? 206 : response.status;
|
|
||||||
|
|
||||||
// Create response with proper headers
|
|
||||||
const proxyResponse = new NextResponse(data, {
|
|
||||||
status: status,
|
|
||||||
statusText: response.statusText,
|
|
||||||
});
|
|
||||||
|
|
||||||
// Copy relevant headers
|
|
||||||
const contentType = response.headers.get("Content-Type");
|
|
||||||
const contentDisposition = response.headers.get("Content-Disposition");
|
|
||||||
const contentLength = response.headers.get("Content-Length");
|
|
||||||
const acceptRanges = response.headers.get("Accept-Ranges");
|
|
||||||
const contentRange = response.headers.get("Content-Range");
|
|
||||||
|
|
||||||
if (contentType) {
|
|
||||||
proxyResponse.headers.set("Content-Type", contentType);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (contentDisposition) {
|
|
||||||
proxyResponse.headers.set("Content-Disposition", contentDisposition);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (contentLength) {
|
|
||||||
proxyResponse.headers.set("Content-Length", contentLength);
|
|
||||||
}
|
|
||||||
|
|
||||||
// **CRITICAL: Copy Range-related headers for video streaming**
|
|
||||||
if (acceptRanges) {
|
|
||||||
proxyResponse.headers.set("Accept-Ranges", acceptRanges);
|
|
||||||
} else if (isVideoRequest) {
|
|
||||||
proxyResponse.headers.set("Accept-Ranges", "bytes");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (contentRange) {
|
|
||||||
proxyResponse.headers.set("Content-Range", contentRange);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set CORS and caching headers
|
|
||||||
proxyResponse.headers.set("Access-Control-Allow-Origin", "*");
|
|
||||||
|
|
||||||
// Different cache strategy for videos
|
|
||||||
if (isVideoRequest) {
|
|
||||||
proxyResponse.headers.set(
|
|
||||||
"Cache-Control",
|
|
||||||
"public, max-age=31536000, immutable",
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
proxyResponse.headers.set(
|
|
||||||
"Cache-Control",
|
|
||||||
"public, max-age=31536000, immutable",
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
proxyResponse.headers.delete("X-Frame-Options");
|
|
||||||
proxyResponse.headers.set(
|
|
||||||
"Content-Security-Policy",
|
|
||||||
"frame-ancestors 'self'",
|
|
||||||
);
|
|
||||||
|
|
||||||
return proxyResponse;
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Storage proxy error:", error);
|
|
||||||
|
|
||||||
// Return fallback on error
|
|
||||||
if (isImageRequest || isNextImageRequest) {
|
|
||||||
const url = req.nextUrl.clone();
|
|
||||||
url.pathname = "/fallback-image.png";
|
|
||||||
url.search = "";
|
|
||||||
return NextResponse.rewrite(url);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isVideoRequest) {
|
|
||||||
const url = req.nextUrl.clone();
|
|
||||||
url.pathname = "/fallback-video.png";
|
|
||||||
url.search = "";
|
|
||||||
return NextResponse.rewrite(url);
|
|
||||||
}
|
|
||||||
|
|
||||||
return new NextResponse("Failed to proxy request", { status: 500 });
|
|
||||||
}
|
}
|
||||||
|
return;
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,9 @@ export const authProxy: ProxyFn = async (req: NextRequest) => {
|
||||||
)?.value;
|
)?.value;
|
||||||
|
|
||||||
const isProtectedRoute =
|
const isProtectedRoute =
|
||||||
(path.startsWith("/area-riservata") || path.startsWith("/servizio")) &&
|
(path.startsWith("/area-riservata") ||
|
||||||
|
path.startsWith("/servizio") ||
|
||||||
|
path.startsWith("/api/generate-pdf")) &&
|
||||||
!path.startsWith("/servizio/pre-onboard");
|
!path.startsWith("/servizio/pre-onboard");
|
||||||
|
|
||||||
if (isProtectedRoute) {
|
if (isProtectedRoute) {
|
||||||
|
|
|
||||||
|
|
@ -95,6 +95,17 @@ services:
|
||||||
resources:
|
resources:
|
||||||
limits:
|
limits:
|
||||||
cpus: "0.5"
|
cpus: "0.5"
|
||||||
|
|
||||||
|
browserless:
|
||||||
|
image: browserless/chrome:latest
|
||||||
|
networks:
|
||||||
|
- dokploy-network
|
||||||
|
expose:
|
||||||
|
- "3000"
|
||||||
|
environment:
|
||||||
|
- MAX_CONCURRENT_SESSIONS=5
|
||||||
|
- CONNECTION_TIMEOUT=30000
|
||||||
|
restart: always
|
||||||
|
|
||||||
backend:
|
backend:
|
||||||
image: "${IMAGE_REGISTRY:-ghcr.io}/${OWNER:-marcopedone}/${PREFIX:-infoalloggi}-backend:${TAG:-latest}"
|
image: "${IMAGE_REGISTRY:-ghcr.io}/${OWNER:-marcopedone}/${PREFIX:-infoalloggi}-backend:${TAG:-latest}"
|
||||||
|
|
@ -161,6 +172,7 @@ services:
|
||||||
SKEBBY_PASS: ${SKEBBY_PASS}
|
SKEBBY_PASS: ${SKEBBY_PASS}
|
||||||
STORAGE_URL: http://storage:8080
|
STORAGE_URL: http://storage:8080
|
||||||
STORAGE_TOKEN: ${STORAGE_TOKEN}
|
STORAGE_TOKEN: ${STORAGE_TOKEN}
|
||||||
|
BROWSER_WS_URL: "ws://browserless:3000"
|
||||||
networks:
|
networks:
|
||||||
- dokploy-network
|
- dokploy-network
|
||||||
ports:
|
ports:
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue