From 727f1e760c4b51567132ad65aa3fd14cc3a0d373 Mon Sep 17 00:00:00 2001 From: Marco Pedone Date: Fri, 17 Oct 2025 19:14:27 +0200 Subject: [PATCH] feat: add revalidation functionality and integrate it into various components and API routes --- apps/infoalloggi/.github/workflows/ci.yml | 1 + apps/infoalloggi/Dockerfile | 3 + apps/infoalloggi/src/env.mjs | 2 + .../src/forms/FormEditAnnuncio.tsx | 147 +++++++++++------- apps/infoalloggi/src/pages/annuncio/[cod].tsx | 2 +- .../infoalloggi/src/pages/api/revalidation.ts | 31 ++++ .../pages/area-riservata/admin/annunci.tsx | 79 +++++++--- .../src/server/api/routers/settings.ts | 109 ++++++++++--- .../server/controllers/annunci.controller.ts | 7 +- .../src/server/utils/revalidationHelper.ts | 56 +++++++ 10 files changed, 331 insertions(+), 106 deletions(-) create mode 100644 apps/infoalloggi/src/pages/api/revalidation.ts create mode 100644 apps/infoalloggi/src/server/utils/revalidationHelper.ts diff --git a/apps/infoalloggi/.github/workflows/ci.yml b/apps/infoalloggi/.github/workflows/ci.yml index 57a5120..e9b2d49 100644 --- a/apps/infoalloggi/.github/workflows/ci.yml +++ b/apps/infoalloggi/.github/workflows/ci.yml @@ -28,6 +28,7 @@ env: SKIP_REDIS: "false" SKEBBY_USER: "test" SKEBBY_PASS : "test" + REVALIDATION_SECRET: "test" jobs: build: runs-on: ubuntu-latest diff --git a/apps/infoalloggi/Dockerfile b/apps/infoalloggi/Dockerfile index fed08f8..2afbd31 100644 --- a/apps/infoalloggi/Dockerfile +++ b/apps/infoalloggi/Dockerfile @@ -63,6 +63,7 @@ ENV KEYDB_URL="localhost:6380" ENV TILES_URL="localhost:6379" ENV SKEBBY_USER="mock" ENV SKEBBY_PASS="mock" +ENV REVALIDATION_SECRET="mock" RUN SKIP_ENV_VALIDATION=1 npm run build @@ -128,6 +129,8 @@ ARG SKEBBY_USER ENV SKEBBY_USER=$SKEBBY_USER ARG SKEBBY_PASS ENV SKEBBY_PASS=$SKEBBY_PASS +ENV REVALIDATION_SECRET="SWKpgaaLfsyeqV1eOT0WG7TUFBewir8kJXure3O37ki8Lt4Z4IiEgBW4zPHDdM5c" + RUN addgroup -g 1001 -S nodejs RUN adduser -S nextjs -u 1001 diff --git a/apps/infoalloggi/src/env.mjs b/apps/infoalloggi/src/env.mjs index d2e8a4f..5ad3c91 100644 --- a/apps/infoalloggi/src/env.mjs +++ b/apps/infoalloggi/src/env.mjs @@ -30,6 +30,7 @@ const server = z.object({ TILES_URL: z.string(), SKEBBY_USER: z.string(), SKEBBY_PASS: z.string(), + REVALIDATION_SECRET: z.string(), }); /** @@ -73,6 +74,7 @@ const processEnv = { TILES_URL: process.env.TILES_URL, SKEBBY_USER: process.env.SKEBBY_USER, SKEBBY_PASS: process.env.SKEBBY_PASS, + REVALIDATION_SECRET: process.env.REVALIDATION_SECRET, }; // Don't touch the part below diff --git a/apps/infoalloggi/src/forms/FormEditAnnuncio.tsx b/apps/infoalloggi/src/forms/FormEditAnnuncio.tsx index 8eef78e..8e582ff 100644 --- a/apps/infoalloggi/src/forms/FormEditAnnuncio.tsx +++ b/apps/infoalloggi/src/forms/FormEditAnnuncio.tsx @@ -2,8 +2,10 @@ import { format } from "date-fns"; import { it } from "date-fns/locale"; import { CalendarIcon, + CodeSquare, Eye, Printer, + RefreshCcw, RotateCw, TriangleAlert, } from "lucide-react"; @@ -133,15 +135,21 @@ export const AnnuncioEditForm = ({ data }: { data: Annunci }) => { const { mutateAsync: revalidate } = api.settings.revalidateAnnuncio.useMutation({ onSettled: async () => { - toast.success( - "La revalidazione dell'annuncio è stata richiesta con successo,\n potrebbero volerci alcuni minuti prima che le modifiche siano visibili.", - { - duration: 5000, - }, - ); + toast.success("Revalidazione completata"); }, }); + const { mutateAsync: update } = api.settings.updateAnnuncio.useMutation({ + onSettled: async () => { + toast.success( + "L'aggiornamento dell'annuncio è stata richiesta con successo,\n potrebbero volerci alcuni minuti prima che le modifiche siano visibili.", + { + duration: 5000, + }, + ); + }, + }); + const defaultValues: FormValues = { ...data, email: data.email || undefined, @@ -214,57 +222,84 @@ export const AnnuncioEditForm = ({ data }: { data: Annunci }) => { onSubmit={form.handleSubmit(onSubmit)} >
-
-

- Annuncio {data.codice} -

- - - +
+
+ +
+ + + + - pubblica - - - - - - { - await revalidate({ cod: data.codice }); - }} - title="Revalidazione Annuncio" - > - - -
- - + + + + + + + + + { + await update({ cod: data.codice }); + }} + title="Aggiornamento Annuncio" + > + + + +
diff --git a/apps/infoalloggi/src/pages/annuncio/[cod].tsx b/apps/infoalloggi/src/pages/annuncio/[cod].tsx index 2d70299..346a7ef 100644 --- a/apps/infoalloggi/src/pages/annuncio/[cod].tsx +++ b/apps/infoalloggi/src/pages/annuncio/[cod].tsx @@ -236,7 +236,7 @@ export async function getStaticProps( meta, trpcState: ssg.dehydrate(), }, - revalidate: 1, + revalidate: 21600, // 6 hours - ensures fresh data 4x per day }; } diff --git a/apps/infoalloggi/src/pages/api/revalidation.ts b/apps/infoalloggi/src/pages/api/revalidation.ts new file mode 100644 index 0000000..e39a5bb --- /dev/null +++ b/apps/infoalloggi/src/pages/api/revalidation.ts @@ -0,0 +1,31 @@ +import type { NextApiRequest, NextApiResponse } from "next"; +import { env } from "~/env.mjs"; + +export default async function handler( + req: NextApiRequest, + res: NextApiResponse, +) { + if (req.query.secret !== env.REVALIDATION_SECRET) { + return res.status(401).json({ message: "Invalid token" }); + } + const path = req.query.path as string; + + if (!path) { + return res.status(400).json({ message: "Missing path parameter" }); + } + + try { + await res.revalidate(`${path}`); + + return res.json({ + success: true, + message: `Page ${path} revalidated successfully`, + }); + } catch (err) { + console.error("Error revalidating:", err); + return res.status(500).json({ + success: false, + message: err instanceof Error ? err.message : "Unknown error", + }); + } +} diff --git a/apps/infoalloggi/src/pages/area-riservata/admin/annunci.tsx b/apps/infoalloggi/src/pages/area-riservata/admin/annunci.tsx index 44bd1bf..3b6c8ec 100644 --- a/apps/infoalloggi/src/pages/area-riservata/admin/annunci.tsx +++ b/apps/infoalloggi/src/pages/area-riservata/admin/annunci.tsx @@ -1,4 +1,4 @@ -import { RefreshCcw, TriangleAlert } from "lucide-react"; +import { CodeSquare, RefreshCcw, TriangleAlert } from "lucide-react"; import toast from "react-hot-toast"; import { Confirm } from "~/components/confirm"; import { AreaRiservataLayout } from "~/components/Layout"; @@ -6,6 +6,11 @@ import { LoadingPage } from "~/components/loading"; import { Status500 } from "~/components/status-page"; import { AnnunciTable } from "~/components/tables/annunci-tables"; import { Button } from "~/components/ui/button"; +import { + Popover, + PopoverContent, + PopoverTrigger, +} from "~/components/ui/popover"; import type { NextPageWithLayout } from "~/pages/_app"; import { generateSSGHelper } from "~/server/utils/ssgHelper"; import { api } from "~/utils/api"; @@ -13,15 +18,20 @@ import { api } from "~/utils/api"; const Admin_Annunci: NextPageWithLayout = () => { const { data, isLoading, refetch } = api.annunci.getAnnunciList.useQuery(); + const { mutateAsync: update } = api.settings.updateAllAnnunci.useMutation({ + onSettled: async () => { + toast.success( + "La revalidazione dell'annuncio è stata richiesta con successo,\n potrebbero volerci alcuni minuti prima che le modifiche siano visibili.", + { + duration: 5000, + }, + ); + }, + }); const { mutateAsync: revalidate } = api.settings.revalidateAllAnnunci.useMutation({ onSettled: async () => { - toast.success( - "La revalidazione dell'annuncio è stata richiesta con successo,\n potrebbero volerci alcuni minuti prima che le modifiche siano visibili.", - { - duration: 5000, - }, - ); + toast.success("Revalidazione completata"); }, }); if (isLoading) return ; @@ -39,22 +49,45 @@ const Admin_Annunci: NextPageWithLayout = () => { > - { - await revalidate(); - }} - title="Rivalida tutti gli annunci" - > - - + + + + + + + { + await update(); + }} + title="Aggiornamento Annunci" + > + + + +
diff --git a/apps/infoalloggi/src/server/api/routers/settings.ts b/apps/infoalloggi/src/server/api/routers/settings.ts index 1f5db66..2ae7a43 100644 --- a/apps/infoalloggi/src/server/api/routers/settings.ts +++ b/apps/infoalloggi/src/server/api/routers/settings.ts @@ -7,6 +7,7 @@ import { createTRPCRouter, publicProcedure, } from "~/server/api/trpc"; +import { getCodici_AnnunciHandler } from "~/server/controllers/annunci.controller"; import { addEtichetta, getAllEtichette, @@ -36,6 +37,10 @@ import { newStringa, updateStringhe, } from "~/server/services/testi_stringhe.service"; +import { + revalidate, + revalidateMultiple, +} from "~/server/utils/revalidationHelper"; import { zEtichettaId, zFlagsId, @@ -250,22 +255,20 @@ export const settingsRouter = createTRPCRouter({ stringaValue: input.stringaValue, }); }), - revalidateAllAnnunci: adminProcedure.mutation(async () => { try { - const response = await fetch(`${env.BACKENDSERVER_URL}/update`); - - if (!response.ok) { - const errorData = await response.text().catch(() => "Unknown error"); - throw new Error( - `Revalidation failed with status ${response.status}: ${errorData}`, - ); + const annunciOnline = await getCodici_AnnunciHandler(); + const { success, failed } = await revalidateMultiple( + annunciOnline.map((cod) => `/annuncio/${cod}`), + ); + console.log(`Successfully revalidated pages: ${success.join(", ")}`); + if (failed.length > 0) { + failed.forEach(({ path, error }) => { + console.error(`Failed to revalidate ${path}: ${error}`); + }); } - - const data = await response.json().catch(() => ({})); return { status: "success", - details: data, }; } catch (err) { console.error("Revalidation error:", err); @@ -284,21 +287,9 @@ export const settingsRouter = createTRPCRouter({ ) .mutation(async ({ input }) => { try { - const response = await fetch( - `${env.BACKENDSERVER_URL}/update-cod/${input.cod}`, - ); - - if (!response.ok) { - const errorData = await response.text().catch(() => "Unknown error"); - throw new Error( - `Revalidation failed with status ${response.status}: ${errorData}`, - ); - } - - const data = await response.json().catch(() => ({})); + await revalidate(`/annuncio/${input.cod}`); return { status: "success", - details: data, }; } catch (err) { console.error("Revalidation error:", err); @@ -309,4 +300,74 @@ export const settingsRouter = createTRPCRouter({ }); } }), + + updateAllAnnunci: adminProcedure.mutation(async () => { + try { + const response = await fetch(`${env.BACKENDSERVER_URL}/update`); + + if (!response.ok) { + const errorData = await response.text().catch(() => "Unknown error"); + throw new Error( + `Update failed with status ${response.status}: ${errorData}`, + ); + } + + const data = await response.json().catch(() => ({})); + const annunciOnline = await getCodici_AnnunciHandler(); + const { success, failed } = await revalidateMultiple( + annunciOnline.map((cod) => `/annuncio/${cod}`), + ); + console.log(`Successfully revalidated pages: ${success.join(", ")}`); + if (failed.length > 0) { + failed.forEach(({ path, error }) => { + console.error(`Failed to revalidate ${path}: ${error}`); + }); + } + return { + status: "success", + details: data, + }; + } catch (err) { + console.error("Update error:", err); + throw new TRPCError({ + code: "INTERNAL_SERVER_ERROR", + message: err instanceof Error ? err.message : "Update failed", + cause: err, + }); + } + }), + updateAnnuncio: adminProcedure + .input( + z.object({ + cod: z.string(), + }), + ) + .mutation(async ({ input }) => { + try { + const response = await fetch( + `${env.BACKENDSERVER_URL}/update-cod/${input.cod}`, + ); + + if (!response.ok) { + const errorData = await response.text().catch(() => "Unknown error"); + throw new Error( + `Update failed with status ${response.status}: ${errorData}`, + ); + } + + const data = await response.json().catch(() => ({})); + await revalidate(`/annuncio/${input.cod}`); + return { + status: "success", + details: data, + }; + } catch (err) { + console.error("Update error:", err); + throw new TRPCError({ + code: "INTERNAL_SERVER_ERROR", + message: err instanceof Error ? err.message : "Update failed", + cause: err, + }); + } + }), }); diff --git a/apps/infoalloggi/src/server/controllers/annunci.controller.ts b/apps/infoalloggi/src/server/controllers/annunci.controller.ts index b160234..38628d4 100644 --- a/apps/infoalloggi/src/server/controllers/annunci.controller.ts +++ b/apps/infoalloggi/src/server/controllers/annunci.controller.ts @@ -9,6 +9,7 @@ import type { ServizioServizioId } from "~/schemas/public/Servizio"; import { db } from "~/server/db"; import { AnnuncioObjectWithImages } from "~/server/services/annunci.service"; import { createSrcset } from "~/server/services/imageServer"; +import { revalidate } from "../utils/revalidationHelper"; // const ratelimit = new RateLimiterHandler({ // windowSize: 10, @@ -371,13 +372,15 @@ export const editAnnuncioHandler = async ({ data: AnnunciUpdate; }) => { try { - await db + const annuncio = await db .updateTable("annunci") .set({ ...data, }) .where("id", "=", annuncioId) - .execute(); + .returning("codice") + .executeTakeFirstOrThrow(); + await revalidate(`/annuncio/${annuncio.codice}`); return true; } catch (e) { throw new TRPCError({ diff --git a/apps/infoalloggi/src/server/utils/revalidationHelper.ts b/apps/infoalloggi/src/server/utils/revalidationHelper.ts new file mode 100644 index 0000000..8cd0a79 --- /dev/null +++ b/apps/infoalloggi/src/server/utils/revalidationHelper.ts @@ -0,0 +1,56 @@ +export const revalidate = async (path: string) => { + try { + const res = await fetch( + `${process.env.NEXT_PUBLIC_BASE_URL}/api/revalidation?secret=${process.env.REVALIDATION_SECRET}&path=${encodeURIComponent(path)}`, + { + method: "GET", + }, + ); + if (!res.ok) { + const errorData = (await res.json()) as { + message: string; + success: boolean; + }; + throw new Error( + `Failed to revalidate path ${path}: ${errorData.message}`, + ); + } + } catch (error) { + console.error(`Error revalidating path ${path}:`, error); + throw error; + } +}; + +export const revalidateMultiple = async ( + paths: string[], +): Promise<{ + success: string[]; + failed: Array<{ path: string; error: string }>; +}> => { + const results = await Promise.allSettled( + paths.map((path) => revalidate(path)), + ); + + const success: string[] = []; + const failed: Array<{ path: string; error: string }> = []; + + results.forEach((result, index) => { + const path = paths[index]; + if (!path) { + return; + } + if (result.status === "fulfilled") { + success.push(path); + } else { + failed.push({ + path, + error: + result.reason instanceof Error + ? result.reason.message + : "Unknown error", + }); + } + }); + + return { success, failed }; +};