Initial commit: DrinkTracker full-stack app
Next.js 14 drink collection tracker with AI-powered search, menu scanning, ratings, wishlist, sharing, and CSV backup/restore. Features: - Auth (credentials + OAuth ready) - Drink collection with ratings and reviews - AI search via Claude/OpenAI with search history - Menu photo scanning with AI extraction - Wishlist / Try Later system - Public sharing via slug URLs - CSV backup and restore (merge/replace modes) - Docker Compose for Postgres + MinIO + dev server Security: docker-compose files use env var interpolation instead of hardcoded secrets. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
102
src/app/api/ai/search/route.ts
Normal file
102
src/app/api/ai/search/route.ts
Normal file
@@ -0,0 +1,102 @@
|
||||
import { NextResponse } from "next/server"
|
||||
import { auth } from "@/lib/auth"
|
||||
import { prisma } from "@/lib/prisma"
|
||||
import { decrypt } from "@/lib/encryption"
|
||||
import { createProvider } from "@/lib/ai/provider-factory"
|
||||
import { rateLimit } from "@/lib/rate-limit"
|
||||
import { z } from "zod"
|
||||
import type { Prisma } from "@prisma/client"
|
||||
|
||||
const searchSchema = z.object({
|
||||
query: z.string().min(1).max(200),
|
||||
})
|
||||
|
||||
export async function POST(request: Request) {
|
||||
const session = await auth()
|
||||
if (!session?.user?.id) {
|
||||
return NextResponse.json({ error: "Unauthorized" }, { status: 401 })
|
||||
}
|
||||
|
||||
// Rate limit: 20 searches per minute
|
||||
const { success: withinLimit } = rateLimit(`ai-search:${session.user.id}`, 20, 60 * 1000)
|
||||
if (!withinLimit) {
|
||||
return NextResponse.json(
|
||||
{ error: "Too many requests. Please wait a moment." },
|
||||
{ status: 429 }
|
||||
)
|
||||
}
|
||||
|
||||
try {
|
||||
const body = await request.json()
|
||||
const parsed = searchSchema.safeParse(body)
|
||||
if (!parsed.success) {
|
||||
return NextResponse.json({ error: "Invalid query" }, { status: 400 })
|
||||
}
|
||||
|
||||
// Get user's AI provider
|
||||
const apiKeyRecord = await prisma.userApiKey.findFirst({
|
||||
where: { userId: session.user.id, isActive: true },
|
||||
})
|
||||
|
||||
if (!apiKeyRecord) {
|
||||
return NextResponse.json(
|
||||
{ error: "No AI provider configured. Add an API key in Settings." },
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
// Check cache first (24hr TTL)
|
||||
const queryHash = parsed.data.query.toLowerCase().trim()
|
||||
const twentyFourHoursAgo = new Date(Date.now() - 24 * 60 * 60 * 1000)
|
||||
|
||||
const cached = await prisma.searchCache.findUnique({
|
||||
where: {
|
||||
userId_queryHash_provider: {
|
||||
userId: session.user.id,
|
||||
queryHash,
|
||||
provider: apiKeyRecord.provider,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
if (cached && cached.createdAt > twentyFourHoursAgo) {
|
||||
return NextResponse.json(cached.results)
|
||||
}
|
||||
|
||||
const apiKey = decrypt(apiKeyRecord.encryptedKey, apiKeyRecord.iv)
|
||||
const provider = createProvider(apiKeyRecord.provider, apiKey)
|
||||
|
||||
const result = await provider.searchDrinks(parsed.data.query)
|
||||
|
||||
// Cache the result
|
||||
await prisma.searchCache.upsert({
|
||||
where: {
|
||||
userId_queryHash_provider: {
|
||||
userId: session.user.id,
|
||||
queryHash,
|
||||
provider: apiKeyRecord.provider,
|
||||
},
|
||||
},
|
||||
update: {
|
||||
query: parsed.data.query,
|
||||
results: { drinks: result.drinks } as unknown as Prisma.InputJsonValue,
|
||||
createdAt: new Date(),
|
||||
},
|
||||
create: {
|
||||
userId: session.user.id,
|
||||
queryHash,
|
||||
query: parsed.data.query,
|
||||
results: { drinks: result.drinks } as unknown as Prisma.InputJsonValue,
|
||||
provider: apiKeyRecord.provider,
|
||||
},
|
||||
})
|
||||
|
||||
return NextResponse.json({ drinks: result.drinks })
|
||||
} catch (error) {
|
||||
console.error("AI search error:", error)
|
||||
return NextResponse.json(
|
||||
{ error: "Search failed. Please try again." },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user