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