Add login and register dialogs, user dropdown menu with profile link and logout option, and integrate with session management hook. Prevent hydration mismatches by conditionally rendering auth elements after mount. Update package-lock.json with new @esbuild dependencies for build tooling.
205 lines
5.6 KiB
TypeScript
205 lines
5.6 KiB
TypeScript
import { NextRequest, NextResponse } from "next/server";
|
|
import {
|
|
checkRateLimit,
|
|
getClientIP,
|
|
createRateLimitResponse,
|
|
} from "@/lib/rate-limiter";
|
|
import { secureLogger } from "@/lib/secure-logger";
|
|
import { createListingSchema } from "@/lib/validation-schemas";
|
|
import { requireAuth, optionalAuth } from "@/lib/auth/middleware";
|
|
import {
|
|
getListings,
|
|
createListing,
|
|
getActiveListings,
|
|
} from "@/lib/db/queries/listings";
|
|
import { isServerActive } from "@/lib/db/queries/servers";
|
|
|
|
/**
|
|
* GET /api/listings
|
|
* Lista anúncios com filtros opcionais
|
|
*/
|
|
export async function GET(request: NextRequest) {
|
|
try {
|
|
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);
|
|
|
|
return NextResponse.json(
|
|
{
|
|
success: true,
|
|
listings: listingsData.listings,
|
|
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
|
|
const listing = await createListing({
|
|
seller_id: auth.userId,
|
|
item_id: validatedData.item_id,
|
|
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,
|
|
});
|
|
|
|
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 }
|
|
);
|
|
}
|
|
}
|
|
|