2026-03-04 16:39:27 +01:00
|
|
|
import { TRPCError } from "@trpc/server";
|
2026-03-04 17:34:15 +01:00
|
|
|
import type Redis from "ioredis";
|
2026-03-04 19:12:01 +01:00
|
|
|
import puppeteer, { type PDFOptions } from "puppeteer-core";
|
2026-03-04 16:39:27 +01:00
|
|
|
import { env } from "~/env";
|
|
|
|
|
import type { AnnunciId } from "~/schemas/public/Annunci";
|
2026-03-04 19:12:01 +01:00
|
|
|
import type { OrdiniOrdineId } from "~/schemas/public/Ordini";
|
|
|
|
|
import type { TestiEStringheStingaId } from "~/schemas/public/TestiEStringhe";
|
2026-03-04 16:39:27 +01:00
|
|
|
import { getKeydbClient } from "~/utils/keydb";
|
|
|
|
|
import { TOKEN_CONFIG } from "../auth/configs";
|
|
|
|
|
|
2026-03-04 17:34:15 +01:00
|
|
|
export const SCHEDA_ANNUNCIO_CACHE_PREFIX = "scheda-annuncio-";
|
2026-03-04 19:12:37 +01:00
|
|
|
const CONDIZIONI_CACHE_PREFIX = "condizioni-";
|
2026-03-04 17:34:15 +01:00
|
|
|
export const invalidateCache = async (cacheKey: string) => {
|
|
|
|
|
const keybd = getKeydbClient();
|
|
|
|
|
if (keybd) {
|
|
|
|
|
await keybd.del(cacheKey);
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
async function deleteByPrefix(redis: Redis, prefix: string) {
|
|
|
|
|
// Create a stream to find keys matching the pattern
|
|
|
|
|
const stream = redis.scanStream({
|
|
|
|
|
match: `${prefix}*`,
|
|
|
|
|
count: 100, // Process 100 keys per iteration
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
stream.on("data", async (keys: string[]) => {
|
|
|
|
|
if (keys.length > 0) {
|
|
|
|
|
// Create a pipeline to delete keys in a single network round-trip
|
|
|
|
|
const pipeline = redis.pipeline();
|
|
|
|
|
|
|
|
|
|
// Use UNLINK instead of DEL for better performance on large keys
|
|
|
|
|
keys.forEach((key) => pipeline.unlink(key));
|
|
|
|
|
|
|
|
|
|
await pipeline.exec();
|
|
|
|
|
console.log(`Deleted ${keys.length} keys`);
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
stream.on("error", (err) => {
|
|
|
|
|
console.error(
|
|
|
|
|
`Error scanning/unlinking keys with prefix ${prefix}: ${err.message}`,
|
|
|
|
|
);
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export const invalidateAllCaches = async () => {
|
|
|
|
|
const keybd = getKeydbClient();
|
|
|
|
|
if (keybd) {
|
|
|
|
|
await deleteByPrefix(keybd, SCHEDA_ANNUNCIO_CACHE_PREFIX);
|
|
|
|
|
await deleteByPrefix(keybd, CONDIZIONI_CACHE_PREFIX);
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
export const getCacheStats = async () => {
|
|
|
|
|
const keybd = getKeydbClient();
|
|
|
|
|
if (keybd) {
|
|
|
|
|
const schedaKeys = await keybd.keys(`${SCHEDA_ANNUNCIO_CACHE_PREFIX}*`);
|
|
|
|
|
const condizioniKeys = await keybd.keys(`${CONDIZIONI_CACHE_PREFIX}*`);
|
|
|
|
|
return {
|
|
|
|
|
success: true,
|
|
|
|
|
schedaCount: schedaKeys.length,
|
|
|
|
|
condizioniCount: condizioniKeys.length,
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
return {
|
|
|
|
|
success: false,
|
|
|
|
|
schedaCount: 0,
|
|
|
|
|
condizioniCount: 0,
|
|
|
|
|
};
|
|
|
|
|
};
|
2026-03-04 16:39:27 +01:00
|
|
|
export const genPdfSchedaAnnuncioBase64 = async (annuncioId: AnnunciId) => {
|
|
|
|
|
const pdf = await genPdfSchedaAnnuncio(annuncioId);
|
|
|
|
|
const pdfBase64 = Buffer.from(pdf).toString("base64");
|
|
|
|
|
const base64Content = pdfBase64.includes("base64,")
|
|
|
|
|
? pdfBase64.split("base64,")[1]
|
|
|
|
|
: pdfBase64;
|
|
|
|
|
if (!base64Content) {
|
|
|
|
|
throw new Error("Failed to convert PDF to base64");
|
|
|
|
|
}
|
|
|
|
|
return base64Content;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
export const genPdfSchedaAnnuncio = async (annuncioId: AnnunciId) => {
|
|
|
|
|
const keybd = getKeydbClient();
|
2026-03-08 01:02:57 +01:00
|
|
|
const url = `/area-riservata/scheda-annuncio-stampa/${annuncioId}?raw=true`;
|
2026-03-04 19:12:01 +01:00
|
|
|
const options: PDFOptions = { printBackground: true };
|
2026-03-04 16:39:27 +01:00
|
|
|
if (keybd) {
|
2026-03-04 17:34:15 +01:00
|
|
|
const cacheKey = `${SCHEDA_ANNUNCIO_CACHE_PREFIX}${annuncioId}`;
|
2026-03-04 16:39:27 +01:00
|
|
|
const cached = await keybd.get(cacheKey);
|
|
|
|
|
if (cached) {
|
|
|
|
|
return new Uint8Array(Buffer.from(cached, "base64"));
|
|
|
|
|
}
|
2026-03-04 19:12:01 +01:00
|
|
|
const scheda = await puppeteerGenPdf({
|
|
|
|
|
url,
|
|
|
|
|
options,
|
|
|
|
|
});
|
2026-03-04 16:39:27 +01:00
|
|
|
await keybd.set(
|
|
|
|
|
cacheKey,
|
|
|
|
|
Buffer.from(scheda).toString("base64"),
|
|
|
|
|
"EX",
|
|
|
|
|
3600 * 5,
|
|
|
|
|
);
|
|
|
|
|
return scheda;
|
|
|
|
|
}
|
2026-03-04 19:12:01 +01:00
|
|
|
return await puppeteerGenPdf({ url, options });
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
export const genPdfCondizioniBase64 = async (
|
|
|
|
|
condizioniId: TestiEStringheStingaId,
|
|
|
|
|
) => {
|
|
|
|
|
const pdf = await genPdfCondizioni(condizioniId);
|
|
|
|
|
const pdfBase64 = Buffer.from(pdf).toString("base64");
|
|
|
|
|
const base64Content = pdfBase64.includes("base64,")
|
|
|
|
|
? pdfBase64.split("base64,")[1]
|
|
|
|
|
: pdfBase64;
|
|
|
|
|
if (!base64Content) {
|
|
|
|
|
throw new Error("Failed to convert PDF to base64");
|
|
|
|
|
}
|
|
|
|
|
return base64Content;
|
2026-03-04 17:34:15 +01:00
|
|
|
};
|
|
|
|
|
|
2026-03-04 19:12:37 +01:00
|
|
|
const genPdfCondizioni = async (condizioniId: TestiEStringheStingaId) => {
|
2026-03-04 17:34:15 +01:00
|
|
|
const keybd = getKeydbClient();
|
|
|
|
|
const url = `/servizio/condizioni/${condizioniId}?raw=true`;
|
|
|
|
|
if (keybd) {
|
|
|
|
|
const cacheKey = `${CONDIZIONI_CACHE_PREFIX}${condizioniId}`;
|
|
|
|
|
const cached = await keybd.get(cacheKey);
|
|
|
|
|
if (cached) {
|
|
|
|
|
return new Uint8Array(Buffer.from(cached, "base64"));
|
|
|
|
|
}
|
2026-03-04 19:12:01 +01:00
|
|
|
const scheda = await puppeteerGenPdf({ url });
|
2026-03-04 17:34:15 +01:00
|
|
|
await keybd.set(
|
|
|
|
|
cacheKey,
|
|
|
|
|
Buffer.from(scheda).toString("base64"),
|
|
|
|
|
"EX",
|
|
|
|
|
3600 * 24 * 5,
|
|
|
|
|
);
|
|
|
|
|
return scheda;
|
|
|
|
|
}
|
2026-03-04 19:12:01 +01:00
|
|
|
return await puppeteerGenPdf({ url });
|
|
|
|
|
};
|
|
|
|
|
|
2026-03-08 01:02:57 +01:00
|
|
|
export const genPdfRicevutaCortesia = async (ordineId: OrdiniOrdineId) => {
|
|
|
|
|
const url = `/servizio/ricevuta-cortesia/${ordineId}?raw=true`;
|
|
|
|
|
return await puppeteerGenPdf({ url });
|
|
|
|
|
};
|
|
|
|
|
|
2026-03-04 19:12:01 +01:00
|
|
|
export const genPdfRicevutaCortesiaBase64 = async (
|
|
|
|
|
ordineId: OrdiniOrdineId,
|
|
|
|
|
) => {
|
2026-03-08 01:02:57 +01:00
|
|
|
const pdf = await genPdfRicevutaCortesia(ordineId);
|
2026-03-04 19:12:01 +01:00
|
|
|
const pdfBase64 = Buffer.from(pdf).toString("base64");
|
|
|
|
|
const base64Content = pdfBase64.includes("base64,")
|
|
|
|
|
? pdfBase64.split("base64,")[1]
|
|
|
|
|
: pdfBase64;
|
|
|
|
|
if (!base64Content) {
|
|
|
|
|
throw new Error("Failed to convert PDF to base64");
|
|
|
|
|
}
|
|
|
|
|
return base64Content;
|
2026-03-04 16:39:27 +01:00
|
|
|
};
|
|
|
|
|
|
2026-03-04 19:12:01 +01:00
|
|
|
const puppeteerGenPdf = async ({
|
|
|
|
|
url,
|
|
|
|
|
options,
|
|
|
|
|
}: {
|
|
|
|
|
url: string;
|
|
|
|
|
options?: PDFOptions;
|
|
|
|
|
}) => {
|
2026-03-04 16:39:27 +01:00
|
|
|
let browser = null;
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
browser = await puppeteer.connect({
|
2026-03-10 13:16:32 +01:00
|
|
|
browserWSEndpoint: `${env.BROWSER_WS_URL}`,
|
2026-03-04 16:39:27 +01:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const page = await browser.newPage();
|
|
|
|
|
|
2026-03-10 12:36:11 +01:00
|
|
|
const c = await browser.cookies();
|
|
|
|
|
if (
|
|
|
|
|
c.length === 0 ||
|
|
|
|
|
!c.some(
|
|
|
|
|
(cookie) => cookie.name === TOKEN_CONFIG.OVERRIDE_TOKEN_COOKIE_NAME,
|
|
|
|
|
)
|
|
|
|
|
) {
|
|
|
|
|
const productionDomain = new URL(env.INTERNAL_BASE_URL).hostname;
|
|
|
|
|
const newCookies = [
|
|
|
|
|
{
|
|
|
|
|
name: TOKEN_CONFIG.OVERRIDE_TOKEN_COOKIE_NAME,
|
|
|
|
|
value: env.EXP_API_PASS,
|
|
|
|
|
domain:
|
|
|
|
|
env.NODE_ENV === "production"
|
|
|
|
|
? productionDomain
|
|
|
|
|
: "host.docker.internal",
|
|
|
|
|
path: "/",
|
|
|
|
|
},
|
|
|
|
|
];
|
|
|
|
|
await browser.setCookie(...newCookies);
|
|
|
|
|
}
|
2026-03-04 16:39:27 +01:00
|
|
|
|
|
|
|
|
const baseUrl =
|
|
|
|
|
env.NODE_ENV === "production"
|
|
|
|
|
? env.INTERNAL_BASE_URL
|
|
|
|
|
: `http://host.docker.internal:3000`;
|
|
|
|
|
|
2026-03-04 17:34:15 +01:00
|
|
|
const targetUrl = `${baseUrl}${url}`;
|
2026-03-10 09:56:33 +01:00
|
|
|
try {
|
|
|
|
|
await page.goto(targetUrl, { waitUntil: "networkidle2", timeout: 60000 });
|
|
|
|
|
} catch (e) {
|
|
|
|
|
if ((e as Error).message.includes("detached")) {
|
|
|
|
|
await page.goto(targetUrl, { waitUntil: "load", timeout: 60000 });
|
|
|
|
|
} else throw e;
|
|
|
|
|
}
|
2026-03-04 16:39:27 +01:00
|
|
|
|
|
|
|
|
const pdf = await page.pdf({
|
|
|
|
|
format: "A4",
|
2026-03-04 19:12:01 +01:00
|
|
|
...options,
|
2026-03-04 16:39:27 +01:00
|
|
|
});
|
|
|
|
|
return pdf;
|
|
|
|
|
} catch (error) {
|
|
|
|
|
throw new TRPCError({
|
|
|
|
|
code: "INTERNAL_SERVER_ERROR",
|
2026-03-10 09:56:33 +01:00
|
|
|
message: `Errore nella generazione del PDF, URL: ${url}, ${(error as Error).message}`,
|
2026-03-04 16:39:27 +01:00
|
|
|
});
|
|
|
|
|
} finally {
|
|
|
|
|
// 'disconnect' tells browserless you're done with this tab
|
|
|
|
|
if (browser) await browser.disconnect();
|
|
|
|
|
}
|
|
|
|
|
};
|