← back to Handbag Auth Nextjs

src/app/api/listings/route.ts

167 lines

import { NextRequest, NextResponse } from 'next/server'
import { prisma } from '@/lib/prisma'
import { listingSearchSchema, validateInput } from '@/lib/validation'
import { rateLimit, getClientIp } from '@/lib/rateLimit'
import { addSecurityHeaders } from '@/lib/securityHeaders'

export async function GET(request: NextRequest) {
  // Apply rate limiting
  const clientIp = getClientIp(request)
  const rateLimitResult = rateLimit(clientIp, {
    windowMs: 60 * 1000,
    maxRequests: 60, // 60 requests per minute for listings
  })

  if (!rateLimitResult.allowed) {
    const response = NextResponse.json(
      { success: false, error: 'Rate limit exceeded' },
      { status: 429 }
    )
    response.headers.set('Retry-After', String(Math.ceil((rateLimitResult.resetTime - Date.now()) / 1000)))
    addSecurityHeaders(response)
    return response
  }

  try {
    // Parse and validate input parameters
    const searchParams = Object.fromEntries(request.nextUrl.searchParams.entries())
    const validation = validateInput(listingSearchSchema, searchParams)

    if (!validation.success) {
      return NextResponse.json(
        {
          success: false,
          error: 'Invalid input parameters',
          details: validation.errors.format()
        },
        { status: 400 }
      )
    }

    const {
      search,
      brand,
      model,
      minPrice,
      maxPrice,
      type: listingType,
      dealOnly,
      showNew,
      minDeal,
      sort,
      limit,
      page
    } = validation.data

    // Build where clause
    const where: any = {
      isActive: 1,
    }

    // Filter for new listings only
    if (showNew) {
      where.isNew = 1
    }

    // Global search - search across ALL text fields including AI analysis
    if (search && search.trim()) {
      const searchTerm = search.trim()
      where.OR = [
        { title: { contains: searchTerm, mode: 'insensitive' } },
        { brand: { contains: searchTerm, mode: 'insensitive' } },
        { model: { contains: searchTerm, mode: 'insensitive' } },
        { seller: { contains: searchTerm, mode: 'insensitive' } },
        { location: { contains: searchTerm, mode: 'insensitive' } },
        { condition: { contains: searchTerm, mode: 'insensitive' } },
        { source: { contains: searchTerm, mode: 'insensitive' } },
        { externalId: { contains: searchTerm, mode: 'insensitive' } },
        { productUrl: { contains: searchTerm, mode: 'insensitive' } },
        { dealAnalysis: { aiNotes: { contains: searchTerm, mode: 'insensitive' } } },
      ]
    }

    // Specific field filters
    if (brand) where.brand = { contains: brand, mode: 'insensitive' }
    if (model) where.model = { contains: model, mode: 'insensitive' }
    if (listingType) where.listingType = listingType

    if (minPrice || maxPrice) {
      where.priceUsd = {} as any
      if (minPrice) where.priceUsd.gte = minPrice as any
      if (maxPrice) where.priceUsd.lte = maxPrice as any
    }

    if (dealOnly) {
      where.dealAnalysis = {
        isDeal: 1
      }
    }

    if (minDeal) {
      where.dealAnalysis = {
        ...where.dealAnalysis,
        dealPercentage: {
          gte: minDeal as any
        }
      }
    }

    // Build orderBy clause
    let orderBy: any = { crawledAt: 'desc' }

    switch (sort) {
      case 'deal':
        orderBy = { dealAnalysis: { dealPercentage: 'desc' } }
        break
      case 'price-high':
        orderBy = { priceUsd: 'desc' }
        break
      case 'price-low':
        orderBy = { priceUsd: 'asc' }
        break
      case 'date':
        orderBy = { crawledAt: 'desc' }
        break
    }

    // Calculate skip for pagination
    const skip = (page - 1) * limit

    // Fetch listings
    const [listings, total] = await Promise.all([
      prisma.listing.findMany({
        where,
        include: {
          dealAnalysis: true,
        },
        orderBy,
        skip,
        take: limit,
      }),
      prisma.listing.count({ where })
    ])

    const response = NextResponse.json({
      success: true,
      count: listings.length,
      total,
      page,
      totalPages: Math.ceil(total / limit),
      listings,
    })

    // Add security headers
    addSecurityHeaders(response)
    return response

  } catch (error) {
    console.error('Listings API error:', error)
    const response = NextResponse.json(
      { success: false, error: 'Failed to fetch listings' },
      { status: 500 }
    )
    addSecurityHeaders(response)
    return response
  }
}