← back to Handbag Auth Nextjs
src/app/api/analyze/image/route.ts
184 lines
import { NextRequest, NextResponse } from 'next/server'
import { GoogleGenerativeAI } from '@google/generative-ai'
import { imageAnalysisSchema, validateInput } from '@/lib/validation'
import { rateLimit, getClientIp } from '@/lib/rateLimit'
import { addSecurityHeaders } from '@/lib/securityHeaders'
import { securityLogger, SecurityEventType } from '@/lib/security-logger'
import type { ImageAnalysisResult } from '@/types/database'
export async function POST(request: NextRequest) {
const clientIp = getClientIp(request)
// Apply strict rate limiting for AI endpoints
const rateLimitResult = rateLimit(clientIp, {
windowMs: 60 * 1000,
maxRequests: 10, // Only 10 image analyses per minute
})
if (!rateLimitResult.allowed) {
securityLogger.log({
type: SecurityEventType.RATE_LIMIT_EXCEEDED,
ip: clientIp,
endpoint: '/api/analyze/image',
method: 'POST',
details: { remaining: rateLimitResult.remaining },
severity: 'medium',
})
const response = NextResponse.json(
{ success: false, error: 'Rate limit exceeded. Please try again later.' },
{ status: 429 }
)
response.headers.set('Retry-After', String(Math.ceil((rateLimitResult.resetTime - Date.now()) / 1000)))
addSecurityHeaders(response)
return response
}
try {
const body = await request.json()
// Validate input
const validation = validateInput(imageAnalysisSchema, body)
if (!validation.success) {
return NextResponse.json(
{ success: false, error: 'Invalid input', details: validation.errors.format() },
{ status: 400 }
)
}
const { image, files } = validation.data
const apiKey = process.env.GEMINI_API_KEY || process.env.GOOGLE_API_KEY
if (!apiKey) {
return NextResponse.json(
{ success: false, error: 'API key not configured' },
{ status: 500 }
)
}
const genAI = new GoogleGenerativeAI(apiKey)
const model = genAI.getGenerativeModel({ model: 'gemini-1.5-flash' })
// Support both single image and multiple files
const filesToAnalyze = files || [{ data: image, type: 'image/jpeg' }]
// Build the prompt parts
const parts: any[] = []
// Add text prompt
parts.push({
text: `Analyze this luxury handbag from ${filesToAnalyze.length} image(s)/video(s). Provide detailed authentication analysis.
Identify:
1. Brand (exact name)
2. Model/Style (e.g., "Birkin 30", "Classic Flap Medium")
3. Material (e.g., "Togo leather", "Lambskin", "Canvas")
4. Color (exact color name)
5. Hardware (e.g., "Gold", "Silver", "Palladium")
6. Condition: Excellent/Very Good/Good/Fair/Poor
7. Hardware condition
8. Visible issues/flaws
9. Authenticity confidence: High/Medium/Low
10. Specific authenticity markers you observe (stitching, stamps, hardware, etc.)
11. Red flags if any
12. Current market value range in USD
13. Best platforms to sell (eBay, Poshmark, Fashionphile, etc.)
14. Market demand level (High/Medium/Low)
15. Investment rating (Excellent/Good/Fair/Poor)
Return ONLY valid JSON with these exact keys:
{
"brand": "",
"model": "",
"style": "",
"material": "",
"color": "",
"hardware": "",
"condition": "",
"hardware_condition": "",
"issues": "",
"authenticity_confidence": "",
"authenticity_markers": "",
"red_flags": "",
"estimated_value_low": 0,
"estimated_value_high": 0,
"current_market_value": 0,
"best_platforms": "",
"market_notes": "",
"investment_rating": "",
"demand_level": ""
}`
})
// Add all images/videos
for (const file of filesToAnalyze) {
if (!file.data) continue
const base64Data = file.data.split(',')[1] // Remove data:image/jpeg;base64, prefix
const mimeType = file.type || 'image/jpeg'
parts.push({
inlineData: {
data: base64Data,
mimeType: mimeType
}
})
}
const result = await model.generateContent(parts)
const aiResponse = await result.response
let text = aiResponse.text()
// Remove markdown code blocks if present
text = text.replace(/```json\n?/g, '').replace(/```\n?/g, '').trim()
const analysis: ImageAnalysisResult = JSON.parse(text)
// Log successful analysis
securityLogger.log({
type: SecurityEventType.ADMIN_ACTION,
ip: clientIp,
endpoint: '/api/analyze/image',
method: 'POST',
details: {
files_analyzed: filesToAnalyze.length,
brand: analysis.brand,
model: analysis.model
},
severity: 'low',
})
const response = NextResponse.json({
success: true,
analysis,
files_analyzed: filesToAnalyze.length
})
addSecurityHeaders(response)
return response
} catch (error: unknown) {
const errorMessage = error instanceof Error ? error.message : 'Unknown error'
console.error('Image analysis error:', errorMessage)
securityLogger.log({
type: SecurityEventType.SUSPICIOUS_ACTIVITY,
ip: clientIp,
endpoint: '/api/analyze/image',
method: 'POST',
details: { error: errorMessage },
severity: 'high',
})
const response = NextResponse.json(
{ success: false, error: 'Analysis failed' },
{ status: 500 }
)
addSecurityHeaders(response)
return response
}
}
// Increase the max body size for image uploads
export const maxDuration = 60 // 60 seconds
export const dynamic = 'force-dynamic'