From 76c797498e7dc3976a69d19283c67f7c2cef0056 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Samy=20Pess=C3=A9?= Date: Tue, 4 Mar 2025 13:52:02 +0100 Subject: [PATCH] Implement route to invalidate data cache (#2916) --- .changeset/lemon-pugs-smell.md | 5 +++ .../src/app/~gitbook/revalidate/route.ts | 33 +++++++++++++++ packages/gitbook-v2/src/lib/env.ts | 5 +++ packages/gitbook-v2/src/lib/routes.ts | 41 +++++++++++++++++++ 4 files changed, 84 insertions(+) create mode 100644 .changeset/lemon-pugs-smell.md create mode 100644 packages/gitbook-v2/src/app/~gitbook/revalidate/route.ts create mode 100644 packages/gitbook-v2/src/lib/routes.ts diff --git a/.changeset/lemon-pugs-smell.md b/.changeset/lemon-pugs-smell.md new file mode 100644 index 000000000..955784c9b --- /dev/null +++ b/.changeset/lemon-pugs-smell.md @@ -0,0 +1,5 @@ +--- +'gitbook-v2': minor +--- + +Add route to revalidate cached data diff --git a/packages/gitbook-v2/src/app/~gitbook/revalidate/route.ts b/packages/gitbook-v2/src/app/~gitbook/revalidate/route.ts new file mode 100644 index 000000000..8aa54dbce --- /dev/null +++ b/packages/gitbook-v2/src/app/~gitbook/revalidate/route.ts @@ -0,0 +1,33 @@ +import { type NextRequest, NextResponse } from 'next/server'; + +import { withVerifySignature } from '@v2/lib/routes'; +import { revalidateTag } from 'next/cache'; + +interface JsonBody { + tags: string[]; +} + +/** + * Revalidate cached data based on tags. + * The body should be a JSON with { tags: string[] } + */ +export async function POST(req: NextRequest) { + return withVerifySignature(req, async (body) => { + if (!body.tags || !Array.isArray(body.tags)) { + return NextResponse.json( + { + error: 'tags must be an array', + }, + { status: 400 } + ); + } + + body.tags.forEach((tag) => { + revalidateTag(tag); + }); + + return NextResponse.json({ + success: true, + }); + }); +} diff --git a/packages/gitbook-v2/src/lib/env.ts b/packages/gitbook-v2/src/lib/env.ts index 28daff70e..1982a49b7 100644 --- a/packages/gitbook-v2/src/lib/env.ts +++ b/packages/gitbook-v2/src/lib/env.ts @@ -58,3 +58,8 @@ export const GITBOOK_INTEGRATIONS_HOST = export const GITBOOK_IMAGE_RESIZE_URL = process.env.GITBOOK_IMAGE_RESIZE_URL ?? null; export const GITBOOK_IMAGE_RESIZE_SIGNING_KEY = process.env.GITBOOK_IMAGE_RESIZE_SIGNING_KEY ?? null; + +/** + * Secret used to validate requests from the GitBook app. + */ +export const GITBOOK_SECRET = process.env.GITBOOK_SECRET ?? null; diff --git a/packages/gitbook-v2/src/lib/routes.ts b/packages/gitbook-v2/src/lib/routes.ts new file mode 100644 index 000000000..798d940a8 --- /dev/null +++ b/packages/gitbook-v2/src/lib/routes.ts @@ -0,0 +1,41 @@ +import crypto from 'node:crypto'; +import { NextResponse } from 'next/server'; +import { GITBOOK_SECRET } from './env'; + +/** + * Verify the signature of the request and call the function with the body. + */ +export async function withVerifySignature( + request: Request, + fn: (body: T) => Promise +) { + try { + const rawBody = await request.text(); + const body = JSON.parse(rawBody) as T; + + if (GITBOOK_SECRET) { + // Retrieve the signature header from the request + const incomingSignature = request.headers.get('x-gitbook-signature'); + if (!incomingSignature) { + return NextResponse.json({ error: 'Missing signature header' }, { status: 400 }); + } + + const computedSignature = crypto + .createHmac('sha256', GITBOOK_SECRET) + .update(rawBody) + .digest('hex'); + + // Compare incoming signature to computed signature + if (incomingSignature !== computedSignature) { + return NextResponse.json({ error: 'Invalid signature' }, { status: 401 }); + } + } + + return await fn(body); + } catch (_error) { + return NextResponse.json( + { error: 'Invalid request or unable to parse JSON' }, + { status: 400 } + ); + } +}