← back to Handbag Auth Nextjs
src/app/api/price-history/route.ts
190 lines
import { NextRequest, NextResponse } from 'next/server'
import { prisma } from '@/lib/prisma'
import { rateLimit, getClientIp } from '@/lib/rateLimit'
import { addSecurityHeaders } from '@/lib/securityHeaders'
import { validateInput, priceHistoryGetSchema, priceHistoryCreateSchema } from '@/lib/validation'
import { Prisma } from '@prisma/client'
export async function GET(request: NextRequest) {
try {
// Rate limiting
const clientIp = getClientIp(request)
const rateLimitResult = rateLimit(clientIp, {
windowMs: 60 * 1000,
maxRequests: 60, // 60 requests per minute
})
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
}
// Input validation
const searchParams = request.nextUrl.searchParams
const validation = validateInput(priceHistoryGetSchema, {
externalId: searchParams.get('externalId'),
listingId: searchParams.get('listingId'),
limit: searchParams.get('limit'),
})
if (!validation.success) {
const response = NextResponse.json(
{ success: false, error: 'Invalid input parameters' },
{ status: 400 }
)
addSecurityHeaders(response)
return response
}
const { externalId, listingId, limit } = validation.data
if (!externalId && !listingId) {
const response = NextResponse.json(
{ success: false, error: 'externalId or listingId required' },
{ status: 400 }
)
addSecurityHeaders(response)
return response
}
// Type-safe where clause
const where: Prisma.PriceHistoryWhereInput = {}
if (externalId) where.externalId = externalId
if (listingId) where.listingId = listingId
const history = await prisma.priceHistory.findMany({
where,
orderBy: { recordedAt: 'desc' },
take: limit,
})
// Calculate price changes
const priceChanges = history.map((record, index) => {
if (index === history.length - 1) {
return {
...record,
priceChange: null,
priceChangePercent: null,
}
}
const prevPrice = history[index + 1].priceUsd
const currentPrice = record.priceUsd
if (!prevPrice || !currentPrice) {
return {
...record,
priceChange: null,
priceChangePercent: null,
}
}
const change = currentPrice - prevPrice
const changePercent = ((change / prevPrice) * 100).toFixed(2)
return {
...record,
priceChange: parseFloat(change.toFixed(2)),
priceChangePercent: parseFloat(changePercent),
}
})
const response = NextResponse.json({
success: true,
count: history.length,
history: priceChanges.reverse(), // Oldest first
})
addSecurityHeaders(response)
return response
} catch (error) {
console.error('Price history API error:', error)
const response = NextResponse.json(
{ success: false, error: 'Failed to fetch price history' },
{ status: 500 }
)
addSecurityHeaders(response)
return response
}
}
export async function POST(request: NextRequest) {
try {
// Rate limiting - stricter for write operations
const clientIp = getClientIp(request)
const rateLimitResult = rateLimit(clientIp, {
windowMs: 60 * 1000,
maxRequests: 20, // Lower limit for write operations
})
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
}
// Input validation
const body = await request.json()
const validation = validateInput(priceHistoryCreateSchema, body)
if (!validation.success) {
const response = NextResponse.json(
{ success: false, error: 'Invalid input data' },
{ status: 400 }
)
addSecurityHeaders(response)
return response
}
const {
listingId,
externalId,
source,
priceJpy,
priceUsd,
condition,
listingType,
isAvailable,
} = validation.data
const now = new Date().toISOString().replace('T', ' ').split('.')[0]
const priceRecord = await prisma.priceHistory.create({
data: {
listingId,
externalId,
source,
priceJpy,
priceUsd,
condition,
listingType,
recordedAt: now,
isAvailable,
},
})
const response = NextResponse.json({
success: true,
priceRecord,
})
addSecurityHeaders(response)
return response
} catch (error) {
console.error('Price history POST error:', error)
const response = NextResponse.json(
{ success: false, error: 'Failed to record price' },
{ status: 500 }
)
addSecurityHeaders(response)
return response
}
}