- Remove newUser variable and directly return created user from Prisma transaction in register route - Add explicit casts for Prisma QueryMode to avoid TypeScript widening in listings search - Fix items search and getItemBySource with any cast and ts-expect-error to satisfy type checks Improves readability and type safety without changing behavior.
302 lines
7.4 KiB
TypeScript
302 lines
7.4 KiB
TypeScript
import { NextRequest, NextResponse } from "next/server";
|
|
import { secureLogger } from "@/lib/secure-logger";
|
|
import { requireAuth, optionalAuth } from "@/lib/auth/middleware";
|
|
import {
|
|
getListingById,
|
|
updateListing,
|
|
deleteListing,
|
|
} from "@/lib/prisma/queries/listings";
|
|
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 { id } = await params;
|
|
const listingId = parseInt(id, 10);
|
|
|
|
if (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 }
|
|
);
|
|
}
|
|
|
|
return NextResponse.json(
|
|
{
|
|
success: true,
|
|
listing,
|
|
},
|
|
{ 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 (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 (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 }
|
|
);
|
|
}
|
|
}
|
|
|