@@ -3,56 +3,54 @@ const multer = require('multer');
33const fs = require ( 'fs' ) ;
44const fetch = require ( 'node-fetch' ) ;
55const vision = require ( '@google-cloud/vision' ) ;
6- const router = express . Router ( ) ;
6+ const { verifyRecaptcha } = require ( '../helpers/verifyRecaptcha' ) ;
77
8+ const router = express . Router ( ) ;
89const upload = multer ( { dest : 'uploads/' } ) ;
910
1011const GEMINI_API_KEY = process . env . GEMINI_API_KEY ;
1112const RECAPTCHA_SECRET_KEY = process . env . RECAPTCHA_SECRET_KEY ;
1213
13- if ( ! GEMINI_API_KEY ) throw new Error ( 'Gemini API key is required' ) ;
14- if ( ! RECAPTCHA_SECRET_KEY ) throw new Error ( 'reCAPTCHA secret key is required' ) ;
14+ if ( ! GEMINI_API_KEY ) throw new Error ( 'Gemini API key is required. ' ) ;
15+ if ( ! RECAPTCHA_SECRET_KEY ) throw new Error ( 'reCAPTCHA secret key is required. ' ) ;
1516
1617const visionClient = new vision . ImageAnnotatorClient ( ) ;
1718
18- async function verifyRecaptcha ( token ) {
19- const res = await fetch ( 'https://www.google.com/recaptcha/api/siteverify' , {
20- method : 'POST' ,
21- headers : { 'Content-Type' : 'application/x-www-form-urlencoded' } ,
22- body : `secret=${ RECAPTCHA_SECRET_KEY } &response=${ token } ` ,
23- } ) ;
24- return res . json ( ) ;
25- }
26-
2719router . post ( '/' , upload . single ( 'image' ) , async ( req , res ) => {
2820 const prompt = req . body . prompt || 'Describe this image' ;
2921 const cleanPrompt = prompt . trim ( ) . replace ( / [ ^ a - z A - Z 0 - 9 ? . , ! " ( ) \- ] / g, '' ) ;
3022 const recaptchaToken = req . body . recaptchaToken ;
3123
3224 if ( ! recaptchaToken ) {
33- return res . status ( 400 ) . json ( { error : 'Missing reCAPTCHA token' } ) ;
25+ return res . status ( 400 ) . json ( { error : 'Missing reCAPTCHA token.' } ) ;
26+ }
27+
28+ if ( ! req . file ) {
29+ return res . status ( 400 ) . json ( { error : 'Image file is required.' } ) ;
3430 }
3531
32+ if ( ! cleanPrompt || cleanPrompt . length > 300 ) {
33+ return res . status ( 400 ) . json ( { error : 'Prompt must be 1–300 characters.' } ) ;
34+ }
35+
36+ // ✅ reCAPTCHA verification
3637 try {
3738 const recaptchaResult = await verifyRecaptcha ( recaptchaToken ) ;
38- if ( ! recaptchaResult . success || recaptchaResult . score < 0.5 ) {
39- console . warn ( '⚠️ reCAPTCHA fail' , {
39+
40+ const score = recaptchaResult . score ?? 0 ;
41+ const success = recaptchaResult . success === true ;
42+
43+ if ( ! success || score < 0.5 ) {
44+ console . warn ( '⚠️ reCAPTCHA verification failed' , {
4045 ip : req . ip ,
41- score : recaptchaResult . score ,
46+ score,
47+ success,
4248 } ) ;
43- return res . status ( 403 ) . json ( { error : 'reCAPTCHA verification failed' } ) ;
49+ return res . status ( 403 ) . json ( { error : 'reCAPTCHA verification failed. ' } ) ;
4450 }
4551 } catch ( err ) {
4652 console . error ( 'Error verifying reCAPTCHA:' , err ) ;
47- return res . status ( 500 ) . json ( { error : 'Failed to verify reCAPTCHA' } ) ;
48- }
49-
50- if ( ! req . file ) {
51- return res . status ( 400 ) . json ( { error : 'Image file is required' } ) ;
52- }
53-
54- if ( ! cleanPrompt || cleanPrompt . length > 300 ) {
55- return res . status ( 400 ) . json ( { error : 'Prompt must be 1–300 characters' } ) ;
53+ return res . status ( 500 ) . json ( { error : 'Failed to verify reCAPTCHA.' } ) ;
5654 }
5755
5856 const imagePath = req . file . path ;
@@ -61,6 +59,7 @@ router.post('/', upload.single('image'), async (req, res) => {
6159 // NSFW filtering using Cloud Vision SafeSearch
6260 const [ result ] = await visionClient . safeSearchDetection ( imagePath ) ;
6361 const safe = result . safeSearchAnnotation ;
62+
6463 if (
6564 safe . adult === 'LIKELY' ||
6665 safe . adult === 'VERY_LIKELY' ||
@@ -69,9 +68,7 @@ router.post('/', upload.single('image'), async (req, res) => {
6968 safe . racy === 'VERY_LIKELY'
7069 ) {
7170 console . warn ( 'Blocked NSFW image:' , safe ) ;
72- return res
73- . status ( 403 )
74- . json ( { error : 'Image flagged as unsafe by content filter.' } ) ;
71+ return res . status ( 403 ) . json ( { error : 'Image flagged as unsafe by content filter.' } ) ;
7572 }
7673
7774 const imageBuffer = fs . readFileSync ( imagePath ) ;
@@ -105,26 +102,25 @@ router.post('/', upload.single('image'), async (req, res) => {
105102 if ( ! geminiRes . ok ) {
106103 const errorData = await geminiRes . json ( ) ;
107104 console . error ( 'Gemini API error:' , errorData ) ;
108- return res
109- . status ( geminiRes . status )
110- . json ( { error : errorData . error || 'Unknown error from Gemini API' } ) ;
105+ return res . status ( geminiRes . status ) . json ( {
106+ error : errorData . error ?. message || 'Unknown error from Gemini API.' ,
107+ } ) ;
111108 }
112109
113110 const data = await geminiRes . json ( ) ;
114111 console . log ( 'Gemini API response:' , JSON . stringify ( data , null , 2 ) ) ;
115112
116113 const responseText = data . candidates ?. length
117- ? data . candidates [ 0 ] . content ?. parts ?. [ 0 ] ?. text ||
118- 'Response format unexpected'
119- : 'No candidates returned from Gemini' ;
114+ ? data . candidates [ 0 ] . content ?. parts ?. [ 0 ] ?. text || 'Response format unexpected.'
115+ : 'No candidates returned from Gemini.' ;
120116
121117 res . json ( { response : responseText } ) ;
122118 } catch ( err ) {
123- console . error ( err ) ;
124- res . status ( 500 ) . json ( { error : 'Error analyzing image' } ) ;
119+ console . error ( 'Error analyzing image:' , err ) ;
120+ res . status ( 500 ) . json ( { error : 'Error analyzing image. ' } ) ;
125121 } finally {
126122 if ( fs . existsSync ( imagePath ) ) {
127- fs . unlinkSync ( imagePath ) ; // cleanup
123+ fs . unlinkSync ( imagePath ) ; // Cleanup temp file
128124 }
129125 }
130126} ) ;
0 commit comments