import { type NextRequest, NextResponse } from "next/server"; import { optionalAuth, requireAuth } from "@/lib/auth/middleware"; import { createListing, getActiveListings, getListings, } from "@/lib/prisma/queries/listings"; import { getPendingIntentionListingIdsForBuyer } from "@/lib/prisma/queries/purchase-intentions"; import { isServerActive } from "@/lib/prisma/queries/servers"; import { checkRateLimit, createRateLimitResponse, getClientIP, } from "@/lib/rate-limiter"; import { secureLogger } from "@/lib/secure-logger"; import { createListingSchema } from "@/lib/validation-schemas"; /** * GET /api/listings * Lista anúncios com filtros opcionais */ export async function GET(request: NextRequest) { try { const auth = await optionalAuth(request); const { searchParams } = new URL(request.url); const filters = { status: searchParams.get("status") as | "active" | "sold" | "cancelled" | "in_escrow" | "expired" | undefined, server_name: searchParams.get("server_name") || undefined, item_id: searchParams.get("item_id") ? parseInt(searchParams.get("item_id")!, 10) : undefined, seller_id: searchParams.get("seller_id") ? parseInt(searchParams.get("seller_id")!, 10) : undefined, is_auction: searchParams.get("is_auction") === "true" ? true : searchParams.get("is_auction") === "false" ? false : undefined, search: searchParams.get("search") || undefined, page: searchParams.get("page") ? parseInt(searchParams.get("page")!, 10) : 1, pageSize: searchParams.get("pageSize") ? parseInt(searchParams.get("pageSize")!, 10) : 20, sortBy: (searchParams.get("sortBy") || "created_at") as | "created_at" | "price_pix" | "price_tibia_coins" | "price_gold_coins" | "views_count", sortOrder: (searchParams.get("sortOrder") || "DESC") as "ASC" | "DESC", }; // Se não especificar status, usar apenas ativos por padrão const listingsData = filters.status ? await getListings(filters) : await getActiveListings(filters); let listingsResponse = listingsData.listings; if (auth && listingsResponse.length > 0) { const listingIds = listingsResponse.map((listing) => listing.listing_id); const pendingListingIds = await getPendingIntentionListingIdsForBuyer( auth.userId, listingIds, ); const pendingSet = new Set(pendingListingIds); listingsResponse = listingsResponse.map((listing) => ({ ...listing, has_pending_intention_for_current_user: pendingSet.has( listing.listing_id, ), })); } return NextResponse.json( { success: true, listings: listingsResponse, pagination: { page: filters.page, pageSize: filters.pageSize, total: listingsData.total, totalPages: Math.ceil(listingsData.total / filters.pageSize), }, }, { status: 200 }, ); } catch (error) { secureLogger.error("❌ Erro ao buscar anúncios", { error: error instanceof Error ? error.message : "Erro desconhecido", stack: error instanceof Error ? error.stack : undefined, }); return NextResponse.json( { success: false, error: "Erro ao buscar anúncios", }, { status: 500 }, ); } } /** * POST /api/listings * Cria um novo anúncio (requer autenticação) */ export async function POST(request: NextRequest) { try { // Rate limiting const ip = getClientIP(request); const rateLimit = checkRateLimit(ip, "/api/listings"); if (!rateLimit.allowed) { return createRateLimitResponse(rateLimit.resetAt); } // Autenticação obrigatória const authResult = await requireAuth(request); if (authResult instanceof NextResponse) { return authResult; } const auth = authResult; const body = await request.json(); secureLogger.info("📥 POST /api/listings - Recebido", { userId: auth.userId, ip, timestamp: new Date().toISOString(), }); // Validação com Zod const validationResult = createListingSchema.safeParse(body); if (!validationResult.success) { const errors = validationResult.error.issues.map((err) => ({ field: err.path.join("."), message: err.message, })); return NextResponse.json( { success: false, error: "Dados de entrada inválidos", details: errors, }, { status: 400 }, ); } const validatedData = validationResult.data; // Validar se o servidor existe e está ativo const serverActive = await isServerActive(validatedData.server_name); if (!serverActive) { return NextResponse.json( { success: false, error: `Servidor "${validatedData.server_name}" não existe ou não está ativo`, }, { status: 400 }, ); } // Criar anúncio // O schema garante que pelo menos item_id ou item_id_unified está presente // Mas a interface CreateListingData exige item_id, então usamos item_id_unified como fallback // O refine do schema garante que pelo menos um está presente, então o fallback nunca será usado const itemId = validatedData.item_id ?? validatedData.item_id_unified!; const listing = await createListing({ seller_id: auth.userId, item_id: itemId, item_name: validatedData.item_name, quantity: validatedData.quantity, server_name: validatedData.server_name, price_pix: validatedData.price_pix ?? undefined, price_tibia_coins: validatedData.price_tibia_coins ?? undefined, price_gold_coins: validatedData.price_gold_coins ?? undefined, is_auction: validatedData.is_auction, auction_ends_at: validatedData.auction_ends_at ?? undefined, description: validatedData.description ?? undefined, trade_item_id: validatedData.trade_item_id ?? undefined, trade_item_name: validatedData.trade_item_name ?? undefined, trade_item_quantity: validatedData.trade_item_quantity ?? undefined, }); secureLogger.info("✅ Anúncio criado com sucesso", { listingId: listing.listing_id, userId: auth.userId, timestamp: new Date().toISOString(), }); return NextResponse.json( { success: true, listing, }, { status: 201 }, ); } catch (error) { secureLogger.error("❌ Erro ao criar anúncio", { error: error instanceof Error ? error.message : "Erro desconhecido", stack: error instanceof Error ? error.stack : undefined, }); return NextResponse.json( { success: false, error: "Erro ao criar anúncio", }, { status: 500 }, ); } }