+ {data.open_contatti_at != null &&
}
{data.user_confirmed_at != null &&
}
{data.accettato_conferma_at != null && (
<>
@@ -122,6 +124,46 @@ export const AdminActions = () => {
);
};
+const AdminSendScheda = () => {
+ const { userId } = useServizio();
+ const { annuncioId } = useServizioAnnuncio();
+
+ const { mutate } = api.comunicazioni.sendSchedaAnnuncioServizio.useMutation({
+ onMutate: () => {
+ toast.loading("Invio email in corso...", {
+ id: "send-scheda-annuncio-email",
+ });
+ },
+ onSuccess: () => {
+ toast.success("Email inviata con successo!", {
+ id: "send-scheda-annuncio-email",
+ });
+ },
+ onError: (error) => {
+ toast.error(`Errore durante l'invio dell'email: ${error.message}`, {
+ id: "send-scheda-annuncio-email",
+ });
+ },
+ });
+
+ return (
+
+ );
+};
+
export const UserActions = () => {
const { servizioId } = useServizio();
const { data, annuncioId } = useServizioAnnuncio();
diff --git a/apps/infoalloggi/src/pages/api/generate-pdf.ts b/apps/infoalloggi/src/pages/api/generate-pdf.ts
deleted file mode 100644
index bfc2d6a..0000000
--- a/apps/infoalloggi/src/pages/api/generate-pdf.ts
+++ /dev/null
@@ -1,54 +0,0 @@
-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();
- }
-}
diff --git a/apps/infoalloggi/src/proxies/auth.ts b/apps/infoalloggi/src/proxies/auth.ts
index 3966545..d7803bb 100644
--- a/apps/infoalloggi/src/proxies/auth.ts
+++ b/apps/infoalloggi/src/proxies/auth.ts
@@ -1,5 +1,6 @@
import { add } from "date-fns";
import { type NextRequest, NextResponse } from "next/server";
+import { env } from "~/env";
import type { ProxyFn } from "~/proxy";
import { TOKEN_CONFIG } from "~/server/auth/configs";
@@ -8,6 +9,15 @@ import { signToken, verifyToken } from "~/server/auth/jwt";
export const authProxy: ProxyFn = async (req: NextRequest) => {
const path = req.nextUrl.pathname;
+ // permette a puppeteer di accedere alla pagina di generazione PDF bypassando l'autenticazione normale
+ if (path.startsWith("/area-riservata/admin/scheda-annuncio-stampa/")) {
+ const overrideToken = req.cookies.get(
+ TOKEN_CONFIG.OVERRIDE_TOKEN_COOKIE_NAME,
+ )?.value;
+ if (overrideToken && overrideToken === env.EXP_API_PASS) {
+ return;
+ }
+ }
const refreshToken = req.cookies.get(
TOKEN_CONFIG.REFRESH_TOKEN_COOKIE_NAME,
)?.value;
@@ -17,9 +27,7 @@ export const authProxy: ProxyFn = async (req: NextRequest) => {
)?.value;
const isProtectedRoute =
- (path.startsWith("/area-riservata") ||
- path.startsWith("/servizio") ||
- path.startsWith("/api/generate-pdf")) &&
+ (path.startsWith("/area-riservata") || path.startsWith("/servizio")) &&
!path.startsWith("/servizio/pre-onboard");
if (isProtectedRoute) {
diff --git a/apps/infoalloggi/src/server/api/routers/annunci.ts b/apps/infoalloggi/src/server/api/routers/annunci.ts
index d07e596..416828d 100644
--- a/apps/infoalloggi/src/server/api/routers/annunci.ts
+++ b/apps/infoalloggi/src/server/api/routers/annunci.ts
@@ -1,3 +1,4 @@
+import { TRPCError } from "@trpc/server";
import { z } from "zod/v4";
import type { AnnunciUpdate } from "~/schemas/public/Annunci";
import {
@@ -25,7 +26,9 @@ import {
} from "~/server/controllers/annunci.controller";
import { db } from "~/server/db";
import { NewMail } from "~/server/services/mailer";
+import { genPdfSchedaAnnuncio } from "~/server/services/puppeteer.service";
import { zAnnuncioId, zServizioId } from "~/server/utils/zod_types";
+import { getKeydbClient } from "~/utils/keydb";
export const annunciRouter = createTRPCRouter({
editAnnuncio: adminProcedure
@@ -33,6 +36,10 @@ export const annunciRouter = createTRPCRouter({
z.object({ annuncioId: zAnnuncioId, data: z.custom
() }),
)
.mutation(async ({ input }) => {
+ const keybd = getKeydbClient();
+ if (keybd) {
+ await keybd.del(`scheda-annuncio-${input.annuncioId}`);
+ }
return await editAnnuncioHandler({
...input,
});
@@ -152,4 +159,20 @@ export const annunciRouter = createTRPCRouter({
...input,
});
}),
+ getSchedaAnnuncio: adminProcedure
+ .input(
+ z.object({
+ id: zAnnuncioId,
+ }),
+ )
+ .mutation(async ({ input }) => {
+ try {
+ return await genPdfSchedaAnnuncio(input.id);
+ } catch (e) {
+ throw new TRPCError({
+ code: "INTERNAL_SERVER_ERROR",
+ message: `Errore nella generazione della scheda: ${(e as Error).message}`,
+ });
+ }
+ }),
});
diff --git a/apps/infoalloggi/src/server/api/routers/comunicazioni.ts b/apps/infoalloggi/src/server/api/routers/comunicazioni.ts
index b4734ea..c84c67f 100644
--- a/apps/infoalloggi/src/server/api/routers/comunicazioni.ts
+++ b/apps/infoalloggi/src/server/api/routers/comunicazioni.ts
@@ -12,6 +12,7 @@ import {
getEmails,
} from "~/server/services/email.service";
import { NewMail } from "~/server/services/mailer";
+import { genPdfSchedaAnnuncioBase64 } from "~/server/services/puppeteer.service";
import { zAnnuncioId, zUserId } from "~/server/utils/zod_types";
export const comunicazioniRouter = createTRPCRouter({
@@ -54,7 +55,7 @@ export const comunicazioniRouter = createTRPCRouter({
sendSchedaAnnuncioEmail: adminProcedure
.input(
z.object({
- pdf: z.string(),
+ id: zAnnuncioId,
codice: z.string(),
numero: z.string(),
nominativo: z.string(),
@@ -64,9 +65,7 @@ export const comunicazioniRouter = createTRPCRouter({
)
.mutation(async ({ input }) => {
try {
- const base64Content = input.pdf.includes("base64,")
- ? input.pdf.split("base64,")[1]
- : input.pdf;
+ const scheda = await genPdfSchedaAnnuncioBase64(input.id);
await NewMail({
template: {
@@ -83,7 +82,7 @@ export const comunicazioniRouter = createTRPCRouter({
attachments: [
{
filename: `scheda annuncio ${input.codice}.pdf`,
- content: base64Content,
+ content: scheda,
encoding: "base64",
contentType: "application/pdf",
},
@@ -110,6 +109,64 @@ export const comunicazioniRouter = createTRPCRouter({
);
}
}),
+ sendSchedaAnnuncioServizio: adminProcedure
+ .input(
+ z.object({
+ userId: zUserId,
+ annuncioId: zAnnuncioId,
+ }),
+ )
+ .mutation(async ({ input }) => {
+ try {
+ const scheda = await genPdfSchedaAnnuncioBase64(input.annuncioId);
+ const user = await db
+ .selectFrom("users")
+ .select("email")
+ .where("id", "=", input.userId)
+ .executeTakeFirstOrThrow();
+ const annuncio = await db
+ .selectFrom("annunci")
+ .select([
+ "codice",
+ "locatore",
+ "indirizzo",
+ "civico",
+ "comune",
+ "cap",
+ "provincia",
+ "numero",
+ ])
+ .where("id", "=", input.annuncioId)
+ .executeTakeFirstOrThrow();
+ await NewMail({
+ template: {
+ mailType: "emailContattiConScheda",
+ props: {
+ codice: annuncio.codice,
+ nominativo: annuncio.locatore || "",
+ telefono: annuncio.numero || "",
+ indirizzo: `${annuncio.indirizzo || ""}, ${annuncio.civico || ""} ${annuncio.comune || ""} ${annuncio.cap || ""} (${annuncio.provincia || ""})`,
+ },
+ },
+ mail: {
+ subject: `Contatti Annuncio ${annuncio.codice} - Infoalloggi.it`,
+ to: user.email,
+ attachments: [
+ {
+ filename: `scheda annuncio ${annuncio.codice}.pdf`,
+ content: scheda,
+ encoding: "base64",
+ contentType: "application/pdf",
+ },
+ ],
+ },
+ });
+ } catch (e) {
+ throw new Error(
+ `Errore nell'invio dell'email: ${(e as Error).message}`,
+ );
+ }
+ }),
sendAnnunciEmail: adminProcedure
.input(
z.object({
diff --git a/apps/infoalloggi/src/server/auth/configs.ts b/apps/infoalloggi/src/server/auth/configs.ts
index 2f33d68..2b3a199 100644
--- a/apps/infoalloggi/src/server/auth/configs.ts
+++ b/apps/infoalloggi/src/server/auth/configs.ts
@@ -1,6 +1,7 @@
export const TOKEN_CONFIG = {
ACCESS_TOKEN_COOKIE_NAME: "access_token",
REFRESH_TOKEN_COOKIE_NAME: "refresh_token",
+ OVERRIDE_TOKEN_COOKIE_NAME: "override_token",
ACCESS_TOKEN_EXPIRY: "1h",
REFRESH_TOKEN_EXPIRY: "7d",
ACCESS_TOKEN_COOKIE_EXPIRY: 1,
diff --git a/apps/infoalloggi/src/server/controllers/servizio.controller.ts b/apps/infoalloggi/src/server/controllers/servizio.controller.ts
index a6a33e1..ef18709 100644
--- a/apps/infoalloggi/src/server/controllers/servizio.controller.ts
+++ b/apps/infoalloggi/src/server/controllers/servizio.controller.ts
@@ -53,6 +53,7 @@ import {
} from "~/utils/kysely-helper";
import { GetUserInterests } from "../services/interests.service";
import { getPrezziarioByIdHandler } from "../services/prezziario.service";
+import { genPdfSchedaAnnuncioBase64 } from "../services/puppeteer.service";
import type { AnnuncioRicerca } from "./annunci.controller";
export const addServizio = async (data: NewServizio) => {
@@ -1308,20 +1309,25 @@ export const SbloccaContatti = async ({
const EmailInteressato: NewMailProps = {
template: {
- mailType: "generic",
+ mailType: "emailContattiConScheda",
props: {
- link: {
- href: `${env.BASE_URL}/area-riservata/dashboard#${servizioId}-${annuncioId}`,
- label: "Vai al servizio",
- },
- noreply: true,
- testo: `I contatti per l'annuncio ${data.annuncio.codice} sono stati sbloccati con successo.`,
- title: "Contatti sbloccati per il servizio di ricerca affitto",
+ nominativo: data.annuncio.locatore || "",
+ telefono: data.annuncio.numero || "",
+ indirizzo: data.annuncio.indirizzo || "",
+ codice: data.annuncio.codice,
},
},
mail: {
- subject: "Contatti sbloccati per il servizio di ricerca affitto",
+ subject: `Contatti Annuncio ${data.annuncio.codice} - InfoAlloggi`,
to: data.user.email,
+ attachments: [
+ {
+ filename: `scheda annuncio ${data.annuncio.codice}.pdf`,
+ content: await genPdfSchedaAnnuncioBase64(data.annuncioId),
+ encoding: "base64",
+ contentType: "application/pdf",
+ },
+ ],
},
userId: data.user.id,
};
diff --git a/apps/infoalloggi/src/server/services/mailer.ts b/apps/infoalloggi/src/server/services/mailer.ts
index cbd746b..e52505f 100644
--- a/apps/infoalloggi/src/server/services/mailer.ts
+++ b/apps/infoalloggi/src/server/services/mailer.ts
@@ -27,6 +27,8 @@ import RegistrazioneAvvenuta from "emails/registrazione-avvenuta";
import EmailSchedaContatto, {
type SchedaContatto_NewMail,
} from "emails/scheda-annuncio";
+import type { ContattiConScheda_NewMail } from "emails/scheda-contatti";
+import EmailContattiConScheda from "emails/scheda-contatti";
import ShareAnnunci, { type ShareAnnunci_NewMail } from "emails/share-annunci";
import VerificaEmail, {
type VerificaEmail_NewMail,
@@ -90,7 +92,8 @@ export type MailsTemplates =
| ShareAnnunci_NewMail
| Generic_NewMail
| SchedaContatto_NewMail
- | Invito_NewMail;
+ | Invito_NewMail
+ | ContattiConScheda_NewMail;
export const genMail = async ({ ...mail }: MailsTemplates) => {
let mailComponent: JSX.Element | null = null;
@@ -182,6 +185,12 @@ export const genMail = async ({ ...mail }: MailsTemplates) => {
});
title = "Invito al sito";
break;
+ case "emailContattiConScheda":
+ mailComponent = EmailContattiConScheda({
+ ...mail.props,
+ });
+ title = "Contatti Con Scheda";
+ break;
default:
throw new TRPCError({
code: "INTERNAL_SERVER_ERROR",
diff --git a/apps/infoalloggi/src/server/services/puppeteer.service.ts b/apps/infoalloggi/src/server/services/puppeteer.service.ts
new file mode 100644
index 0000000..7834172
--- /dev/null
+++ b/apps/infoalloggi/src/server/services/puppeteer.service.ts
@@ -0,0 +1,86 @@
+import { TRPCError } from "@trpc/server";
+import puppeteer from "puppeteer-core";
+import { env } from "~/env";
+import type { AnnunciId } from "~/schemas/public/Annunci";
+import { getKeydbClient } from "~/utils/keydb";
+import { TOKEN_CONFIG } from "../auth/configs";
+
+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();
+ if (keybd) {
+ const cacheKey = `scheda-annuncio-${annuncioId}`;
+ const cached = await keybd.get(cacheKey);
+ if (cached) {
+ return new Uint8Array(Buffer.from(cached, "base64"));
+ }
+ const scheda = await puppeteerSchedaAnnuncio(annuncioId);
+ await keybd.set(
+ cacheKey,
+ Buffer.from(scheda).toString("base64"),
+ "EX",
+ 3600 * 5,
+ );
+ return scheda;
+ }
+ return await puppeteerSchedaAnnuncio(annuncioId);
+};
+
+const puppeteerSchedaAnnuncio = async (annuncioId: AnnunciId) => {
+ let browser = null;
+
+ try {
+ browser = await puppeteer.connect({
+ browserWSEndpoint: env.BROWSER_WS_URL,
+ });
+
+ const page = await browser.newPage();
+
+ 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);
+
+ const baseUrl =
+ env.NODE_ENV === "production"
+ ? env.INTERNAL_BASE_URL
+ : `http://host.docker.internal:3000`;
+
+ const targetUrl = `${baseUrl}/area-riservata/admin/scheda-annuncio-stampa/${annuncioId}?raw=true`;
+ await page.goto(targetUrl, { waitUntil: "networkidle0" });
+
+ const pdf = await page.pdf({
+ format: "A4",
+ printBackground: true,
+ });
+ return pdf;
+ } catch (error) {
+ throw new TRPCError({
+ code: "INTERNAL_SERVER_ERROR",
+ message: `Errore nella generazione del PDF: ${(error as Error).message}`,
+ });
+ } finally {
+ // 'disconnect' tells browserless you're done with this tab
+ if (browser) await browser.disconnect();
+ }
+};