← 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'