mirror of
https://github.com/ershisan99/www.git
synced 2025-12-17 12:34:17 +00:00
add webhook handler
This commit is contained in:
109
src/app/api/neatqueue-webhook/route.ts
Normal file
109
src/app/api/neatqueue-webhook/route.ts
Normal file
@@ -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 }
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -17,6 +17,7 @@ export const env = createEnv({
|
|||||||
DISCORD_BOT_TOKEN: z.string(),
|
DISCORD_BOT_TOKEN: z.string(),
|
||||||
DATABASE_URL: z.string().url(),
|
DATABASE_URL: z.string().url(),
|
||||||
REDIS_URL: z.string().url(),
|
REDIS_URL: z.string().url(),
|
||||||
|
WEBHOOK_QUERY_SECRET: z.string(),
|
||||||
NODE_ENV: z
|
NODE_ENV: z
|
||||||
.enum(['development', 'test', 'production'])
|
.enum(['development', 'test', 'production'])
|
||||||
.default('development'),
|
.default('development'),
|
||||||
@@ -44,6 +45,7 @@ export const env = createEnv({
|
|||||||
REDIS_URL: process.env.REDIS_URL,
|
REDIS_URL: process.env.REDIS_URL,
|
||||||
NODE_ENV: process.env.NODE_ENV,
|
NODE_ENV: process.env.NODE_ENV,
|
||||||
CRON_SECRET: process.env.CRON_SECRET,
|
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
|
* Run `build` or `dev` with `SKIP_ENV_VALIDATION` to skip env validation. This is especially
|
||||||
|
|||||||
Reference in New Issue
Block a user