feat: add user authentication UI to header component
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.
This commit is contained in:
1 parent
e641138887
commit
55eebde0ba
55 files changed
+7353
-7
No files matched your search
@@ -0,0 +1,282 @@
|
||||
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/db/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;
|
||||
|
||||
// Atualizar anúncio
|
||||
const updatedListing = await updateListing(listingId, validatedData);
|
||||
|
||||
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 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { secureLogger } from "@/lib/secure-logger";
|
||||
import { incrementViews } from "@/lib/db/queries/listings";
|
||||
|
||||
/**
|
||||
* POST /api/listings/[id]/views
|
||||
* Incrementa contador de views de um anúncio
|
||||
* Não requer autenticação (público)
|
||||
*/
|
||||
export async function POST(
|
||||
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 }
|
||||
);
|
||||
}
|
||||
|
||||
// Incrementar views
|
||||
await incrementViews(listingId);
|
||||
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: true,
|
||||
message: "View incrementada",
|
||||
},
|
||||
{ status: 200 }
|
||||
);
|
||||
} catch (error) {
|
||||
secureLogger.error("❌ Erro ao incrementar views", {
|
||||
error: error instanceof Error ? error.message : "Erro desconhecido",
|
||||
stack: error instanceof Error ? error.stack : undefined,
|
||||
});
|
||||
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: "Erro ao incrementar views",
|
||||
},
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,204 @@
|
||||
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 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user