From c3276e77596ae738cbc5e77cd62d0cecb9492bc7 Mon Sep 17 00:00:00 2001 From: Andres Date: Tue, 22 Apr 2025 14:13:46 +0200 Subject: [PATCH] add webhook handler --- src/app/api/neatqueue-webhook/route.ts | 109 +++++++++++++++++++++++++ src/env.js | 2 + 2 files changed, 111 insertions(+) create mode 100644 src/app/api/neatqueue-webhook/route.ts diff --git a/src/app/api/neatqueue-webhook/route.ts b/src/app/api/neatqueue-webhook/route.ts new file mode 100644 index 0000000..c896a2d --- /dev/null +++ b/src/app/api/neatqueue-webhook/route.ts @@ -0,0 +1,109 @@ +import crypto from 'node:crypto' +import { type NextRequest, NextResponse } from 'next/server' + +const EXPECTED_QUERY_SECRET = process.env.WEBHOOK_QUERY_SECRET +const QUERY_PARAM_NAME = 'token' + +/** + * Verifies the secret from the query parameter. + */ +function verifyQuerySecret(req: NextRequest): boolean { + if (!EXPECTED_QUERY_SECRET) { + console.error( + 'Webhook query secret is not configured in environment variables.' + ) + return false + } + + const providedSecret = req.nextUrl.searchParams.get(QUERY_PARAM_NAME) + + if (!providedSecret) { + console.warn(`Query parameter "${QUERY_PARAM_NAME}" missing.`) + return false + } + + const expectedBuffer = Buffer.from(EXPECTED_QUERY_SECRET, 'utf8') + const providedBuffer = Buffer.from(providedSecret, 'utf8') + + if ( + expectedBuffer.length !== providedBuffer.length || + !crypto.timingSafeEqual(expectedBuffer, providedBuffer) + ) { + console.warn('Invalid query secret provided.') + return false + } + + console.log('Query secret verified successfully.') + return true +} + +/** + * Handles POST requests to the /api/webhooks endpoint. + * Verifies query secret, logs payload, and handles actions. + */ +export async function POST(req: NextRequest) { + let payload: any + + try { + const isVerified = verifyQuerySecret(req) + if (!isVerified) { + console.log('Webhook verification failed (query secret).') + return NextResponse.json( + { message: 'Unauthorized: Invalid or missing secret' }, + { status: 401 } + ) + } + + payload = await req.json() + + console.log( + '--- Verified Webhook Received (Query Auth) ---', + new Date().toISOString(), + '---\n', + JSON.stringify(payload, null, 2), + '\n--- End Webhook ---' + ) + + console.log( + `Action: ${payload?.action || 'Unknown'}. Sending generic success response.` + ) + return NextResponse.json( + { message: 'Webhook received successfully' }, + { status: 200 } + ) + } catch (error: any) { + console.error('!!! Error processing webhook:', error) + try { + // Attempt to read body on error + const errorBody = await req.clone().text() + console.error('Raw request body on error:', errorBody) + } catch (bodyError) { + console.error('Could not read raw request body on error:', bodyError) + } + + if (error instanceof SyntaxError) { + return NextResponse.json( + { message: 'Invalid JSON payload' }, + { status: 400 } + ) + } + if (error.message.includes('Webhook query secret is not configured')) { + return NextResponse.json( + { message: 'Internal Server Error: Webhook secret not configured' }, + { status: 500 } + ) + } + + return NextResponse.json( + { message: 'Internal Server Error processing webhook' }, + { status: 500 } + ) + } +} + +export async function GET(req: NextRequest) { + return NextResponse.json( + { message: 'Method Not Allowed. Please use POST.' }, + { status: 405 } + ) +} diff --git a/src/env.js b/src/env.js index 5305fd4..fec5f67 100644 --- a/src/env.js +++ b/src/env.js @@ -17,6 +17,7 @@ export const env = createEnv({ DISCORD_BOT_TOKEN: z.string(), DATABASE_URL: z.string().url(), REDIS_URL: z.string().url(), + WEBHOOK_QUERY_SECRET: z.string(), NODE_ENV: z .enum(['development', 'test', 'production']) .default('development'), @@ -44,6 +45,7 @@ export const env = createEnv({ REDIS_URL: process.env.REDIS_URL, NODE_ENV: process.env.NODE_ENV, CRON_SECRET: process.env.CRON_SECRET, + WEBHOOK_QUERY_SECRET: process.env.WEBHOOK_QUERY_SECRET, }, /** * Run `build` or `dev` with `SKIP_ENV_VALIDATION` to skip env validation. This is especially