@@ -5,16 +5,24 @@ import * as Sentry from "@sentry/nextjs";
55
66import { getGlobalPayload } from "@/lib/payload" ;
77import { downloadFile } from "@/utils/files" ;
8- import type {
9- Promise as PayloadPromise ,
10- } from "@/payload-types" ;
8+ import type { Promise as PayloadPromise } from "@/payload-types" ;
119import {
1210 buildMeedanIdCandidates ,
1311 normaliseString ,
1412 type MeedanWebhookPayload ,
1513} from "./utils" ;
1614
1715const WEBHOOK_SECRET_ENV_KEY = "WEBHOOK_SECRET_KEY" ;
16+ const DEFAULT_MAX_IMAGE_BYTES =
17+ Number ( process . env . MEEDAN_MAX_IMAGE_BYTES ) || 10 * 1024 * 1024 ;
18+ const ALLOWED_IMAGE_MIME_TYPES = [
19+ "image/jpeg" ,
20+ "image/jpg" ,
21+ "image/png" ,
22+ "image/gif" ,
23+ "image/webp" ,
24+ "image/svg+xml" ,
25+ ] ;
1826
1927type WithOptionalId = {
2028 id ?: unknown ;
@@ -63,7 +71,7 @@ export const POST = async (request: NextRequest) => {
6371 Sentry . captureMessage ( message , "error" ) ;
6472 return NextResponse . json (
6573 { ok : false , updated : false , error : "Service misconfigured" } ,
66- { status : 200 }
74+ { status : 500 } ,
6775 ) ;
6876 }
6977
@@ -84,10 +92,6 @@ export const POST = async (request: NextRequest) => {
8492 Sentry . captureMessage ( message , "error" ) ;
8593 return NextResponse . json ( { error : "Invalid JSON" } , { status : 400 } ) ;
8694 }
87- Sentry . captureMessage (
88- `meedan-sync:: Received webhook payload ${ JSON . stringify ( parsed ) } ` ,
89- "info"
90- ) ;
9195 const annotationType = parsed ?. object ?. annotation_type ?. trim ( ) ;
9296 if ( annotationType && annotationType !== "report_design" ) {
9397 return NextResponse . json ( { ok : true , skipped : true } , { status : 200 } ) ;
@@ -103,7 +107,7 @@ export const POST = async (request: NextRequest) => {
103107 {
104108 error : "Missing Meedan ID" ,
105109 } ,
106- { status : 400 }
110+ { status : 400 } ,
107111 ) ;
108112 }
109113
@@ -119,10 +123,19 @@ export const POST = async (request: NextRequest) => {
119123 const url = normaliseString ( options ?. published_article_url ) ;
120124 const headline = normaliseString ( options ?. headline ) ;
121125 const fieldValue = normaliseString (
122- annotationData ?. fields ?. [ 0 ] ?. value ?? null
126+ annotationData ?. fields ?. [ 0 ] ?. value ?? null ,
123127 ) ;
124128 const imageUrl = normaliseString ( parsed ?. object ?. file ?. [ 0 ] ?. url ?? null ) ;
125129
130+ Sentry . withScope ( ( scope ) => {
131+ scope . setTag ( "route" , "meedan-sync" ) ;
132+ scope . setContext ( "webhook" , {
133+ annotationType,
134+ meedanId,
135+ } ) ;
136+ Sentry . captureMessage ( "meedan-sync:: Received webhook" , "info" ) ;
137+ } ) ;
138+
126139 try {
127140 const payload = await getGlobalPayload ( ) ;
128141
@@ -145,10 +158,7 @@ export const POST = async (request: NextRequest) => {
145158
146159 console . error ( message ) ;
147160 Sentry . captureMessage ( message , "error" ) ;
148- return NextResponse . json (
149- { error : "Promise not found" } ,
150- { status : 404 }
151- ) ;
161+ return NextResponse . json ( { error : "Promise not found" } , { status : 404 } ) ;
152162 }
153163
154164 const updateData : Partial < PayloadPromise > = { } ;
@@ -176,13 +186,16 @@ export const POST = async (request: NextRequest) => {
176186 Sentry . captureMessage ( message , "error" ) ;
177187 return NextResponse . json (
178188 { error : "Failed to persist promise" } ,
179- { status : 500 }
189+ { status : 500 } ,
180190 ) ;
181191 }
182192
183193 if ( ! imageUrl ) {
184194 return NextResponse . json ( { ok : true , created, updated } , { status : 200 } ) ;
185195 }
196+ if ( ! imageUrl . startsWith ( "https://" ) ) {
197+ return NextResponse . json ( { error : "Invalid image URL" } , { status : 400 } ) ;
198+ }
186199
187200 const fallbackAlt =
188201 headline ??
@@ -196,7 +209,10 @@ export const POST = async (request: NextRequest) => {
196209 let filePath : string | null = null ;
197210
198211 try {
199- filePath = await downloadFile ( imageUrl ) ;
212+ filePath = await downloadFile ( imageUrl , {
213+ allowedMimeTypes : ALLOWED_IMAGE_MIME_TYPES ,
214+ maxBytes : DEFAULT_MAX_IMAGE_BYTES ,
215+ } ) ;
200216
201217 const existingImageId = getRelationId ( promise . image ) ;
202218 let mediaId = existingImageId ;
@@ -233,12 +249,12 @@ export const POST = async (request: NextRequest) => {
233249 } ) ;
234250 Sentry . captureMessage (
235251 "meedan-sync:: Failed to cache image after processing webhook" ,
236- "error"
252+ "error" ,
237253 ) ;
238254 } ) ;
239255 return NextResponse . json (
240256 { error : "Failed to cache image" } ,
241- { status : 500 }
257+ { status : 500 } ,
242258 ) ;
243259 }
244260
@@ -251,18 +267,15 @@ export const POST = async (request: NextRequest) => {
251267 } ) ;
252268
253269 updated = true ;
254- return NextResponse . json (
255- { ok : true , created, updated } ,
256- { status : 200 }
257- ) ;
270+ return NextResponse . json ( { ok : true , created, updated } , { status : 200 } ) ;
258271 } finally {
259272 if ( filePath ) {
260273 try {
261274 await unlink ( filePath ) ;
262275 } catch ( cleanupError ) {
263276 console . warn (
264277 "meedan-sync:: Failed to clean up temp image" ,
265- cleanupError
278+ cleanupError ,
266279 ) ;
267280
268281 Sentry . withScope ( ( scope ) => {
@@ -289,7 +302,7 @@ export const POST = async (request: NextRequest) => {
289302 } ) ;
290303 return NextResponse . json (
291304 { ok : false , updated : false , error : "Failed to process webhook" } ,
292- { status : 200 }
305+ { status : 500 } ,
293306 ) ;
294307 }
295308} ;
0 commit comments