331 lines
7.9 KiB
TypeScript
331 lines
7.9 KiB
TypeScript
import { type NextRequest, NextResponse } from "next/server";
|
|
import { optionalAuth, requireAuth } from "@/lib/auth/middleware";
|
|
import {
|
|
deleteListing,
|
|
getListingById,
|
|
updateListing,
|
|
} from "@/lib/prisma/queries/listings";
|
|
import { hasPendingIntention } from "@/lib/prisma/queries/purchase-intentions";
|
|
import { secureLogger } from "@/lib/secure-logger";
|
|
import { updateListingSchema } from "@/lib/validation-schemas";
|
|
|
|
/**
|
|
* GET /api/listings/[id]
|
|
* Busca anúncio por ID
|
|
*/
|
|
export async function GET(
|
|
request: NextRequest,
|
|
{ params }: { params: Promise<{ id: string }> },
|
|
) {
|
|
try {
|
|
const auth = await optionalAuth(request);
|
|
const { id } = await params;
|
|
const listingId = parseInt(id, 10);
|
|
|
|
if (Number.isNaN(listingId)) {
|
|
return NextResponse.json(
|
|
{
|
|
success: false,
|
|
error: "ID de anúncio inválido",
|
|
},
|
|
{ status: 400 },
|
|
);
|
|
}
|
|
|
|
const listing = await getListingById(listingId);
|
|
|
|
if (!listing) {
|
|
return NextResponse.json(
|
|
{
|
|
success: false,
|
|
error: "Anúncio não encontrado",
|
|
},
|
|
{ status: 404 },
|
|
);
|
|
}
|
|
|
|
const hasPendingForUser = auth?.userId
|
|
? await hasPendingIntention(listingId, auth.userId)
|
|
: false;
|
|
|
|
return NextResponse.json(
|
|
{
|
|
success: true,
|
|
listing: {
|
|
...listing,
|
|
has_pending_intention_for_current_user: hasPendingForUser,
|
|
},
|
|
},
|
|
{ status: 200 },
|
|
);
|
|
} catch (error) {
|
|
secureLogger.error("❌ Erro ao buscar 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 buscar anúncio",
|
|
},
|
|
{ status: 500 },
|
|
);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* PATCH /api/listings/[id]
|
|
* Atualiza anúncio (apenas dono)
|
|
*/
|
|
export async function PATCH(
|
|
request: NextRequest,
|
|
{ params }: { params: Promise<{ id: string }> },
|
|
) {
|
|
try {
|
|
// Autenticação obrigatória
|
|
const authResult = await requireAuth(request);
|
|
|
|
if (authResult instanceof NextResponse) {
|
|
return authResult;
|
|
}
|
|
|
|
const auth = authResult;
|
|
|
|
const { id } = await params;
|
|
const listingId = parseInt(id, 10);
|
|
|
|
if (Number.isNaN(listingId)) {
|
|
return NextResponse.json(
|
|
{
|
|
success: false,
|
|
error: "ID de anúncio inválido",
|
|
},
|
|
{ status: 400 },
|
|
);
|
|
}
|
|
|
|
// Verificar se o anúncio existe e pertence ao usuário
|
|
const listing = await getListingById(listingId);
|
|
|
|
if (!listing) {
|
|
return NextResponse.json(
|
|
{
|
|
success: false,
|
|
error: "Anúncio não encontrado",
|
|
},
|
|
{ status: 404 },
|
|
);
|
|
}
|
|
|
|
if (listing.seller_id !== auth.userId) {
|
|
return NextResponse.json(
|
|
{
|
|
success: false,
|
|
error: "Você não tem permissão para editar este anúncio",
|
|
},
|
|
{ status: 403 },
|
|
);
|
|
}
|
|
|
|
const body = await request.json();
|
|
|
|
// Validação com Zod
|
|
const validationResult = updateListingSchema.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;
|
|
|
|
// Converter null para undefined para compatibilidade com UpdateListingData
|
|
const updateData: {
|
|
quantity?: number;
|
|
price_pix?: number;
|
|
price_tibia_coins?: number;
|
|
price_gold_coins?: number;
|
|
description?: string;
|
|
listing_status?:
|
|
| "active"
|
|
| "sold"
|
|
| "cancelled"
|
|
| "in_escrow"
|
|
| "expired";
|
|
current_bid_amount?: number;
|
|
current_bidder_id?: number;
|
|
} = {
|
|
...(validatedData.quantity !== undefined && {
|
|
quantity: validatedData.quantity,
|
|
}),
|
|
...(validatedData.price_pix !== null &&
|
|
validatedData.price_pix !== undefined && {
|
|
price_pix: validatedData.price_pix,
|
|
}),
|
|
...(validatedData.price_tibia_coins !== null &&
|
|
validatedData.price_tibia_coins !== undefined && {
|
|
price_tibia_coins: validatedData.price_tibia_coins,
|
|
}),
|
|
...(validatedData.price_gold_coins !== null &&
|
|
validatedData.price_gold_coins !== undefined && {
|
|
price_gold_coins: validatedData.price_gold_coins,
|
|
}),
|
|
...(validatedData.description !== null &&
|
|
validatedData.description !== undefined && {
|
|
description: validatedData.description,
|
|
}),
|
|
...(validatedData.listing_status !== undefined && {
|
|
listing_status: validatedData.listing_status,
|
|
}),
|
|
};
|
|
|
|
// Atualizar anúncio
|
|
const updatedListing = await updateListing(listingId, updateData);
|
|
|
|
if (!updatedListing) {
|
|
return NextResponse.json(
|
|
{
|
|
success: false,
|
|
error: "Erro ao atualizar anúncio",
|
|
},
|
|
{ status: 500 },
|
|
);
|
|
}
|
|
|
|
secureLogger.info("✅ Anúncio atualizado com sucesso", {
|
|
listingId,
|
|
userId: auth.userId,
|
|
timestamp: new Date().toISOString(),
|
|
});
|
|
|
|
return NextResponse.json(
|
|
{
|
|
success: true,
|
|
listing: updatedListing,
|
|
},
|
|
{ status: 200 },
|
|
);
|
|
} catch (error) {
|
|
secureLogger.error("❌ Erro ao atualizar 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 atualizar anúncio",
|
|
},
|
|
{ status: 500 },
|
|
);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* DELETE /api/listings/[id]
|
|
* Cancela anúncio (apenas dono)
|
|
*/
|
|
export async function DELETE(
|
|
request: NextRequest,
|
|
{ params }: { params: Promise<{ id: string }> },
|
|
) {
|
|
try {
|
|
// Autenticação obrigatória
|
|
const authResult = await requireAuth(request);
|
|
|
|
if (authResult instanceof NextResponse) {
|
|
return authResult;
|
|
}
|
|
|
|
const auth = authResult;
|
|
|
|
const { id } = await params;
|
|
const listingId = parseInt(id, 10);
|
|
|
|
if (Number.isNaN(listingId)) {
|
|
return NextResponse.json(
|
|
{
|
|
success: false,
|
|
error: "ID de anúncio inválido",
|
|
},
|
|
{ status: 400 },
|
|
);
|
|
}
|
|
|
|
// Verificar se o anúncio existe e pertence ao usuário
|
|
const listing = await getListingById(listingId);
|
|
|
|
if (!listing) {
|
|
return NextResponse.json(
|
|
{
|
|
success: false,
|
|
error: "Anúncio não encontrado",
|
|
},
|
|
{ status: 404 },
|
|
);
|
|
}
|
|
|
|
if (listing.seller_id !== auth.userId) {
|
|
return NextResponse.json(
|
|
{
|
|
success: false,
|
|
error: "Você não tem permissão para cancelar este anúncio",
|
|
},
|
|
{ status: 403 },
|
|
);
|
|
}
|
|
|
|
// Cancelar anúncio (soft delete)
|
|
const deleted = await deleteListing(listingId);
|
|
|
|
if (!deleted) {
|
|
return NextResponse.json(
|
|
{
|
|
success: false,
|
|
error: "Erro ao cancelar anúncio",
|
|
},
|
|
{ status: 500 },
|
|
);
|
|
}
|
|
|
|
secureLogger.info("✅ Anúncio cancelado com sucesso", {
|
|
listingId,
|
|
userId: auth.userId,
|
|
timestamp: new Date().toISOString(),
|
|
});
|
|
|
|
return NextResponse.json(
|
|
{
|
|
success: true,
|
|
message: "Anúncio cancelado com sucesso",
|
|
},
|
|
{ status: 200 },
|
|
);
|
|
} catch (error) {
|
|
secureLogger.error("❌ Erro ao cancelar 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 cancelar anúncio",
|
|
},
|
|
{ status: 500 },
|
|
);
|
|
}
|
|
}
|