style: clean up code formatting and remove trailing whitespace
refactor: reorganize imports and fix type imports
This commit is contained in:
1 parent
d3704602fd
commit
47c04de6fe
232 files changed
+7140
-4798
No files matched your search
@@ -1,12 +1,12 @@
|
||||
import type { Metadata } from "next";
|
||||
import { cookies } from "next/headers";
|
||||
import { redirect } from "next/navigation";
|
||||
import Header from "@/components/Header";
|
||||
import { AdminItemCreatePage } from "@/components/admin/items/AdminItemCreatePage";
|
||||
import { getSession } from "@/lib/redis/session";
|
||||
import { getUserById } from "@/lib/prisma/queries/users";
|
||||
import Header from "@/components/Header";
|
||||
import { searchAdminItems } from "@/lib/prisma/queries/items";
|
||||
import { getActiveServers } from "@/lib/prisma/queries/servers";
|
||||
import { getUserById } from "@/lib/prisma/queries/users";
|
||||
import { getSession } from "@/lib/redis/session";
|
||||
|
||||
export async function generateMetadata(): Promise<Metadata> {
|
||||
return {
|
||||
@@ -57,4 +57,3 @@ export default async function AdminNewItemPage({
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
+24
-11
@@ -1,12 +1,12 @@
|
||||
import type { Metadata } from "next";
|
||||
import { cookies } from "next/headers";
|
||||
import { redirect } from "next/navigation";
|
||||
import { AdminPanelTabs } from "@/components/admin/AdminPanelTabs";
|
||||
import Header from "@/components/Header";
|
||||
import { getSession } from "@/lib/redis/session";
|
||||
import { searchAdminItems } from "@/lib/prisma/queries/items";
|
||||
import { getAllSubscriptionTiers } from "@/lib/prisma/queries/subscription-tiers";
|
||||
import { getUserById } from "@/lib/prisma/queries/users";
|
||||
import { AdminPanelTabs } from "@/components/admin/AdminPanelTabs";
|
||||
import { searchAdminItems } from "@/lib/prisma/queries/items";
|
||||
import { getSession } from "@/lib/redis/session";
|
||||
|
||||
export async function generateMetadata(): Promise<Metadata> {
|
||||
return {
|
||||
@@ -30,7 +30,8 @@ export default async function AdminPage({
|
||||
searchParams instanceof Promise ? await searchParams : searchParams;
|
||||
const tabParam = resolvedSearchParams?.tab;
|
||||
const initialTab =
|
||||
typeof tabParam === "string" && ["overview", "subscriptions", "items"].includes(tabParam)
|
||||
typeof tabParam === "string" &&
|
||||
["overview", "subscriptions", "items"].includes(tabParam)
|
||||
? (tabParam as "overview" | "subscriptions" | "items")
|
||||
: "overview";
|
||||
|
||||
@@ -58,14 +59,24 @@ export default async function AdminPage({
|
||||
...tier,
|
||||
price_monthly: tier.price_monthly ? Number(tier.price_monthly) : null,
|
||||
price_yearly: tier.price_yearly ? Number(tier.price_yearly) : null,
|
||||
created_at: tier.created_at instanceof Date ? tier.created_at.toISOString() : tier.created_at,
|
||||
updated_at: tier.updated_at instanceof Date ? tier.updated_at.toISOString() : tier.updated_at,
|
||||
created_at:
|
||||
tier.created_at instanceof Date
|
||||
? tier.created_at.toISOString()
|
||||
: tier.created_at,
|
||||
updated_at:
|
||||
tier.updated_at instanceof Date
|
||||
? tier.updated_at.toISOString()
|
||||
: tier.updated_at,
|
||||
features: featureList.map((feature) => ({
|
||||
...feature,
|
||||
created_at:
|
||||
feature.created_at instanceof Date ? feature.created_at.toISOString() : feature.created_at ?? null,
|
||||
feature.created_at instanceof Date
|
||||
? feature.created_at.toISOString()
|
||||
: (feature.created_at ?? null),
|
||||
updated_at:
|
||||
feature.updated_at instanceof Date ? feature.updated_at.toISOString() : feature.updated_at ?? null,
|
||||
feature.updated_at instanceof Date
|
||||
? feature.updated_at.toISOString()
|
||||
: (feature.updated_at ?? null),
|
||||
})),
|
||||
};
|
||||
});
|
||||
@@ -75,9 +86,12 @@ export default async function AdminPage({
|
||||
<Header />
|
||||
<main className="container mx-auto px-4 py-10 space-y-8">
|
||||
<section className="space-y-2">
|
||||
<h1 className="text-3xl font-bold tracking-tight">Painel administrativo</h1>
|
||||
<h1 className="text-3xl font-bold tracking-tight">
|
||||
Painel administrativo
|
||||
</h1>
|
||||
<p className="text-muted-foreground max-w-2xl">
|
||||
Centralize a gestão de planos, permissões e o catálogo de itens. Utilize as abas para acessar cada área temática.
|
||||
Centralize a gestão de planos, permissões e o catálogo de itens.
|
||||
Utilize as abas para acessar cada área temática.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
@@ -92,4 +106,3 @@ export default async function AdminPage({
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -4,8 +4,7 @@ import { redirect } from "next/navigation";
|
||||
export async function generateMetadata(): Promise<Metadata> {
|
||||
return {
|
||||
title: "Assinaturas e Permissões",
|
||||
description:
|
||||
"Redirecionando para o painel administrativo centralizado.",
|
||||
description: "Redirecionando para o painel administrativo centralizado.",
|
||||
};
|
||||
}
|
||||
|
||||
@@ -17,4 +16,3 @@ export default async function AdminSubscriptionsRedirect({
|
||||
const { locale } = await params;
|
||||
redirect(`/${locale}/admin?tab=subscriptions`);
|
||||
}
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect } from "react";
|
||||
import { useParams } from "next/navigation";
|
||||
import confetti from "canvas-confetti";
|
||||
import Header from "@/components/Header";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { BlurFade } from "@/components/ui/blur-fade";
|
||||
import { Heart, Share2, Home } from "lucide-react";
|
||||
import { Heart, Home, Share2 } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { useParams } from "next/navigation";
|
||||
import { useEffect } from "react";
|
||||
import Header from "@/components/Header";
|
||||
import { BlurFade } from "@/components/ui/blur-fade";
|
||||
import { Button } from "@/components/ui/button";
|
||||
|
||||
export default function ThankYouPage() {
|
||||
const params = useParams();
|
||||
|
||||
@@ -1,20 +1,19 @@
|
||||
import Header from "@/components/Header";
|
||||
import { Heart } from "lucide-react";
|
||||
import type { Metadata } from "next";
|
||||
import { getTranslations } from "next-intl/server";
|
||||
import { DonationCard } from "@/components/donation-card";
|
||||
import { BlurFade } from "@/components/ui/blur-fade";
|
||||
import { NumberTicker } from "@/components/ui/number-ticker";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import Header from "@/components/Header";
|
||||
import { ShareButton } from "@/components/share-button";
|
||||
import {
|
||||
Accordion,
|
||||
AccordionContent,
|
||||
AccordionItem,
|
||||
AccordionTrigger,
|
||||
} from "@/components/ui/accordion";
|
||||
import { Heart } from "lucide-react";
|
||||
import { getTranslations } from "next-intl/server";
|
||||
import Link from "next/link";
|
||||
import { ShareButton } from "@/components/share-button";
|
||||
import type { Metadata } from "next";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { BlurFade } from "@/components/ui/blur-fade";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { NumberTicker } from "@/components/ui/number-ticker";
|
||||
|
||||
export async function generateMetadata({
|
||||
params,
|
||||
@@ -60,7 +59,7 @@ export default async function SupportPage({
|
||||
};
|
||||
|
||||
// Custos mensais REAIS
|
||||
const monthlyCosts = {
|
||||
const _monthlyCosts = {
|
||||
vps: 69, // R$ 69/mês
|
||||
devHours: 40, // média horas/mês
|
||||
devRate: 50, // R$ 50/hora
|
||||
|
||||
+13
-4
@@ -1,7 +1,7 @@
|
||||
import type { Metadata } from "next";
|
||||
import Script from "next/script";
|
||||
import { NextIntlClientProvider } from "next-intl";
|
||||
import { getMessages, setRequestLocale } from "next-intl/server";
|
||||
import Script from "next/script";
|
||||
import { ThemeProvider } from "@/components/theme-provider";
|
||||
import { QueryProvider } from "@/lib/providers/query-provider";
|
||||
|
||||
@@ -113,14 +113,19 @@ export default async function LocaleLayout({
|
||||
<QueryProvider>
|
||||
<NextIntlClientProvider messages={messages}>
|
||||
<ThemeProvider attribute="class" defaultTheme="dark">
|
||||
<Script id="structured-data-organization" type="application/ld+json" strategy="afterInteractive">
|
||||
<Script
|
||||
id="structured-data-organization"
|
||||
type="application/ld+json"
|
||||
strategy="afterInteractive"
|
||||
>
|
||||
{JSON.stringify({
|
||||
"@context": "https://schema.org",
|
||||
"@type": "Organization",
|
||||
name: "Tibia Trade",
|
||||
url: baseUrl,
|
||||
logo: `${baseUrl}/logo.png`,
|
||||
description: "Marketplace multi-servidor seguro para negociação de itens de Tibia",
|
||||
description:
|
||||
"Marketplace multi-servidor seguro para negociação de itens de Tibia",
|
||||
sameAs: ["https://discord.gg/tibiatrade"],
|
||||
contactPoint: {
|
||||
"@type": "ContactPoint",
|
||||
@@ -129,7 +134,11 @@ export default async function LocaleLayout({
|
||||
},
|
||||
})}
|
||||
</Script>
|
||||
<Script id="structured-data-website" type="application/ld+json" strategy="afterInteractive">
|
||||
<Script
|
||||
id="structured-data-website"
|
||||
type="application/ld+json"
|
||||
strategy="afterInteractive"
|
||||
>
|
||||
{JSON.stringify({
|
||||
"@context": "https://schema.org",
|
||||
"@type": "WebSite",
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useMemo, useRef, useState } from "react";
|
||||
import { MarketplaceFilters } from "@/components/marketplace-filters";
|
||||
import { ItemCard } from "@/components/item-card";
|
||||
import { MarketplaceFilters } from "@/components/marketplace-filters";
|
||||
|
||||
interface Item {
|
||||
id: string | number;
|
||||
@@ -38,7 +38,9 @@ export default function LibraryClient({ initial }: { initial: InitialData }) {
|
||||
const [categories, setCategories] = useState<string[]>([]);
|
||||
const [rarities, setRarities] = useState<string[]>([]);
|
||||
const [sort, setSort] = useState<string>("name");
|
||||
const [categoryOptions, setCategoryOptions] = useState<{ value: string; label: string }[] | undefined>(undefined);
|
||||
const [categoryOptions, setCategoryOptions] = useState<
|
||||
{ value: string; label: string }[] | undefined
|
||||
>(undefined);
|
||||
const [vocation, setVocation] = useState<string | null>(null);
|
||||
|
||||
const debounceRef = useRef<number | undefined>(undefined);
|
||||
@@ -77,7 +79,10 @@ export default function LibraryClient({ initial }: { initial: InitialData }) {
|
||||
if (!res.ok) return;
|
||||
const data = await res.json();
|
||||
if (Array.isArray(data.categories)) {
|
||||
const opts = (data.categories as string[]).map((c) => ({ value: c, label: c.charAt(0).toUpperCase() + c.slice(1) }));
|
||||
const opts = (data.categories as string[]).map((c) => ({
|
||||
value: c,
|
||||
label: c.charAt(0).toUpperCase() + c.slice(1),
|
||||
}));
|
||||
setCategoryOptions(opts);
|
||||
}
|
||||
} catch {
|
||||
@@ -90,7 +95,12 @@ export default function LibraryClient({ initial }: { initial: InitialData }) {
|
||||
setPage(1);
|
||||
setQuery(q);
|
||||
};
|
||||
const onFilter = (f: { rarity: string[]; category: string[]; priceRange?: { min: number; max: number }; vocation?: string | null }) => {
|
||||
const onFilter = (f: {
|
||||
rarity: string[];
|
||||
category: string[];
|
||||
priceRange?: { min: number; max: number };
|
||||
vocation?: string | null;
|
||||
}) => {
|
||||
setPage(1);
|
||||
setCategories(f.category || []);
|
||||
setRarities(f.rarity || []);
|
||||
@@ -103,7 +113,14 @@ export default function LibraryClient({ initial }: { initial: InitialData }) {
|
||||
|
||||
return (
|
||||
<div className="space-y-8">
|
||||
<MarketplaceFilters onSearch={onSearch} onFilter={onFilter} onSort={onSort} totalItems={total} showRarity={false} categoryOptionsOverride={categoryOptions} />
|
||||
<MarketplaceFilters
|
||||
onSearch={onSearch}
|
||||
onFilter={onFilter}
|
||||
onSort={onSort}
|
||||
totalItems={total}
|
||||
showRarity={false}
|
||||
categoryOptionsOverride={categoryOptions}
|
||||
/>
|
||||
|
||||
<section className="grid gap-6 grid-cols-2 sm:grid-cols-3 lg:grid-cols-4 xl:grid-cols-6 grid-flow-row-dense">
|
||||
{items.map((item) => {
|
||||
@@ -142,7 +159,10 @@ export default function LibraryClient({ initial }: { initial: InitialData }) {
|
||||
return map[v] || v || "other";
|
||||
};
|
||||
|
||||
const categoryPt = typeof anyItem.categoria === "string" ? anyItem.categoria : undefined;
|
||||
const categoryPt =
|
||||
typeof anyItem.categoria === "string"
|
||||
? anyItem.categoria
|
||||
: undefined;
|
||||
const category = normalizeCategory(item.category ?? categoryPt);
|
||||
const name = item.name ?? anyItem.nome ?? String(item.id);
|
||||
const image = item.image ?? anyItem.image_url ?? "";
|
||||
@@ -152,39 +172,39 @@ export default function LibraryClient({ initial }: { initial: InitialData }) {
|
||||
typeof item.level === "number"
|
||||
? item.level
|
||||
: typeof anyItem.nivel === "number"
|
||||
? anyItem.nivel
|
||||
: typeof anyItem.lvl === "number"
|
||||
? anyItem.lvl
|
||||
: undefined;
|
||||
? anyItem.nivel
|
||||
: typeof anyItem.lvl === "number"
|
||||
? anyItem.lvl
|
||||
: undefined;
|
||||
|
||||
const weight: number | undefined =
|
||||
typeof item.weight === "number"
|
||||
? item.weight
|
||||
: typeof anyItem.peso === "number"
|
||||
? anyItem.peso
|
||||
: typeof anyItem.peso_em_oz === "number"
|
||||
? anyItem.peso_em_oz
|
||||
: undefined;
|
||||
? anyItem.peso
|
||||
: typeof anyItem.peso_em_oz === "number"
|
||||
? anyItem.peso_em_oz
|
||||
: undefined;
|
||||
|
||||
const slots: number | undefined =
|
||||
typeof item.slots === "number"
|
||||
? item.slots
|
||||
: typeof anyItem.slots === "number"
|
||||
? anyItem.slots
|
||||
: undefined;
|
||||
? anyItem.slots
|
||||
: undefined;
|
||||
|
||||
const attack: number | undefined =
|
||||
typeof anyItem.ataque === "number"
|
||||
? anyItem.ataque
|
||||
: typeof anyItem.atq === "number"
|
||||
? anyItem.atq
|
||||
: undefined;
|
||||
? anyItem.atq
|
||||
: undefined;
|
||||
const defense: number | undefined =
|
||||
typeof anyItem.defesa === "number"
|
||||
? anyItem.defesa
|
||||
: typeof anyItem.def === "number"
|
||||
? anyItem.def
|
||||
: undefined;
|
||||
? anyItem.def
|
||||
: undefined;
|
||||
const armor: number | undefined =
|
||||
typeof anyItem.arm === "number" ? anyItem.arm : undefined;
|
||||
|
||||
@@ -192,9 +212,10 @@ export default function LibraryClient({ initial }: { initial: InitialData }) {
|
||||
const skill: string | undefined =
|
||||
typeof item.skill === "string" && item.skill !== "Nenhum."
|
||||
? item.skill
|
||||
: typeof anyItem.skill_para_usar === "string" && anyItem.skill_para_usar !== "Nenhum."
|
||||
? anyItem.skill_para_usar
|
||||
: undefined;
|
||||
: typeof anyItem.skill_para_usar === "string" &&
|
||||
anyItem.skill_para_usar !== "Nenhum."
|
||||
? anyItem.skill_para_usar
|
||||
: undefined;
|
||||
const vocation: string | undefined =
|
||||
typeof anyItem.voc === "string" && anyItem.voc
|
||||
? anyItem.voc
|
||||
@@ -203,31 +224,45 @@ export default function LibraryClient({ initial }: { initial: InitialData }) {
|
||||
return (
|
||||
<ItemCard
|
||||
key={`${String((anyItem as any).category)}::${String(item.id)}::${name}`}
|
||||
item={{
|
||||
id: String(item.id),
|
||||
name,
|
||||
image,
|
||||
category: category as any,
|
||||
rarity: (item.rarity as any) ?? "common",
|
||||
categoryPt,
|
||||
stats: {
|
||||
weight,
|
||||
slots,
|
||||
attack,
|
||||
defense,
|
||||
armor,
|
||||
} as any,
|
||||
requirements: {
|
||||
level,
|
||||
skill,
|
||||
vocation,
|
||||
} as any,
|
||||
// Campos específicos de aljavas (e reutilizáveis para outras categorias)
|
||||
volume: typeof anyItem.volume === "number" ? anyItem.volume : undefined,
|
||||
bonus_de_skill: typeof anyItem.bonus_de_skill === "number" ? anyItem.bonus_de_skill : undefined,
|
||||
protecao: typeof anyItem.protecao === "string" ? anyItem.protecao : undefined,
|
||||
dropa_de: typeof anyItem.dropa_de === "string" ? anyItem.dropa_de : undefined,
|
||||
} as any}
|
||||
item={
|
||||
{
|
||||
id: String(item.id),
|
||||
name,
|
||||
image,
|
||||
category: category as any,
|
||||
rarity: (item.rarity as any) ?? "common",
|
||||
categoryPt,
|
||||
stats: {
|
||||
weight,
|
||||
slots,
|
||||
attack,
|
||||
defense,
|
||||
armor,
|
||||
} as any,
|
||||
requirements: {
|
||||
level,
|
||||
skill,
|
||||
vocation,
|
||||
} as any,
|
||||
// Campos específicos de aljavas (e reutilizáveis para outras categorias)
|
||||
volume:
|
||||
typeof anyItem.volume === "number"
|
||||
? anyItem.volume
|
||||
: undefined,
|
||||
bonus_de_skill:
|
||||
typeof anyItem.bonus_de_skill === "number"
|
||||
? anyItem.bonus_de_skill
|
||||
: undefined,
|
||||
protecao:
|
||||
typeof anyItem.protecao === "string"
|
||||
? anyItem.protecao
|
||||
: undefined,
|
||||
dropa_de:
|
||||
typeof anyItem.dropa_de === "string"
|
||||
? anyItem.dropa_de
|
||||
: undefined,
|
||||
} as any
|
||||
}
|
||||
mode="catalog"
|
||||
/>
|
||||
);
|
||||
@@ -236,14 +271,18 @@ export default function LibraryClient({ initial }: { initial: InitialData }) {
|
||||
|
||||
<div className="flex items-center justify-center gap-2">
|
||||
<button
|
||||
type="button"
|
||||
className="px-3 py-1 rounded border text-sm hover:bg-muted disabled:opacity-50"
|
||||
onClick={() => setPage((p) => Math.max(1, p - 1))}
|
||||
disabled={page <= 1}
|
||||
>
|
||||
Anterior
|
||||
</button>
|
||||
<span className="text-sm text-muted-foreground">Página {page} de {totalPages}</span>
|
||||
<span className="text-sm text-muted-foreground">
|
||||
Página {page} de {totalPages}
|
||||
</span>
|
||||
<button
|
||||
type="button"
|
||||
className="px-3 py-1 rounded border text-sm hover:bg-muted disabled:opacity-50"
|
||||
onClick={() => setPage((p) => Math.min(totalPages, p + 1))}
|
||||
disabled={page >= totalPages}
|
||||
@@ -254,5 +293,3 @@ export default function LibraryClient({ initial }: { initial: InitialData }) {
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import Header from "@/components/Header";
|
||||
import { getTranslations } from "next-intl/server";
|
||||
import Header from "@/components/Header";
|
||||
import LibraryClient from "./LibraryClient";
|
||||
|
||||
interface Item {
|
||||
@@ -18,17 +18,24 @@ interface Item {
|
||||
|
||||
async function fetchItems(params: URLSearchParams) {
|
||||
const qs = params.toString();
|
||||
const res = await fetch(`${process.env.NEXT_PUBLIC_BASE_URL ?? ""}/api/items?${qs}`, {
|
||||
// Em ambiente server, revalidação curta para manter leve
|
||||
next: { revalidate: 15 },
|
||||
});
|
||||
const res = await fetch(
|
||||
`${process.env.NEXT_PUBLIC_BASE_URL ?? ""}/api/items?${qs}`,
|
||||
{
|
||||
// Em ambiente server, revalidação curta para manter leve
|
||||
next: { revalidate: 15 },
|
||||
},
|
||||
);
|
||||
if (!res.ok) {
|
||||
return { items: [], total: 0, page: 1, pageSize: 24, totalPages: 1 };
|
||||
}
|
||||
return res.json();
|
||||
}
|
||||
|
||||
export async function generateMetadata({ params }: { params: Promise<{ locale: string }> }) {
|
||||
export async function generateMetadata({
|
||||
params,
|
||||
}: {
|
||||
params: Promise<{ locale: string }>;
|
||||
}) {
|
||||
const { locale } = await params;
|
||||
const t = await getTranslations({ locale, namespace: "underConstruction" });
|
||||
return {
|
||||
@@ -37,7 +44,10 @@ export async function generateMetadata({ params }: { params: Promise<{ locale: s
|
||||
};
|
||||
}
|
||||
|
||||
export default async function LibraryPage({ params, searchParams }: {
|
||||
export default async function LibraryPage({
|
||||
params,
|
||||
searchParams,
|
||||
}: {
|
||||
params: Promise<{ locale: string }>;
|
||||
searchParams?: Promise<Record<string, string | string[] | undefined>>;
|
||||
}) {
|
||||
@@ -69,5 +79,3 @@ export default async function LibraryPage({ params, searchParams }: {
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,8 +1,14 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { Grid, List, Search, SortAsc, SortDesc, X } from "lucide-react";
|
||||
import { useLocale } from "next-intl";
|
||||
import { useEffect, useState } from "react";
|
||||
import { AdCard } from "@/components/marketplace/AdCard";
|
||||
import { CreateAdModal } from "@/components/marketplace/CreateAdModal";
|
||||
import { MyAdsPanel } from "@/components/marketplace/MyAdsPanel";
|
||||
import { MyPurchaseIntentionsPanel } from "@/components/marketplace/MyPurchaseIntentionsPanel";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
@@ -11,19 +17,13 @@ import {
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/components/ui/dropdown-menu";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Skeleton } from "@/components/ui/skeleton";
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||
import { Search, SortAsc, SortDesc, Filter, X, Grid, List } from "lucide-react";
|
||||
import { useMarketTelemetry } from "@/lib/hooks/use-telemetry";
|
||||
import { AdCard } from "@/components/marketplace/AdCard";
|
||||
import { CreateAdModal } from "@/components/marketplace/CreateAdModal";
|
||||
import { MyAdsPanel } from "@/components/marketplace/MyAdsPanel";
|
||||
import { MyPurchaseIntentionsPanel } from "@/components/marketplace/MyPurchaseIntentionsPanel";
|
||||
import { useSession } from "@/lib/hooks/use-session";
|
||||
import { useListings, useInvalidateListings } from "@/lib/hooks/use-listings";
|
||||
import { useInvalidateListings, useListings } from "@/lib/hooks/use-listings";
|
||||
import { useMyPurchaseIntentions } from "@/lib/hooks/use-purchase-intentions";
|
||||
import { useLocale } from "next-intl";
|
||||
import { useSession } from "@/lib/hooks/use-session";
|
||||
import { useMarketTelemetry } from "@/lib/hooks/use-telemetry";
|
||||
|
||||
interface Listing {
|
||||
listing_id: number;
|
||||
@@ -57,7 +57,7 @@ export default function MarketClient({ initial }: { initial: InitialData }) {
|
||||
const { trackSearch, trackFilter, trackPageView } = useMarketTelemetry();
|
||||
const invalidateListings = useInvalidateListings();
|
||||
const locale = useLocale();
|
||||
|
||||
|
||||
const [page, setPage] = useState<number>(initial.page || 1);
|
||||
const [pageSize] = useState<number>(initial.pageSize || 20);
|
||||
const [viewMode, setViewMode] = useState<"grid" | "list">("grid");
|
||||
@@ -73,7 +73,12 @@ export default function MarketClient({ initial }: { initial: InitialData }) {
|
||||
const [activeTab, setActiveTab] = useState<string>("explore");
|
||||
|
||||
// Usar React Query para buscar listings com atualização automática
|
||||
const { listings, total, totalPages, isLoading: loading } = useListings({
|
||||
const {
|
||||
listings,
|
||||
total,
|
||||
totalPages,
|
||||
isLoading: loading,
|
||||
} = useListings({
|
||||
search: searchQuery || undefined,
|
||||
server_name: serverName || undefined,
|
||||
price_min: priceMin || undefined,
|
||||
@@ -144,7 +149,7 @@ export default function MarketClient({ initial }: { initial: InitialData }) {
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold">Marketplace</h1>
|
||||
<p className="text-muted-foreground mt-1">
|
||||
{activeTab === "explore"
|
||||
{activeTab === "explore"
|
||||
? `${total} ${total === 1 ? "anúncio" : "anúncios"} disponíveis`
|
||||
: "Gerencie seus anúncios"}
|
||||
</p>
|
||||
@@ -161,226 +166,238 @@ export default function MarketClient({ initial }: { initial: InitialData }) {
|
||||
<TabsList>
|
||||
<TabsTrigger value="explore">Explorar</TabsTrigger>
|
||||
{user && <TabsTrigger value="my-ads">Meus Anúncios</TabsTrigger>}
|
||||
{user && (
|
||||
<MyPurchaseIntentionsTabTrigger />
|
||||
)}
|
||||
{user && <MyPurchaseIntentionsTabTrigger />}
|
||||
</TabsList>
|
||||
|
||||
{/* Aba: Explorar Anúncios */}
|
||||
<TabsContent value="explore" className="space-y-6 mt-6">
|
||||
|
||||
{/* Barra de busca */}
|
||||
<div className="relative">
|
||||
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 h-4 w-4 text-muted-foreground" />
|
||||
<Input
|
||||
placeholder="Buscar anúncios por item ou descrição..."
|
||||
value={searchQuery}
|
||||
onChange={(e) => handleSearch(e.target.value)}
|
||||
className="pl-10 pr-4"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Filtros e controles */}
|
||||
<div className="flex flex-wrap items-center gap-4">
|
||||
{/* Filtro por servidor */}
|
||||
<Input
|
||||
placeholder="Filtrar por servidor"
|
||||
value={serverName}
|
||||
onChange={(e) => {
|
||||
setServerName(e.target.value);
|
||||
handleFilter({ server: e.target.value });
|
||||
}}
|
||||
className="w-48"
|
||||
/>
|
||||
|
||||
{/* Filtro por preço mínimo */}
|
||||
<Input
|
||||
type="number"
|
||||
placeholder="Preço min"
|
||||
value={priceMin}
|
||||
onChange={(e) => {
|
||||
setPriceMin(e.target.value);
|
||||
handleFilter({ priceMin: e.target.value ? parseFloat(e.target.value) : undefined });
|
||||
}}
|
||||
className="w-32"
|
||||
/>
|
||||
|
||||
{/* Filtro por preço máximo */}
|
||||
<Input
|
||||
type="number"
|
||||
placeholder="Preço max"
|
||||
value={priceMax}
|
||||
onChange={(e) => {
|
||||
setPriceMax(e.target.value);
|
||||
handleFilter({ priceMax: e.target.value ? parseFloat(e.target.value) : undefined });
|
||||
}}
|
||||
className="w-32"
|
||||
/>
|
||||
|
||||
{/* Ordenação */}
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="outline" className="gap-2">
|
||||
{(() => {
|
||||
const selectedOption = sortOptions.find((opt) => opt.value === sortBy);
|
||||
const Icon = selectedOption?.icon;
|
||||
return Icon ? <Icon className="h-4 w-4" /> : null;
|
||||
})()}
|
||||
{sortOptions.find((opt) => opt.value === sortBy)?.label || "Ordenar"}
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent className="w-48">
|
||||
<DropdownMenuLabel>Ordenar por</DropdownMenuLabel>
|
||||
<DropdownMenuSeparator />
|
||||
{sortOptions.map((option) => {
|
||||
const Icon = option.icon;
|
||||
return (
|
||||
<DropdownMenuItem
|
||||
key={option.value}
|
||||
onClick={() => handleSort(option.value)}
|
||||
className="flex items-center gap-2"
|
||||
>
|
||||
<Icon className="h-4 w-4" />
|
||||
<span>{option.label}</span>
|
||||
{sortBy === option.value && (
|
||||
<div className="w-2 h-2 bg-primary rounded-full ml-auto" />
|
||||
)}
|
||||
</DropdownMenuItem>
|
||||
);
|
||||
})}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
|
||||
{/* Toggle de visualização */}
|
||||
<div className="flex items-center gap-1 border rounded-md p-1">
|
||||
<Button
|
||||
variant={viewMode === "grid" ? "default" : "ghost"}
|
||||
size="sm"
|
||||
onClick={() => setViewMode("grid")}
|
||||
>
|
||||
<Grid className="h-4 w-4" />
|
||||
</Button>
|
||||
<Button
|
||||
variant={viewMode === "list" ? "default" : "ghost"}
|
||||
size="sm"
|
||||
onClick={() => setViewMode("list")}
|
||||
>
|
||||
<List className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* Limpar filtros */}
|
||||
{activeFiltersCount > 0 && (
|
||||
<Button variant="ghost" onClick={clearFilters} className="gap-2">
|
||||
<X className="h-4 w-4" />
|
||||
Limpar Filtros
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Badges de filtros ativos */}
|
||||
{activeFiltersCount > 0 && (
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{searchQuery && (
|
||||
<Badge variant="secondary" className="gap-1">
|
||||
Busca: {searchQuery}
|
||||
<X
|
||||
className="h-3 w-3 cursor-pointer"
|
||||
onClick={() => {
|
||||
setSearchQuery("");
|
||||
setPage(1);
|
||||
}}
|
||||
/>
|
||||
</Badge>
|
||||
)}
|
||||
{serverName && (
|
||||
<Badge variant="secondary" className="gap-1">
|
||||
Servidor: {serverName}
|
||||
<X
|
||||
className="h-3 w-3 cursor-pointer"
|
||||
onClick={() => {
|
||||
setServerName("");
|
||||
setPage(1);
|
||||
}}
|
||||
/>
|
||||
</Badge>
|
||||
)}
|
||||
{(priceMin || priceMax) && (
|
||||
<Badge variant="secondary" className="gap-1">
|
||||
Preço: {priceMin || "0"} - {priceMax || "∞"}
|
||||
<X
|
||||
className="h-3 w-3 cursor-pointer"
|
||||
onClick={() => {
|
||||
setPriceMin("");
|
||||
setPriceMax("");
|
||||
setPage(1);
|
||||
}}
|
||||
/>
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Listagem de anúncios */}
|
||||
{loading ? (
|
||||
<div
|
||||
className={
|
||||
viewMode === "grid"
|
||||
? "grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6"
|
||||
: "space-y-4"
|
||||
}
|
||||
>
|
||||
{Array.from({ length: 8 }).map((_, i) => (
|
||||
<Skeleton key={i} className="h-64 w-full" />
|
||||
))}
|
||||
</div>
|
||||
) : listings.length === 0 ? (
|
||||
<div className="text-center py-12">
|
||||
<p className="text-muted-foreground text-lg">
|
||||
Nenhum anúncio encontrado
|
||||
</p>
|
||||
<p className="text-muted-foreground text-sm mt-2">
|
||||
Tente ajustar os filtros ou criar um novo anúncio
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<div
|
||||
className={
|
||||
viewMode === "grid"
|
||||
? "grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6"
|
||||
: "space-y-4"
|
||||
}
|
||||
>
|
||||
{listings.map((listing) => (
|
||||
<AdCard key={listing.listing_id} listing={listing} viewMode={viewMode} />
|
||||
))}
|
||||
{/* Barra de busca */}
|
||||
<div className="relative">
|
||||
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 h-4 w-4 text-muted-foreground" />
|
||||
<Input
|
||||
placeholder="Buscar anúncios por item ou descrição..."
|
||||
value={searchQuery}
|
||||
onChange={(e) => handleSearch(e.target.value)}
|
||||
className="pl-10 pr-4"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Paginação */}
|
||||
{totalPages > 1 && (
|
||||
<div className="flex items-center justify-center gap-2">
|
||||
{/* Filtros e controles */}
|
||||
<div className="flex flex-wrap items-center gap-4">
|
||||
{/* Filtro por servidor */}
|
||||
<Input
|
||||
placeholder="Filtrar por servidor"
|
||||
value={serverName}
|
||||
onChange={(e) => {
|
||||
setServerName(e.target.value);
|
||||
handleFilter({ server: e.target.value });
|
||||
}}
|
||||
className="w-48"
|
||||
/>
|
||||
|
||||
{/* Filtro por preço mínimo */}
|
||||
<Input
|
||||
type="number"
|
||||
placeholder="Preço min"
|
||||
value={priceMin}
|
||||
onChange={(e) => {
|
||||
setPriceMin(e.target.value);
|
||||
handleFilter({
|
||||
priceMin: e.target.value
|
||||
? parseFloat(e.target.value)
|
||||
: undefined,
|
||||
});
|
||||
}}
|
||||
className="w-32"
|
||||
/>
|
||||
|
||||
{/* Filtro por preço máximo */}
|
||||
<Input
|
||||
type="number"
|
||||
placeholder="Preço max"
|
||||
value={priceMax}
|
||||
onChange={(e) => {
|
||||
setPriceMax(e.target.value);
|
||||
handleFilter({
|
||||
priceMax: e.target.value
|
||||
? parseFloat(e.target.value)
|
||||
: undefined,
|
||||
});
|
||||
}}
|
||||
className="w-32"
|
||||
/>
|
||||
|
||||
{/* Ordenação */}
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="outline" className="gap-2">
|
||||
{(() => {
|
||||
const selectedOption = sortOptions.find(
|
||||
(opt) => opt.value === sortBy,
|
||||
);
|
||||
const Icon = selectedOption?.icon;
|
||||
return Icon ? <Icon className="h-4 w-4" /> : null;
|
||||
})()}
|
||||
{sortOptions.find((opt) => opt.value === sortBy)?.label ||
|
||||
"Ordenar"}
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent className="w-48">
|
||||
<DropdownMenuLabel>Ordenar por</DropdownMenuLabel>
|
||||
<DropdownMenuSeparator />
|
||||
{sortOptions.map((option) => {
|
||||
const Icon = option.icon;
|
||||
return (
|
||||
<DropdownMenuItem
|
||||
key={option.value}
|
||||
onClick={() => handleSort(option.value)}
|
||||
className="flex items-center gap-2"
|
||||
>
|
||||
<Icon className="h-4 w-4" />
|
||||
<span>{option.label}</span>
|
||||
{sortBy === option.value && (
|
||||
<div className="w-2 h-2 bg-primary rounded-full ml-auto" />
|
||||
)}
|
||||
</DropdownMenuItem>
|
||||
);
|
||||
})}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
|
||||
{/* Toggle de visualização */}
|
||||
<div className="flex items-center gap-1 border rounded-md p-1">
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => setPage((p) => Math.max(1, p - 1))}
|
||||
disabled={page === 1}
|
||||
variant={viewMode === "grid" ? "default" : "ghost"}
|
||||
size="sm"
|
||||
onClick={() => setViewMode("grid")}
|
||||
>
|
||||
Anterior
|
||||
<Grid className="h-4 w-4" />
|
||||
</Button>
|
||||
<span className="text-sm text-muted-foreground">
|
||||
Página {page} de {totalPages}
|
||||
</span>
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => setPage((p) => Math.min(totalPages, p + 1))}
|
||||
disabled={page === totalPages}
|
||||
variant={viewMode === "list" ? "default" : "ghost"}
|
||||
size="sm"
|
||||
onClick={() => setViewMode("list")}
|
||||
>
|
||||
Próxima
|
||||
<List className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* Limpar filtros */}
|
||||
{activeFiltersCount > 0 && (
|
||||
<Button variant="ghost" onClick={clearFilters} className="gap-2">
|
||||
<X className="h-4 w-4" />
|
||||
Limpar Filtros
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Badges de filtros ativos */}
|
||||
{activeFiltersCount > 0 && (
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{searchQuery && (
|
||||
<Badge variant="secondary" className="gap-1">
|
||||
Busca: {searchQuery}
|
||||
<X
|
||||
className="h-3 w-3 cursor-pointer"
|
||||
onClick={() => {
|
||||
setSearchQuery("");
|
||||
setPage(1);
|
||||
}}
|
||||
/>
|
||||
</Badge>
|
||||
)}
|
||||
{serverName && (
|
||||
<Badge variant="secondary" className="gap-1">
|
||||
Servidor: {serverName}
|
||||
<X
|
||||
className="h-3 w-3 cursor-pointer"
|
||||
onClick={() => {
|
||||
setServerName("");
|
||||
setPage(1);
|
||||
}}
|
||||
/>
|
||||
</Badge>
|
||||
)}
|
||||
{(priceMin || priceMax) && (
|
||||
<Badge variant="secondary" className="gap-1">
|
||||
Preço: {priceMin || "0"} - {priceMax || "∞"}
|
||||
<X
|
||||
className="h-3 w-3 cursor-pointer"
|
||||
onClick={() => {
|
||||
setPriceMin("");
|
||||
setPriceMax("");
|
||||
setPage(1);
|
||||
}}
|
||||
/>
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Listagem de anúncios */}
|
||||
{loading ? (
|
||||
<div
|
||||
className={
|
||||
viewMode === "grid"
|
||||
? "grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6"
|
||||
: "space-y-4"
|
||||
}
|
||||
>
|
||||
{Array.from({ length: 8 }).map((_, i) => (
|
||||
<Skeleton key={i} className="h-64 w-full" />
|
||||
))}
|
||||
</div>
|
||||
) : listings.length === 0 ? (
|
||||
<div className="text-center py-12">
|
||||
<p className="text-muted-foreground text-lg">
|
||||
Nenhum anúncio encontrado
|
||||
</p>
|
||||
<p className="text-muted-foreground text-sm mt-2">
|
||||
Tente ajustar os filtros ou criar um novo anúncio
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<div
|
||||
className={
|
||||
viewMode === "grid"
|
||||
? "grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6"
|
||||
: "space-y-4"
|
||||
}
|
||||
>
|
||||
{listings.map((listing) => (
|
||||
<AdCard
|
||||
key={listing.listing_id}
|
||||
listing={listing}
|
||||
viewMode={viewMode}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Paginação */}
|
||||
{totalPages > 1 && (
|
||||
<div className="flex items-center justify-center gap-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => setPage((p) => Math.max(1, p - 1))}
|
||||
disabled={page === 1}
|
||||
>
|
||||
Anterior
|
||||
</Button>
|
||||
<span className="text-sm text-muted-foreground">
|
||||
Página {page} de {totalPages}
|
||||
</span>
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => setPage((p) => Math.min(totalPages, p + 1))}
|
||||
disabled={page === totalPages}
|
||||
>
|
||||
Próxima
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Modal de criação de anúncio */}
|
||||
{showCreateModal && (
|
||||
@@ -437,4 +454,3 @@ function MyPurchaseIntentionsTabTrigger() {
|
||||
</TabsTrigger>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,9 +1,6 @@
|
||||
import Header from "@/components/Header";
|
||||
import { getTranslations } from "next-intl/server";
|
||||
import type { Metadata } from "next";
|
||||
import Header from "@/components/Header";
|
||||
import { AnalyticsDashboard } from "@/components/marketplace/AnalyticsDashboard";
|
||||
import { requireAuth } from "@/lib/auth/middleware";
|
||||
import { NextRequest } from "next/server";
|
||||
|
||||
export async function generateMetadata({
|
||||
params,
|
||||
@@ -38,4 +35,3 @@ export default async function AnalyticsPage({
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import Header from "@/components/Header";
|
||||
import { getTranslations } from "next-intl/server";
|
||||
import type { Metadata } from "next";
|
||||
import MarketClient from "./MarketClient";
|
||||
import Header from "@/components/Header";
|
||||
import { getActiveListings } from "@/lib/prisma/queries/listings";
|
||||
import MarketClient from "./MarketClient";
|
||||
|
||||
export async function generateMetadata({
|
||||
params,
|
||||
@@ -46,12 +45,14 @@ export default async function MarketPage({
|
||||
const sp = (await searchParams) ?? {};
|
||||
|
||||
const page = typeof sp.page === "string" ? parseInt(sp.page, 10) : 1;
|
||||
const pageSize = typeof sp.pageSize === "string" ? parseInt(sp.pageSize, 10) : 20;
|
||||
const pageSize =
|
||||
typeof sp.pageSize === "string" ? parseInt(sp.pageSize, 10) : 20;
|
||||
|
||||
// Buscar anúncios ativos
|
||||
const listingsData = await getActiveListings({
|
||||
search: typeof sp.search === "string" ? sp.search : undefined,
|
||||
server_name: typeof sp.server_name === "string" ? sp.server_name : undefined,
|
||||
server_name:
|
||||
typeof sp.server_name === "string" ? sp.server_name : undefined,
|
||||
page,
|
||||
pageSize,
|
||||
sortBy: (typeof sp.sortBy === "string" ? sp.sortBy : "created_at") as
|
||||
@@ -60,27 +61,34 @@ export default async function MarketPage({
|
||||
| "price_tibia_coins"
|
||||
| "price_gold_coins"
|
||||
| "views_count",
|
||||
sortOrder: (typeof sp.sortOrder === "string" ? sp.sortOrder : "DESC") as "ASC" | "DESC",
|
||||
sortOrder: (typeof sp.sortOrder === "string" ? sp.sortOrder : "DESC") as
|
||||
| "ASC"
|
||||
| "DESC",
|
||||
});
|
||||
|
||||
// Converter Date para string para compatibilidade com o tipo do cliente
|
||||
const serializedListings = listingsData.listings.map((listing) => ({
|
||||
...listing,
|
||||
created_at: listing.created_at instanceof Date
|
||||
? listing.created_at.toISOString()
|
||||
: listing.created_at,
|
||||
updated_at: listing.updated_at instanceof Date
|
||||
? listing.updated_at.toISOString()
|
||||
: listing.updated_at,
|
||||
sold_at: listing.sold_at instanceof Date
|
||||
? listing.sold_at.toISOString()
|
||||
: listing.sold_at,
|
||||
auction_ends_at: listing.auction_ends_at instanceof Date
|
||||
? listing.auction_ends_at.toISOString()
|
||||
: listing.auction_ends_at,
|
||||
expires_at: listing.expires_at instanceof Date
|
||||
? listing.expires_at.toISOString()
|
||||
: listing.expires_at,
|
||||
created_at:
|
||||
listing.created_at instanceof Date
|
||||
? listing.created_at.toISOString()
|
||||
: listing.created_at,
|
||||
updated_at:
|
||||
listing.updated_at instanceof Date
|
||||
? listing.updated_at.toISOString()
|
||||
: listing.updated_at,
|
||||
sold_at:
|
||||
listing.sold_at instanceof Date
|
||||
? listing.sold_at.toISOString()
|
||||
: listing.sold_at,
|
||||
auction_ends_at:
|
||||
listing.auction_ends_at instanceof Date
|
||||
? listing.auction_ends_at.toISOString()
|
||||
: listing.auction_ends_at,
|
||||
expires_at:
|
||||
listing.expires_at instanceof Date
|
||||
? listing.expires_at.toISOString()
|
||||
: listing.expires_at,
|
||||
}));
|
||||
|
||||
return (
|
||||
|
||||
@@ -1,19 +1,19 @@
|
||||
import { DonationFooterCard } from "@/components/donation-footer-card";
|
||||
import Header from "@/components/Header";
|
||||
import { EscrowBanner } from "@/components/sections/escrow-banner";
|
||||
import { EscrowExplanation } from "@/components/sections/escrow-explanation";
|
||||
import { FeaturedItems } from "@/components/sections/featured-items";
|
||||
import { HeroSection } from "@/components/sections/hero-section";
|
||||
import { HowItWorks } from "@/components/sections/how-it-works";
|
||||
import { FeaturedItems } from "@/components/sections/featured-items";
|
||||
import { MarketExample } from "@/components/sections/market-example";
|
||||
import { MarketStats } from "@/components/sections/market-stats";
|
||||
import { DonationFooterCard } from "@/components/donation-footer-card";
|
||||
|
||||
export default function Home() {
|
||||
return (
|
||||
<div className="min-h-screen">
|
||||
<Header />
|
||||
<EscrowBanner />
|
||||
<main role="main" aria-label="Página principal do Tibia Trade">
|
||||
<main aria-label="Página principal do Tibia Trade">
|
||||
<HeroSection />
|
||||
<HowItWorks />
|
||||
<FeaturedItems />
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { Metadata } from "next";
|
||||
import { getTranslations } from "next-intl/server";
|
||||
import Header from "@/components/Header";
|
||||
import { UnderConstruction } from "@/components/under-construction";
|
||||
import { getTranslations } from "next-intl/server";
|
||||
import type { Metadata } from "next";
|
||||
|
||||
export async function generateMetadata({
|
||||
params,
|
||||
@@ -9,7 +9,7 @@ export async function generateMetadata({
|
||||
params: Promise<{ locale: string }>;
|
||||
}): Promise<Metadata> {
|
||||
const { locale } = await params;
|
||||
const t = await getTranslations({ locale, namespace: "underConstruction" });
|
||||
const _t = await getTranslations({ locale, namespace: "underConstruction" });
|
||||
|
||||
const titles = {
|
||||
pt: "Preços - Estrutura de Custos",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { Metadata } from "next";
|
||||
import { getTranslations } from "next-intl/server";
|
||||
import Header from "@/components/Header";
|
||||
import { UnderConstruction } from "@/components/under-construction";
|
||||
import { getTranslations } from "next-intl/server";
|
||||
import type { Metadata } from "next";
|
||||
|
||||
export async function generateMetadata({
|
||||
params,
|
||||
|
||||
@@ -1,10 +1,14 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { type NextRequest, NextResponse } from "next/server";
|
||||
import { requireAdminUser } from "@/lib/auth/middleware";
|
||||
import { addColumnToCategory, type SQLiteColumnDefinition } from "@/lib/items/sqlite-manager";
|
||||
import {
|
||||
addColumnToCategory,
|
||||
type SQLiteColumnDefinition,
|
||||
} from "@/lib/items/sqlite-manager";
|
||||
import { secureLogger } from "@/lib/secure-logger";
|
||||
|
||||
function sanitizeColumn(column: any): SQLiteColumnDefinition {
|
||||
const name = typeof column?.name === "string" ? column.name.trim().toLowerCase() : "";
|
||||
const name =
|
||||
typeof column?.name === "string" ? column.name.trim().toLowerCase() : "";
|
||||
const type = typeof column?.type === "string" ? column.type : "TEXT";
|
||||
const notNull = column?.notNull === true;
|
||||
const defaultValue = column?.defaultValue ?? undefined;
|
||||
@@ -26,7 +30,10 @@ function sanitizeColumn(column: any): SQLiteColumnDefinition {
|
||||
} satisfies SQLiteColumnDefinition;
|
||||
}
|
||||
|
||||
export async function POST(request: NextRequest, { params }: { params: Promise<{ name: string }> }) {
|
||||
export async function POST(
|
||||
request: NextRequest,
|
||||
{ params }: { params: Promise<{ name: string }> },
|
||||
) {
|
||||
const auth = await requireAdminUser(request);
|
||||
if (auth instanceof NextResponse) {
|
||||
return auth;
|
||||
@@ -38,14 +45,19 @@ export async function POST(request: NextRequest, { params }: { params: Promise<{
|
||||
const body = await request.json();
|
||||
const column = sanitizeColumn(body?.column ?? body);
|
||||
const created = addColumnToCategory({ category: name, column });
|
||||
return NextResponse.json({ success: true, column: created }, { status: 201 });
|
||||
return NextResponse.json(
|
||||
{ success: true, column: created },
|
||||
{ status: 201 },
|
||||
);
|
||||
} catch (error) {
|
||||
secureLogger.error("Falha ao adicionar coluna à categoria", error);
|
||||
return NextResponse.json(
|
||||
{ success: false, error: error instanceof Error ? error.message : "Erro ao adicionar coluna" },
|
||||
{
|
||||
success: false,
|
||||
error:
|
||||
error instanceof Error ? error.message : "Erro ao adicionar coluna",
|
||||
},
|
||||
{ status: 400 },
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { type NextRequest, NextResponse } from "next/server";
|
||||
import { requireAdminUser } from "@/lib/auth/middleware";
|
||||
import {
|
||||
listCategoryTemplates,
|
||||
createCategoryTemplate,
|
||||
type SQLiteColumnDefinition,
|
||||
type CreateCategoryInput,
|
||||
createCategoryTemplate,
|
||||
listCategoryTemplates,
|
||||
type SQLiteColumnDefinition,
|
||||
} from "@/lib/items/sqlite-manager";
|
||||
import { secureLogger } from "@/lib/secure-logger";
|
||||
|
||||
@@ -16,7 +16,10 @@ export async function GET(request: NextRequest) {
|
||||
|
||||
try {
|
||||
const templates = listCategoryTemplates();
|
||||
return NextResponse.json({ success: true, categories: templates }, { status: 200 });
|
||||
return NextResponse.json(
|
||||
{ success: true, categories: templates },
|
||||
{ status: 200 },
|
||||
);
|
||||
} catch (error) {
|
||||
secureLogger.error("Falha ao listar categorias dinâmicas", error);
|
||||
return NextResponse.json(
|
||||
@@ -27,7 +30,8 @@ export async function GET(request: NextRequest) {
|
||||
}
|
||||
|
||||
function sanitizeColumn(column: any): SQLiteColumnDefinition {
|
||||
const name = typeof column?.name === "string" ? column.name.trim().toLowerCase() : "";
|
||||
const name =
|
||||
typeof column?.name === "string" ? column.name.trim().toLowerCase() : "";
|
||||
const type = typeof column?.type === "string" ? column.type : "TEXT";
|
||||
const notNull = column?.notNull === true;
|
||||
const defaultValue = column?.defaultValue ?? undefined;
|
||||
@@ -53,7 +57,8 @@ export async function POST(request: NextRequest) {
|
||||
|
||||
try {
|
||||
const body = await request.json();
|
||||
const rawName = typeof body?.name === "string" ? body.name.trim().toLowerCase() : "";
|
||||
const rawName =
|
||||
typeof body?.name === "string" ? body.name.trim().toLowerCase() : "";
|
||||
if (!rawName) {
|
||||
return NextResponse.json(
|
||||
{ success: false, error: "Nome da categoria é obrigatório" },
|
||||
@@ -61,10 +66,17 @@ export async function POST(request: NextRequest) {
|
||||
);
|
||||
}
|
||||
|
||||
const columnsArray: unknown[] = Array.isArray(body?.columns) ? body.columns : [];
|
||||
const columns = columnsArray.map((column) => sanitizeColumn(column)).filter((column) => {
|
||||
return !(column && ["id", "nome", "image_url", "image_local_path"].includes(column.name));
|
||||
});
|
||||
const columnsArray: unknown[] = Array.isArray(body?.columns)
|
||||
? body.columns
|
||||
: [];
|
||||
const columns = columnsArray
|
||||
.map((column) => sanitizeColumn(column))
|
||||
.filter((column) => {
|
||||
return !(
|
||||
column &&
|
||||
["id", "nome", "image_url", "image_local_path"].includes(column.name)
|
||||
);
|
||||
});
|
||||
|
||||
const input: CreateCategoryInput = {
|
||||
name: rawName,
|
||||
@@ -76,7 +88,11 @@ export async function POST(request: NextRequest) {
|
||||
} catch (error) {
|
||||
secureLogger.error("Falha ao criar categoria dinâmica", error);
|
||||
return NextResponse.json(
|
||||
{ success: false, error: error instanceof Error ? error.message : "Erro ao criar categoria" },
|
||||
{
|
||||
success: false,
|
||||
error:
|
||||
error instanceof Error ? error.message : "Erro ao criar categoria",
|
||||
},
|
||||
{ status: 400 },
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { requireAdminUser } from "@/lib/auth/middleware";
|
||||
import { type NextRequest, NextResponse } from "next/server";
|
||||
import { generateItemStructure } from "@/lib/ai/generate-item-structure";
|
||||
import { calculateTokenCost } from "@/lib/ai/token-pricing";
|
||||
import { recordTokenUsage } from "@/lib/ai/token-usage";
|
||||
import { requireAdminUser } from "@/lib/auth/middleware";
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
const auth = await requireAdminUser(request);
|
||||
@@ -39,7 +39,7 @@ export async function POST(request: NextRequest) {
|
||||
success: false,
|
||||
error: "Forneça descrição ou ao menos uma imagem para análise.",
|
||||
},
|
||||
{ status: 400 }
|
||||
{ status: 400 },
|
||||
);
|
||||
}
|
||||
|
||||
@@ -64,7 +64,8 @@ export async function POST(request: NextRequest) {
|
||||
});
|
||||
|
||||
// Cálculo de custo baseado em usageMetadata
|
||||
const model = (result.model ?? "gemini-2.5-flash") as keyof typeof import("@/lib/ai/token-pricing").TOKEN_PRICING;
|
||||
const _model = (result.model ??
|
||||
"gemini-2.5-flash") as keyof typeof import("@/lib/ai/token-pricing").TOKEN_PRICING;
|
||||
const breakdown = calculateTokenCost("gemini-2.5-flash", result.usage);
|
||||
|
||||
// Persistir histórico para relatórios (não bloqueia resposta; erros são logados no Redis util)
|
||||
@@ -104,7 +105,6 @@ export async function POST(request: NextRequest) {
|
||||
model: result.model ?? "gemini-2.5-flash",
|
||||
requestId,
|
||||
},
|
||||
{ status: 200 }
|
||||
{ status: 200 },
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,18 +1,21 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { type NextRequest, NextResponse } from "next/server";
|
||||
import { requireAdminUser } from "@/lib/auth/middleware";
|
||||
import {
|
||||
insertCategoryItem,
|
||||
listCategoryTemplates,
|
||||
updateCategoryItem,
|
||||
} from "@/lib/items/sqlite-manager";
|
||||
import {
|
||||
createAdminItem,
|
||||
getItemById,
|
||||
searchAdminItems,
|
||||
updateAdminItem,
|
||||
getItemById,
|
||||
} from "@/lib/prisma/queries/items";
|
||||
import {
|
||||
insertCategoryItem,
|
||||
updateCategoryItem,
|
||||
listCategoryTemplates,
|
||||
} from "@/lib/items/sqlite-manager";
|
||||
|
||||
function parseNumber(value: string | null | undefined, fallback: number): number {
|
||||
function parseNumber(
|
||||
value: string | null | undefined,
|
||||
fallback: number,
|
||||
): number {
|
||||
const parsed = Number(value);
|
||||
return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback;
|
||||
}
|
||||
@@ -32,18 +35,25 @@ function parseList(value: unknown): string[] {
|
||||
return [];
|
||||
}
|
||||
|
||||
function normalizeDynamicFields(category: string, rawFields: Record<string, unknown> | undefined) {
|
||||
function normalizeDynamicFields(
|
||||
category: string,
|
||||
rawFields: Record<string, unknown> | undefined,
|
||||
) {
|
||||
if (!rawFields || typeof rawFields !== "object") {
|
||||
return {} as Record<string, unknown>;
|
||||
}
|
||||
|
||||
const templates = listCategoryTemplates();
|
||||
const template = templates.find((entry) => entry.name === category.toLowerCase());
|
||||
const template = templates.find(
|
||||
(entry) => entry.name === category.toLowerCase(),
|
||||
);
|
||||
if (!template) {
|
||||
return rawFields;
|
||||
}
|
||||
|
||||
const columnMap = new Map(template.columns.map((column) => [column.name, column]));
|
||||
const columnMap = new Map(
|
||||
template.columns.map((column) => [column.name, column]),
|
||||
);
|
||||
const normalized: Record<string, unknown> = {};
|
||||
|
||||
for (const [key, value] of Object.entries(rawFields)) {
|
||||
@@ -114,7 +124,7 @@ export async function GET(request: NextRequest) {
|
||||
error: "Falha ao consultar itens",
|
||||
message: error?.message || "Erro desconhecido",
|
||||
},
|
||||
{ status: 500 }
|
||||
{ status: 500 },
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -128,21 +138,28 @@ export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const body = await request.json();
|
||||
const name = typeof body?.name === "string" ? body.name.trim() : "";
|
||||
const category = typeof body?.category === "string" ? body.category.trim().toLowerCase() : "";
|
||||
const originServer = typeof body?.server === "string" ? body.server.trim() || null : null;
|
||||
const category =
|
||||
typeof body?.category === "string"
|
||||
? body.category.trim().toLowerCase()
|
||||
: "";
|
||||
const originServer =
|
||||
typeof body?.server === "string" ? body.server.trim() || null : null;
|
||||
|
||||
if (!name || !category) {
|
||||
return NextResponse.json(
|
||||
{ success: false, error: "Nome e categoria são obrigatórios." },
|
||||
{ status: 400 }
|
||||
{ status: 400 },
|
||||
);
|
||||
}
|
||||
|
||||
const dynamicFields = normalizeDynamicFields(category, body?.fields);
|
||||
|
||||
const imageUrl = typeof body?.imageUrl === "string" ? body.imageUrl.trim() || null : null;
|
||||
const imageUrl =
|
||||
typeof body?.imageUrl === "string" ? body.imageUrl.trim() || null : null;
|
||||
const imageLocalPath =
|
||||
typeof body?.imageLocalPath === "string" ? body.imageLocalPath.trim() || null : null;
|
||||
typeof body?.imageLocalPath === "string"
|
||||
? body.imageLocalPath.trim() || null
|
||||
: null;
|
||||
|
||||
const sqliteData = {
|
||||
nome: name,
|
||||
@@ -158,16 +175,25 @@ export async function POST(request: NextRequest) {
|
||||
|
||||
const sqliteId = insertCategoryItem({ category, data: sqliteData });
|
||||
|
||||
const metadataBase = typeof body?.metadata === "object" && body.metadata !== null ? body.metadata : {};
|
||||
const metadata = { ...metadataBase, ...dynamicFields } as Record<string, unknown>;
|
||||
const metadataBase =
|
||||
typeof body?.metadata === "object" && body.metadata !== null
|
||||
? body.metadata
|
||||
: {};
|
||||
const metadata = { ...metadataBase, ...dynamicFields } as Record<
|
||||
string,
|
||||
unknown
|
||||
>;
|
||||
|
||||
const itemServers = parseList(body?.servers);
|
||||
const finalServers = originServer ? Array.from(new Set([originServer, ...itemServers])) : itemServers;
|
||||
const finalServers = originServer
|
||||
? Array.from(new Set([originServer, ...itemServers]))
|
||||
: itemServers;
|
||||
|
||||
const item = await createAdminItem({
|
||||
item_name: name,
|
||||
category,
|
||||
rarity: typeof body?.rarity === "string" ? body.rarity.trim() || null : null,
|
||||
rarity:
|
||||
typeof body?.rarity === "string" ? body.rarity.trim() || null : null,
|
||||
image_url: imageUrl,
|
||||
image_local_path: imageLocalPath,
|
||||
level: typeof body?.level === "number" ? body.level : null,
|
||||
@@ -177,7 +203,9 @@ export async function POST(request: NextRequest) {
|
||||
servers: finalServers,
|
||||
tags: parseList(body?.tags),
|
||||
description:
|
||||
typeof body?.description === "string" ? body.description.trim() || null : null,
|
||||
typeof body?.description === "string"
|
||||
? body.description.trim() || null
|
||||
: null,
|
||||
metadata,
|
||||
source_table_name: category,
|
||||
source_table_id: sqliteId,
|
||||
@@ -188,7 +216,7 @@ export async function POST(request: NextRequest) {
|
||||
success: true,
|
||||
item,
|
||||
},
|
||||
{ status: 201 }
|
||||
{ status: 201 },
|
||||
);
|
||||
} catch (error: any) {
|
||||
console.error("[admin/items][POST] erro", error?.message);
|
||||
@@ -198,7 +226,7 @@ export async function POST(request: NextRequest) {
|
||||
error: "Falha ao criar item",
|
||||
message: error?.message || "Erro desconhecido",
|
||||
},
|
||||
{ status: 500 }
|
||||
{ status: 500 },
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -216,7 +244,7 @@ export async function PUT(request: NextRequest) {
|
||||
if (!Number.isFinite(id) || id <= 0) {
|
||||
return NextResponse.json(
|
||||
{ success: false, error: "ID inválido." },
|
||||
{ status: 400 }
|
||||
{ status: 400 },
|
||||
);
|
||||
}
|
||||
|
||||
@@ -224,44 +252,67 @@ export async function PUT(request: NextRequest) {
|
||||
if (!existing) {
|
||||
return NextResponse.json(
|
||||
{ success: false, error: "Item não encontrado." },
|
||||
{ status: 404 }
|
||||
{ status: 404 },
|
||||
);
|
||||
}
|
||||
|
||||
const category = (typeof body?.category === "string" ? body.category.trim().toLowerCase() : existing.category) ?? existing.category;
|
||||
const category =
|
||||
(typeof body?.category === "string"
|
||||
? body.category.trim().toLowerCase()
|
||||
: existing.category) ?? existing.category;
|
||||
if (category !== existing.category) {
|
||||
return NextResponse.json(
|
||||
{ success: false, error: "Alterar a categoria de um item não é suportado." },
|
||||
{ status: 400 }
|
||||
{
|
||||
success: false,
|
||||
error: "Alterar a categoria de um item não é suportado.",
|
||||
},
|
||||
{ status: 400 },
|
||||
);
|
||||
}
|
||||
|
||||
const dynamicFields = normalizeDynamicFields(category, body?.fields);
|
||||
|
||||
const imageUrl = typeof body?.imageUrl === "string" ? body.imageUrl.trim() : undefined;
|
||||
const imageUrl =
|
||||
typeof body?.imageUrl === "string" ? body.imageUrl.trim() : undefined;
|
||||
const imageLocalPath =
|
||||
typeof body?.imageLocalPath === "string" ? body.imageLocalPath.trim() : undefined;
|
||||
typeof body?.imageLocalPath === "string"
|
||||
? body.imageLocalPath.trim()
|
||||
: undefined;
|
||||
|
||||
if (existing.source_table_id && existing.source_table_name) {
|
||||
const sqliteData: Record<string, unknown> = {
|
||||
nome: typeof body?.name === "string" ? body.name.trim() : existing.item_name,
|
||||
nome:
|
||||
typeof body?.name === "string"
|
||||
? body.name.trim()
|
||||
: existing.item_name,
|
||||
};
|
||||
|
||||
if (imageUrl !== undefined) sqliteData.image_url = imageUrl;
|
||||
else sqliteData.image_url = existing.image_url;
|
||||
|
||||
if (imageLocalPath !== undefined) sqliteData.image_local_path = imageLocalPath;
|
||||
if (imageLocalPath !== undefined)
|
||||
sqliteData.image_local_path = imageLocalPath;
|
||||
else sqliteData.image_local_path = existing.image_local_path;
|
||||
|
||||
for (const [key, value] of Object.entries(dynamicFields)) {
|
||||
sqliteData[key] = value;
|
||||
}
|
||||
|
||||
updateCategoryItem(existing.source_table_name, existing.source_table_id, sqliteData);
|
||||
updateCategoryItem(
|
||||
existing.source_table_name,
|
||||
existing.source_table_id,
|
||||
sqliteData,
|
||||
);
|
||||
}
|
||||
|
||||
const metadataBase = typeof body?.metadata === "object" && body.metadata !== null ? body.metadata : existing.metadata;
|
||||
const metadata = { ...metadataBase, ...dynamicFields } as Record<string, unknown>;
|
||||
const metadataBase =
|
||||
typeof body?.metadata === "object" && body.metadata !== null
|
||||
? body.metadata
|
||||
: existing.metadata;
|
||||
const metadata = { ...metadataBase, ...dynamicFields } as Record<
|
||||
string,
|
||||
unknown
|
||||
>;
|
||||
|
||||
const item = await updateAdminItem(id, {
|
||||
item_name: typeof body?.name === "string" ? body.name.trim() : undefined,
|
||||
@@ -279,8 +330,8 @@ export async function PUT(request: NextRequest) {
|
||||
typeof body?.description === "string"
|
||||
? body.description.trim()
|
||||
: body?.description === null
|
||||
? null
|
||||
: undefined,
|
||||
? null
|
||||
: undefined,
|
||||
metadata,
|
||||
});
|
||||
|
||||
@@ -289,7 +340,7 @@ export async function PUT(request: NextRequest) {
|
||||
success: true,
|
||||
item,
|
||||
},
|
||||
{ status: 200 }
|
||||
{ status: 200 },
|
||||
);
|
||||
} catch (error: any) {
|
||||
console.error("[admin/items][PUT] erro", error?.message);
|
||||
@@ -299,8 +350,7 @@ export async function PUT(request: NextRequest) {
|
||||
error: "Falha ao atualizar item",
|
||||
message: error?.message || "Erro desconhecido",
|
||||
},
|
||||
{ status: 500 }
|
||||
{ status: 500 },
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
+20
-24
@@ -1,27 +1,24 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import {
|
||||
checkRateLimit,
|
||||
getClientIP,
|
||||
createRateLimitResponse,
|
||||
} from "@/lib/rate-limiter";
|
||||
import { secureLogger } from "@/lib/secure-logger";
|
||||
import { loginSchema } from "@/lib/validation-schemas";
|
||||
import { randomUUID } from "node:crypto";
|
||||
import { type NextRequest, NextResponse } from "next/server";
|
||||
import { generateAccessToken, generateRefreshToken } from "@/lib/auth/jwt";
|
||||
import { comparePassword } from "@/lib/auth/password";
|
||||
import {
|
||||
generateAccessToken,
|
||||
generateRefreshToken,
|
||||
} from "@/lib/auth/jwt";
|
||||
getDefaultSubscriptionTier,
|
||||
getSubscriptionTierById,
|
||||
} from "@/lib/prisma/queries/subscription-tiers";
|
||||
import {
|
||||
getUserByEmail,
|
||||
getUserByUsername,
|
||||
updateLastLogin,
|
||||
} from "@/lib/prisma/queries/users";
|
||||
import { setSession } from "@/lib/redis/session";
|
||||
import { randomUUID } from "crypto";
|
||||
import {
|
||||
getDefaultSubscriptionTier,
|
||||
getSubscriptionTierById,
|
||||
} from "@/lib/prisma/queries/subscription-tiers";
|
||||
checkRateLimit,
|
||||
createRateLimitResponse,
|
||||
getClientIP,
|
||||
} from "@/lib/rate-limiter";
|
||||
import { setSession } from "@/lib/redis/session";
|
||||
import { secureLogger } from "@/lib/secure-logger";
|
||||
import { loginSchema } from "@/lib/validation-schemas";
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
@@ -55,7 +52,7 @@ export async function POST(request: NextRequest) {
|
||||
error: "Dados de entrada inválidos",
|
||||
details: errors,
|
||||
},
|
||||
{ status: 400 }
|
||||
{ status: 400 },
|
||||
);
|
||||
}
|
||||
|
||||
@@ -77,7 +74,7 @@ export async function POST(request: NextRequest) {
|
||||
success: false,
|
||||
error: "Email/username ou senha incorretos",
|
||||
},
|
||||
{ status: 401 }
|
||||
{ status: 401 },
|
||||
);
|
||||
}
|
||||
|
||||
@@ -88,7 +85,7 @@ export async function POST(request: NextRequest) {
|
||||
success: false,
|
||||
error: "Conta desativada. Entre em contato com o suporte.",
|
||||
},
|
||||
{ status: 403 }
|
||||
{ status: 403 },
|
||||
);
|
||||
}
|
||||
|
||||
@@ -104,7 +101,7 @@ export async function POST(request: NextRequest) {
|
||||
success: false,
|
||||
error: "Erro interno. Tente novamente mais tarde.",
|
||||
},
|
||||
{ status: 500 }
|
||||
{ status: 500 },
|
||||
);
|
||||
}
|
||||
|
||||
@@ -121,7 +118,7 @@ export async function POST(request: NextRequest) {
|
||||
success: false,
|
||||
error: "Email/username ou senha incorretos",
|
||||
},
|
||||
{ status: 401 }
|
||||
{ status: 401 },
|
||||
);
|
||||
}
|
||||
|
||||
@@ -181,7 +178,7 @@ export async function POST(request: NextRequest) {
|
||||
},
|
||||
accessToken,
|
||||
},
|
||||
{ status: 200 }
|
||||
{ status: 200 },
|
||||
);
|
||||
|
||||
// Adicionar refresh token e sessionId como cookies httpOnly
|
||||
@@ -220,8 +217,7 @@ export async function POST(request: NextRequest) {
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : "Erro interno",
|
||||
},
|
||||
{ status: 500 }
|
||||
{ status: 500 },
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,19 +1,19 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { secureLogger } from "@/lib/secure-logger";
|
||||
import { type NextRequest, NextResponse } from "next/server";
|
||||
import { requireAuth } from "@/lib/auth/middleware";
|
||||
import { getUserById } from "@/lib/prisma/queries/users";
|
||||
import { getUserCharacters } from "@/lib/prisma/queries/characters";
|
||||
import { getUserById } from "@/lib/prisma/queries/users";
|
||||
import { secureLogger } from "@/lib/secure-logger";
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
try {
|
||||
// Middleware de autenticação
|
||||
const authResult = await requireAuth(request);
|
||||
|
||||
|
||||
// Se retornou NextResponse, é um erro de autenticação
|
||||
if (authResult instanceof NextResponse) {
|
||||
return authResult;
|
||||
}
|
||||
|
||||
|
||||
const auth = authResult;
|
||||
|
||||
// Buscar dados do usuário
|
||||
@@ -25,7 +25,7 @@ export async function GET(request: NextRequest) {
|
||||
success: false,
|
||||
error: "Usuário não encontrado",
|
||||
},
|
||||
{ status: 404 }
|
||||
{ status: 404 },
|
||||
);
|
||||
}
|
||||
|
||||
@@ -66,7 +66,7 @@ export async function GET(request: NextRequest) {
|
||||
createdAt: char.created_at,
|
||||
})),
|
||||
},
|
||||
{ status: 200 }
|
||||
{ status: 200 },
|
||||
);
|
||||
} catch (error) {
|
||||
// Se for NextResponse (erro de autenticação), re-lançar
|
||||
@@ -85,8 +85,7 @@ export async function GET(request: NextRequest) {
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : "Erro interno",
|
||||
},
|
||||
{ status: 500 }
|
||||
{ status: 500 },
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { type NextRequest, NextResponse } from "next/server";
|
||||
import { generateAccessToken, verifyRefreshToken } from "@/lib/auth/jwt";
|
||||
import {
|
||||
checkRateLimit,
|
||||
getClientIP,
|
||||
createRateLimitResponse,
|
||||
getClientIP,
|
||||
} from "@/lib/rate-limiter";
|
||||
import { secureLogger } from "@/lib/secure-logger";
|
||||
import { verifyRefreshToken, generateAccessToken } from "@/lib/auth/jwt";
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
@@ -26,7 +26,7 @@ export async function POST(request: NextRequest) {
|
||||
success: false,
|
||||
error: "Refresh token não fornecido",
|
||||
},
|
||||
{ status: 401 }
|
||||
{ status: 401 },
|
||||
);
|
||||
}
|
||||
|
||||
@@ -40,7 +40,7 @@ export async function POST(request: NextRequest) {
|
||||
success: false,
|
||||
error: "Refresh token inválido ou expirado",
|
||||
},
|
||||
{ status: 401 }
|
||||
{ status: 401 },
|
||||
);
|
||||
|
||||
response.cookies.delete("refreshToken");
|
||||
@@ -60,7 +60,7 @@ export async function POST(request: NextRequest) {
|
||||
success: true,
|
||||
accessToken,
|
||||
},
|
||||
{ status: 200 }
|
||||
{ status: 200 },
|
||||
);
|
||||
} catch (error) {
|
||||
secureLogger.error("❌ Erro ao renovar token", {
|
||||
@@ -74,8 +74,7 @@ export async function POST(request: NextRequest) {
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : "Erro interno",
|
||||
},
|
||||
{ status: 500 }
|
||||
{ status: 500 },
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,29 +1,18 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { randomUUID } from "node:crypto";
|
||||
import { type NextRequest, NextResponse } from "next/server";
|
||||
import { generateAccessToken, generateRefreshToken } from "@/lib/auth/jwt";
|
||||
import { hashPassword } from "@/lib/auth/password";
|
||||
import { isServerActive } from "@/lib/prisma/queries/servers";
|
||||
import { getDefaultSubscriptionTier } from "@/lib/prisma/queries/subscription-tiers";
|
||||
import { getUserByEmail, getUserByUsername } from "@/lib/prisma/queries/users";
|
||||
import {
|
||||
checkRateLimit,
|
||||
getClientIP,
|
||||
createRateLimitResponse,
|
||||
getClientIP,
|
||||
} from "@/lib/rate-limiter";
|
||||
import { setSession } from "@/lib/redis/session";
|
||||
import { secureLogger } from "@/lib/secure-logger";
|
||||
import { registerSchema } from "@/lib/validation-schemas";
|
||||
import { hashPassword } from "@/lib/auth/password";
|
||||
import {
|
||||
generateAccessToken,
|
||||
generateRefreshToken,
|
||||
} from "@/lib/auth/jwt";
|
||||
import {
|
||||
getUserByEmail,
|
||||
getUserByUsername,
|
||||
} from "@/lib/prisma/queries/users";
|
||||
import {
|
||||
isServerActive,
|
||||
incrementUsersForServers,
|
||||
} from "@/lib/prisma/queries/servers";
|
||||
import { setSession } from "@/lib/redis/session";
|
||||
import { randomUUID } from "crypto";
|
||||
import { createUser } from "@/lib/prisma/queries/users";
|
||||
import { createCharacter } from "@/lib/prisma/queries/characters";
|
||||
import { getDefaultSubscriptionTier } from "@/lib/prisma/queries/subscription-tiers";
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
@@ -62,7 +51,7 @@ export async function POST(request: NextRequest) {
|
||||
error: "Dados de entrada inválidos",
|
||||
details: errors,
|
||||
},
|
||||
{ status: 400 }
|
||||
{ status: 400 },
|
||||
);
|
||||
}
|
||||
|
||||
@@ -76,7 +65,7 @@ export async function POST(request: NextRequest) {
|
||||
success: false,
|
||||
error: "Email já está em uso",
|
||||
},
|
||||
{ status: 409 }
|
||||
{ status: 409 },
|
||||
);
|
||||
}
|
||||
|
||||
@@ -88,7 +77,7 @@ export async function POST(request: NextRequest) {
|
||||
success: false,
|
||||
error: "Username já está em uso",
|
||||
},
|
||||
{ status: 409 }
|
||||
{ status: 409 },
|
||||
);
|
||||
}
|
||||
|
||||
@@ -104,7 +93,7 @@ export async function POST(request: NextRequest) {
|
||||
success: false,
|
||||
error: `Servidor "${serverName}" não existe ou não está ativo`,
|
||||
},
|
||||
{ status: 400 }
|
||||
{ status: 400 },
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -114,8 +103,11 @@ export async function POST(request: NextRequest) {
|
||||
|
||||
// Criar usuário e personagens em transação atômica usando Prisma
|
||||
const { prisma } = await import("@/lib/prisma/app");
|
||||
|
||||
const createdCharacters: Array<{ characterName: string; serverName: string }> = [];
|
||||
|
||||
const createdCharacters: Array<{
|
||||
characterName: string;
|
||||
serverName: string;
|
||||
}> = [];
|
||||
|
||||
// Usar transação Prisma
|
||||
const newUser = await prisma.$transaction(async (tx) => {
|
||||
@@ -199,7 +191,7 @@ export async function POST(request: NextRequest) {
|
||||
{
|
||||
tierSlug: defaultTier?.slug ?? undefined,
|
||||
tierLevel: defaultTier?.level ?? undefined,
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
// Criar sessão no Redis
|
||||
@@ -235,7 +227,7 @@ export async function POST(request: NextRequest) {
|
||||
},
|
||||
accessToken,
|
||||
},
|
||||
{ status: 201 }
|
||||
{ status: 201 },
|
||||
);
|
||||
|
||||
// Adicionar refresh token e sessionId como cookies httpOnly
|
||||
@@ -274,8 +266,7 @@ export async function POST(request: NextRequest) {
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : "Erro interno",
|
||||
},
|
||||
{ status: 500 }
|
||||
{ status: 500 },
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,17 +1,25 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { cookies } from "next/headers";
|
||||
import { secureLogger } from "@/lib/secure-logger";
|
||||
import { getSession, refreshSession, deleteSession, setSession } from "@/lib/redis/session";
|
||||
import { type NextRequest, NextResponse } from "next/server";
|
||||
import { verifyAccessToken } from "@/lib/auth/jwt";
|
||||
import { getMainCharacter } from "@/lib/prisma/queries/characters";
|
||||
import {
|
||||
getDefaultSubscriptionTier,
|
||||
getSubscriptionTierById,
|
||||
} from "@/lib/prisma/queries/subscription-tiers";
|
||||
import { getUserById } from "@/lib/prisma/queries/users";
|
||||
import { getSubscriptionTierById, getDefaultSubscriptionTier } from "@/lib/prisma/queries/subscription-tiers";
|
||||
import {
|
||||
deleteSession,
|
||||
getSession,
|
||||
refreshSession,
|
||||
setSession,
|
||||
} from "@/lib/redis/session";
|
||||
import { secureLogger } from "@/lib/secure-logger";
|
||||
|
||||
/**
|
||||
* GET /api/auth/session
|
||||
* Verifica e retorna dados da sessão atual
|
||||
*/
|
||||
export async function GET(request: NextRequest) {
|
||||
export async function GET(_request: NextRequest) {
|
||||
try {
|
||||
// Buscar sessionId do cookie
|
||||
const cookieStore = await cookies();
|
||||
@@ -24,7 +32,7 @@ export async function GET(request: NextRequest) {
|
||||
authenticated: false,
|
||||
error: "Nenhuma sessão encontrada",
|
||||
},
|
||||
{ status: 401 }
|
||||
{ status: 401 },
|
||||
);
|
||||
}
|
||||
|
||||
@@ -39,7 +47,7 @@ export async function GET(request: NextRequest) {
|
||||
authenticated: false,
|
||||
error: "Sessão expirada ou inválida",
|
||||
},
|
||||
{ status: 401 }
|
||||
{ status: 401 },
|
||||
);
|
||||
|
||||
response.cookies.delete("sessionId");
|
||||
@@ -58,21 +66,19 @@ export async function GET(request: NextRequest) {
|
||||
authenticated: false,
|
||||
error: "Token expirado",
|
||||
},
|
||||
{ status: 401 }
|
||||
{ status: 401 },
|
||||
);
|
||||
|
||||
response.cookies.delete("sessionId");
|
||||
return response;
|
||||
}
|
||||
|
||||
let tierInfo:
|
||||
| {
|
||||
subscription_tier_id: number | null;
|
||||
name: string | null;
|
||||
slug: string | null;
|
||||
level: number | null;
|
||||
}
|
||||
| null = {
|
||||
let tierInfo: {
|
||||
subscription_tier_id: number | null;
|
||||
name: string | null;
|
||||
slug: string | null;
|
||||
level: number | null;
|
||||
} | null = {
|
||||
subscription_tier_id: session.subscriptionTierId,
|
||||
name: session.subscriptionTierName,
|
||||
slug: session.subscriptionTierSlug,
|
||||
@@ -86,7 +92,9 @@ export async function GET(request: NextRequest) {
|
||||
) {
|
||||
const dbUser = await getUserById(session.userId);
|
||||
const fetchedTier = dbUser?.subscription_tier
|
||||
? await getSubscriptionTierById(dbUser.subscription_tier.subscription_tier_id)
|
||||
? await getSubscriptionTierById(
|
||||
dbUser.subscription_tier.subscription_tier_id,
|
||||
)
|
||||
: await getDefaultSubscriptionTier();
|
||||
|
||||
tierInfo = fetchedTier
|
||||
@@ -139,7 +147,7 @@ export async function GET(request: NextRequest) {
|
||||
: null,
|
||||
},
|
||||
},
|
||||
{ status: 200 }
|
||||
{ status: 200 },
|
||||
);
|
||||
} catch (error) {
|
||||
secureLogger.error("❌ Erro ao verificar sessão", {
|
||||
@@ -154,7 +162,7 @@ export async function GET(request: NextRequest) {
|
||||
authenticated: false,
|
||||
error: error instanceof Error ? error.message : "Erro interno",
|
||||
},
|
||||
{ status: 500 }
|
||||
{ status: 500 },
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -163,7 +171,7 @@ export async function GET(request: NextRequest) {
|
||||
* DELETE /api/auth/session
|
||||
* Encerra a sessão atual
|
||||
*/
|
||||
export async function DELETE(request: NextRequest) {
|
||||
export async function DELETE(_request: NextRequest) {
|
||||
try {
|
||||
const cookieStore = await cookies();
|
||||
const sessionId = cookieStore.get("sessionId")?.value;
|
||||
@@ -177,7 +185,7 @@ export async function DELETE(request: NextRequest) {
|
||||
success: true,
|
||||
message: "Sessão encerrada",
|
||||
},
|
||||
{ status: 200 }
|
||||
{ status: 200 },
|
||||
);
|
||||
|
||||
response.cookies.delete("sessionId");
|
||||
@@ -193,8 +201,7 @@ export async function DELETE(request: NextRequest) {
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : "Erro interno",
|
||||
},
|
||||
{ status: 500 }
|
||||
{ status: 500 },
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,13 +3,16 @@
|
||||
* POST /api/chats/[chatId]/messages - Envia mensagem (fallback se Socket.io não estiver disponível)
|
||||
*/
|
||||
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { type NextRequest, NextResponse } from "next/server";
|
||||
import { requireAuth } from "@/lib/auth/middleware";
|
||||
import { connectMongoDB } from "@/lib/mongodb/connection";
|
||||
import { Chat } from "@/lib/mongodb/models/Chat";
|
||||
import { Message } from "@/lib/mongodb/models/Message";
|
||||
import { sendMessageSchema, getMessagesSchema } from "@/lib/validation/chat-schemas";
|
||||
import { secureLogger } from "@/lib/secure-logger";
|
||||
import {
|
||||
getMessagesSchema,
|
||||
sendMessageSchema,
|
||||
} from "@/lib/validation/chat-schemas";
|
||||
|
||||
/**
|
||||
* GET /api/chats/[chatId]/messages
|
||||
@@ -17,7 +20,7 @@ import { secureLogger } from "@/lib/secure-logger";
|
||||
*/
|
||||
export async function GET(
|
||||
request: NextRequest,
|
||||
{ params }: { params: Promise<{ chatId: string }> }
|
||||
{ params }: { params: Promise<{ chatId: string }> },
|
||||
) {
|
||||
try {
|
||||
const authResult = await requireAuth(request);
|
||||
@@ -26,7 +29,7 @@ export async function GET(
|
||||
|
||||
const { chatId } = await params;
|
||||
const { searchParams } = new URL(request.url);
|
||||
|
||||
|
||||
const queryValidation = getMessagesSchema.safeParse({
|
||||
page: searchParams.get("page"),
|
||||
limit: searchParams.get("limit"),
|
||||
@@ -39,7 +42,7 @@ export async function GET(
|
||||
error: "Parâmetros inválidos",
|
||||
details: queryValidation.error.issues,
|
||||
},
|
||||
{ status: 400 }
|
||||
{ status: 400 },
|
||||
);
|
||||
}
|
||||
|
||||
@@ -53,14 +56,17 @@ export async function GET(
|
||||
if (!chat) {
|
||||
return NextResponse.json(
|
||||
{ success: false, error: "Chat não encontrado" },
|
||||
{ status: 404 }
|
||||
{ status: 404 },
|
||||
);
|
||||
}
|
||||
|
||||
if (chat.buyer_id !== auth.userId && chat.seller_id !== auth.userId) {
|
||||
return NextResponse.json(
|
||||
{ success: false, error: "Você não tem permissão para acessar este chat" },
|
||||
{ status: 403 }
|
||||
{
|
||||
success: false,
|
||||
error: "Você não tem permissão para acessar este chat",
|
||||
},
|
||||
{ status: 403 },
|
||||
);
|
||||
}
|
||||
|
||||
@@ -88,7 +94,7 @@ export async function GET(
|
||||
},
|
||||
{
|
||||
$set: { read_at: new Date() },
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
// Atualizar contador de não lidas
|
||||
@@ -100,19 +106,17 @@ export async function GET(
|
||||
await chat.save();
|
||||
|
||||
// Serializar mensagens (ordem reversa para mostrar mais antigas primeiro)
|
||||
const serializedMessages = messages
|
||||
.reverse()
|
||||
.map((msg) => ({
|
||||
_id: msg._id.toString(),
|
||||
chat_id: msg.chat_id,
|
||||
sender_id: msg.sender_id,
|
||||
content: msg.content,
|
||||
created_at: msg.created_at.toISOString(),
|
||||
read_at: msg.read_at?.toISOString() || null,
|
||||
edited_at: msg.edited_at?.toISOString() || null,
|
||||
message_type: msg.message_type,
|
||||
metadata: msg.metadata || null,
|
||||
}));
|
||||
const serializedMessages = messages.reverse().map((msg) => ({
|
||||
_id: msg._id.toString(),
|
||||
chat_id: msg.chat_id,
|
||||
sender_id: msg.sender_id,
|
||||
content: msg.content,
|
||||
created_at: msg.created_at.toISOString(),
|
||||
read_at: msg.read_at?.toISOString() || null,
|
||||
edited_at: msg.edited_at?.toISOString() || null,
|
||||
message_type: msg.message_type,
|
||||
metadata: msg.metadata || null,
|
||||
}));
|
||||
|
||||
return NextResponse.json(
|
||||
{
|
||||
@@ -125,7 +129,7 @@ export async function GET(
|
||||
totalPages: Math.ceil(total / limit),
|
||||
},
|
||||
},
|
||||
{ status: 200 }
|
||||
{ status: 200 },
|
||||
);
|
||||
} catch (error) {
|
||||
secureLogger.error("❌ Erro ao buscar mensagens", {
|
||||
@@ -135,7 +139,7 @@ export async function GET(
|
||||
|
||||
return NextResponse.json(
|
||||
{ success: false, error: "Erro ao buscar mensagens" },
|
||||
{ status: 500 }
|
||||
{ status: 500 },
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -146,7 +150,7 @@ export async function GET(
|
||||
*/
|
||||
export async function POST(
|
||||
request: NextRequest,
|
||||
{ params }: { params: Promise<{ chatId: string }> }
|
||||
{ params }: { params: Promise<{ chatId: string }> },
|
||||
) {
|
||||
try {
|
||||
const authResult = await requireAuth(request);
|
||||
@@ -164,7 +168,7 @@ export async function POST(
|
||||
error: "Dados inválidos",
|
||||
details: validationResult.error.issues,
|
||||
},
|
||||
{ status: 400 }
|
||||
{ status: 400 },
|
||||
);
|
||||
}
|
||||
|
||||
@@ -178,14 +182,17 @@ export async function POST(
|
||||
if (!chat) {
|
||||
return NextResponse.json(
|
||||
{ success: false, error: "Chat não encontrado" },
|
||||
{ status: 404 }
|
||||
{ status: 404 },
|
||||
);
|
||||
}
|
||||
|
||||
if (chat.buyer_id !== auth.userId && chat.seller_id !== auth.userId) {
|
||||
return NextResponse.json(
|
||||
{ success: false, error: "Você não tem permissão para enviar mensagens neste chat" },
|
||||
{ status: 403 }
|
||||
{
|
||||
success: false,
|
||||
error: "Você não tem permissão para enviar mensagens neste chat",
|
||||
},
|
||||
{ status: 403 },
|
||||
);
|
||||
}
|
||||
|
||||
@@ -202,7 +209,7 @@ export async function POST(
|
||||
// Atualizar chat
|
||||
chat.last_message_at = new Date();
|
||||
chat.last_message_preview = content.substring(0, 100);
|
||||
|
||||
|
||||
if (chat.buyer_id === auth.userId) {
|
||||
chat.seller_unread_count += 1;
|
||||
} else {
|
||||
@@ -228,7 +235,7 @@ export async function POST(
|
||||
},
|
||||
});
|
||||
}
|
||||
} catch (socketError) {
|
||||
} catch (_socketError) {
|
||||
// Socket.io não disponível, mensagem já foi salva
|
||||
secureLogger.warn("Socket.io não disponível, mensagem salva via REST", {
|
||||
chatId,
|
||||
@@ -248,7 +255,7 @@ export async function POST(
|
||||
message_type: message.message_type,
|
||||
},
|
||||
},
|
||||
{ status: 201 }
|
||||
{ status: 201 },
|
||||
);
|
||||
} catch (error) {
|
||||
secureLogger.error("❌ Erro ao enviar mensagem", {
|
||||
@@ -258,8 +265,7 @@ export async function POST(
|
||||
|
||||
return NextResponse.json(
|
||||
{ success: false, error: "Erro ao enviar mensagem" },
|
||||
{ status: 500 }
|
||||
{ status: 500 },
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
* GET /api/chats/[chatId] - Busca detalhes de um chat específico
|
||||
*/
|
||||
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { type NextRequest, NextResponse } from "next/server";
|
||||
import { requireAuth } from "@/lib/auth/middleware";
|
||||
import { connectMongoDB } from "@/lib/mongodb/connection";
|
||||
import { Chat } from "@/lib/mongodb/models/Chat";
|
||||
@@ -10,7 +10,7 @@ import { secureLogger } from "@/lib/secure-logger";
|
||||
|
||||
export async function GET(
|
||||
request: NextRequest,
|
||||
{ params }: { params: Promise<{ chatId: string }> }
|
||||
{ params }: { params: Promise<{ chatId: string }> },
|
||||
) {
|
||||
try {
|
||||
const authResult = await requireAuth(request);
|
||||
@@ -27,15 +27,18 @@ export async function GET(
|
||||
if (!chat) {
|
||||
return NextResponse.json(
|
||||
{ success: false, error: "Chat não encontrado" },
|
||||
{ status: 404 }
|
||||
{ status: 404 },
|
||||
);
|
||||
}
|
||||
|
||||
// Verificar se o usuário pertence ao chat
|
||||
if (chat.buyer_id !== auth.userId && chat.seller_id !== auth.userId) {
|
||||
return NextResponse.json(
|
||||
{ success: false, error: "Você não tem permissão para acessar este chat" },
|
||||
{ status: 403 }
|
||||
{
|
||||
success: false,
|
||||
error: "Você não tem permissão para acessar este chat",
|
||||
},
|
||||
{ status: 403 },
|
||||
);
|
||||
}
|
||||
|
||||
@@ -59,7 +62,7 @@ export async function GET(
|
||||
seller_cache: chat.seller_cache,
|
||||
},
|
||||
},
|
||||
{ status: 200 }
|
||||
{ status: 200 },
|
||||
);
|
||||
} catch (error) {
|
||||
secureLogger.error("❌ Erro ao buscar chat", {
|
||||
@@ -69,8 +72,7 @@ export async function GET(
|
||||
|
||||
return NextResponse.json(
|
||||
{ success: false, error: "Erro ao buscar chat" },
|
||||
{ status: 500 }
|
||||
{ status: 500 },
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
* POST /api/chats/mark-all-read
|
||||
* Marca todas as mensagens de todos os chats do usuário como lidas
|
||||
*/
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { type NextRequest, NextResponse } from "next/server";
|
||||
import { requireAuth } from "@/lib/auth/middleware";
|
||||
import { connectMongoDB } from "@/lib/mongodb/connection";
|
||||
import { Chat } from "@/lib/mongodb/models/Chat";
|
||||
@@ -26,7 +26,7 @@ export async function POST(request: NextRequest) {
|
||||
// Para cada chat, marcar mensagens não lidas como lidas
|
||||
const updatePromises = chats.map(async (chat) => {
|
||||
const chatId = chat.chat_id;
|
||||
|
||||
|
||||
// Marcar mensagens como lidas
|
||||
await Message.updateMany(
|
||||
{
|
||||
@@ -36,7 +36,7 @@ export async function POST(request: NextRequest) {
|
||||
},
|
||||
{
|
||||
$set: { read_at: new Date() },
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
// Zerar contador de não lidas
|
||||
@@ -45,7 +45,7 @@ export async function POST(request: NextRequest) {
|
||||
} else {
|
||||
chat.seller_unread_count = 0;
|
||||
}
|
||||
|
||||
|
||||
return chat.save();
|
||||
});
|
||||
|
||||
@@ -56,7 +56,7 @@ export async function POST(request: NextRequest) {
|
||||
success: true,
|
||||
message: "Todos os chats marcados como lidos",
|
||||
},
|
||||
{ status: 200 }
|
||||
{ status: 200 },
|
||||
);
|
||||
} catch (error) {
|
||||
secureLogger.error("❌ Erro ao marcar chats como lidos", {
|
||||
@@ -66,7 +66,7 @@ export async function POST(request: NextRequest) {
|
||||
|
||||
return NextResponse.json(
|
||||
{ success: false, error: "Erro ao marcar como lido" },
|
||||
{ status: 500 }
|
||||
{ status: 500 },
|
||||
);
|
||||
}
|
||||
}
|
||||
+17
-16
@@ -3,15 +3,15 @@
|
||||
* POST /api/chats - Cria novo chat
|
||||
*/
|
||||
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { type NextRequest, NextResponse } from "next/server";
|
||||
import { requireAuth } from "@/lib/auth/middleware";
|
||||
import { connectMongoDB } from "@/lib/mongodb/connection";
|
||||
import { Chat } from "@/lib/mongodb/models/Chat";
|
||||
import { createChatSchema } from "@/lib/validation/chat-schemas";
|
||||
import { getListingById } from "@/lib/prisma/queries/listings";
|
||||
import { getMainCharacter } from "@/lib/prisma/queries/characters";
|
||||
import { getListingById } from "@/lib/prisma/queries/listings";
|
||||
import { getUserById } from "@/lib/prisma/queries/users";
|
||||
import { secureLogger } from "@/lib/secure-logger";
|
||||
import { createChatSchema } from "@/lib/validation/chat-schemas";
|
||||
|
||||
/**
|
||||
* GET /api/chats
|
||||
@@ -57,7 +57,7 @@ export async function GET(request: NextRequest) {
|
||||
chats: serializedChats,
|
||||
count: serializedChats.length,
|
||||
},
|
||||
{ status: 200 }
|
||||
{ status: 200 },
|
||||
);
|
||||
} catch (error) {
|
||||
secureLogger.error("❌ Erro ao buscar chats", {
|
||||
@@ -67,7 +67,7 @@ export async function GET(request: NextRequest) {
|
||||
|
||||
return NextResponse.json(
|
||||
{ success: false, error: "Erro ao buscar chats" },
|
||||
{ status: 500 }
|
||||
{ status: 500 },
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -92,7 +92,7 @@ export async function POST(request: NextRequest) {
|
||||
error: "Dados inválidos",
|
||||
details: validationResult.error.issues,
|
||||
},
|
||||
{ status: 400 }
|
||||
{ status: 400 },
|
||||
);
|
||||
}
|
||||
|
||||
@@ -105,7 +105,7 @@ export async function POST(request: NextRequest) {
|
||||
if (!listing) {
|
||||
return NextResponse.json(
|
||||
{ success: false, error: "Anúncio não encontrado" },
|
||||
{ status: 404 }
|
||||
{ status: 404 },
|
||||
);
|
||||
}
|
||||
|
||||
@@ -113,7 +113,7 @@ export async function POST(request: NextRequest) {
|
||||
if (listing.seller_id === auth.userId) {
|
||||
return NextResponse.json(
|
||||
{ success: false, error: "Você não pode criar um chat com você mesmo" },
|
||||
{ status: 400 }
|
||||
{ status: 400 },
|
||||
);
|
||||
}
|
||||
|
||||
@@ -121,7 +121,7 @@ export async function POST(request: NextRequest) {
|
||||
if (listing.listing_status !== "active") {
|
||||
return NextResponse.json(
|
||||
{ success: false, error: "Anúncio não está disponível para chat" },
|
||||
{ status: 400 }
|
||||
{ status: 400 },
|
||||
);
|
||||
}
|
||||
|
||||
@@ -154,7 +154,7 @@ export async function POST(request: NextRequest) {
|
||||
},
|
||||
already_exists: true,
|
||||
},
|
||||
{ status: 200 }
|
||||
{ status: 200 },
|
||||
);
|
||||
}
|
||||
|
||||
@@ -163,7 +163,7 @@ export async function POST(request: NextRequest) {
|
||||
if (!buyer) {
|
||||
return NextResponse.json(
|
||||
{ success: false, error: "Usuário não encontrado" },
|
||||
{ status: 404 }
|
||||
{ status: 404 },
|
||||
);
|
||||
}
|
||||
|
||||
@@ -174,7 +174,7 @@ export async function POST(request: NextRequest) {
|
||||
if (!seller) {
|
||||
return NextResponse.json(
|
||||
{ success: false, error: "Vendedor não encontrado" },
|
||||
{ status: 404 }
|
||||
{ status: 404 },
|
||||
);
|
||||
}
|
||||
|
||||
@@ -191,7 +191,9 @@ export async function POST(request: NextRequest) {
|
||||
item_image_url: null, // Imagem será obtida via item_id se necessário
|
||||
price_pix: listing.price_pix ? Number(listing.price_pix) : null,
|
||||
price_tibia_coins: listing.price_tibia_coins || null,
|
||||
price_gold_coins: listing.price_gold_coins ? Number(listing.price_gold_coins) : null,
|
||||
price_gold_coins: listing.price_gold_coins
|
||||
? Number(listing.price_gold_coins)
|
||||
: null,
|
||||
server_name: listing.server_name,
|
||||
listing_status: listing.listing_status,
|
||||
},
|
||||
@@ -248,7 +250,7 @@ export async function POST(request: NextRequest) {
|
||||
},
|
||||
already_exists: false,
|
||||
},
|
||||
{ status: 201 }
|
||||
{ status: 201 },
|
||||
);
|
||||
} catch (error) {
|
||||
secureLogger.error("❌ Erro ao criar chat", {
|
||||
@@ -258,8 +260,7 @@ export async function POST(request: NextRequest) {
|
||||
|
||||
return NextResponse.json(
|
||||
{ success: false, error: "Erro ao criar chat" },
|
||||
{ status: 500 }
|
||||
{ status: 500 },
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
* GET /api/chats/unread-count
|
||||
* Retorna contagem de usuários únicos com mensagens não vistas
|
||||
*/
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { type NextRequest, NextResponse } from "next/server";
|
||||
import { requireAuth } from "@/lib/auth/middleware";
|
||||
import { connectMongoDB } from "@/lib/mongodb/connection";
|
||||
import { Chat } from "@/lib/mongodb/models/Chat";
|
||||
@@ -54,7 +54,7 @@ export async function GET(request: NextRequest) {
|
||||
return total + chat.seller_unread_count;
|
||||
}, 0),
|
||||
},
|
||||
{ status: 200 }
|
||||
{ status: 200 },
|
||||
);
|
||||
} catch (error) {
|
||||
secureLogger.error("❌ Erro ao buscar contagem de chats não lidos", {
|
||||
@@ -64,7 +64,7 @@ export async function GET(request: NextRequest) {
|
||||
|
||||
return NextResponse.json(
|
||||
{ success: false, error: "Erro ao buscar contagem" },
|
||||
{ status: 500 }
|
||||
{ status: 500 },
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,13 +1,13 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { type NextRequest, NextResponse } from "next/server";
|
||||
import { abacatePay } from "@/lib/abacatepay";
|
||||
import { DONATION_PRODUCTS, DonorData } from "@/types/donation";
|
||||
import { createDonationSchema } from "@/lib/validation-schemas";
|
||||
import {
|
||||
checkRateLimit,
|
||||
getClientIP,
|
||||
createRateLimitResponse,
|
||||
getClientIP,
|
||||
} from "@/lib/rate-limiter";
|
||||
import { secureLogger } from "@/lib/secure-logger";
|
||||
import { createDonationSchema } from "@/lib/validation-schemas";
|
||||
import { DONATION_PRODUCTS } from "@/types/donation";
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
@@ -49,7 +49,7 @@ export async function POST(request: NextRequest) {
|
||||
error: "Dados de entrada inválidos",
|
||||
details: errors,
|
||||
},
|
||||
{ status: 400 }
|
||||
{ status: 400 },
|
||||
);
|
||||
}
|
||||
|
||||
@@ -72,8 +72,8 @@ export async function POST(request: NextRequest) {
|
||||
if (validatedData.tier === "custom") {
|
||||
// Converter para centavos
|
||||
finalAmount = Math.round(validatedData.customAmount! * 100);
|
||||
finalDescription = `Doação Personalizada - R$ ${validatedData.customAmount!.toFixed(
|
||||
2
|
||||
finalDescription = `Doação Personalizada - R$ ${validatedData.customAmount?.toFixed(
|
||||
2,
|
||||
)}`;
|
||||
|
||||
secureLogger.info("💰 Valor customizado calculado", {
|
||||
@@ -161,7 +161,7 @@ export async function POST(request: NextRequest) {
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : "Erro interno",
|
||||
},
|
||||
{ status: 500 }
|
||||
{ status: 500 },
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,9 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { type NextRequest, NextResponse } from "next/server";
|
||||
import { getPaymentStatus } from "@/lib/payment-cache";
|
||||
import {
|
||||
checkRateLimit,
|
||||
getClientIP,
|
||||
createRateLimitResponse,
|
||||
getClientIP,
|
||||
} from "@/lib/rate-limiter";
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
@@ -21,7 +21,7 @@ export async function GET(request: NextRequest) {
|
||||
if (!pixId) {
|
||||
return NextResponse.json(
|
||||
{ error: "pixId é obrigatório" },
|
||||
{ status: 400 }
|
||||
{ status: 400 },
|
||||
);
|
||||
}
|
||||
|
||||
@@ -38,7 +38,7 @@ export async function GET(request: NextRequest) {
|
||||
console.error("Erro ao verificar status do pagamento:", error);
|
||||
return NextResponse.json(
|
||||
{ error: "Erro ao verificar status" },
|
||||
{ status: 500 }
|
||||
{ status: 500 },
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,9 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { type NextRequest, NextResponse } from "next/server";
|
||||
import { abacatePay } from "@/lib/abacatepay";
|
||||
import {
|
||||
checkRateLimit,
|
||||
getClientIP,
|
||||
createRateLimitResponse,
|
||||
getClientIP,
|
||||
} from "@/lib/rate-limiter";
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
@@ -21,7 +21,7 @@ export async function GET(request: NextRequest) {
|
||||
if (!billingId) {
|
||||
return NextResponse.json(
|
||||
{ success: false, error: "ID da cobrança é obrigatório" },
|
||||
{ status: 400 }
|
||||
{ status: 400 },
|
||||
);
|
||||
}
|
||||
|
||||
@@ -42,7 +42,7 @@ export async function GET(request: NextRequest) {
|
||||
error:
|
||||
error instanceof Error ? error.message : "Erro ao verificar cobrança",
|
||||
},
|
||||
{ status: 500 }
|
||||
{ status: 500 },
|
||||
);
|
||||
}
|
||||
}
|
||||
+31
-23
@@ -1,9 +1,9 @@
|
||||
import { NextRequest } from "next/server";
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import { getItemById } from "@/lib/items-db";
|
||||
import type { NextRequest } from "next/server";
|
||||
import { getItemByIdUnified } from "@/lib/db/queries/items";
|
||||
import { buildItemImagePath } from "@/lib/item-images";
|
||||
import { getItemById } from "@/lib/items-db";
|
||||
|
||||
export async function GET(req: NextRequest) {
|
||||
const { searchParams } = new URL(req.url);
|
||||
@@ -17,12 +17,12 @@ export async function GET(req: NextRequest) {
|
||||
if (itemId) {
|
||||
try {
|
||||
const itemIdNum = parseInt(itemId, 10);
|
||||
if (!isNaN(itemIdNum)) {
|
||||
if (!Number.isNaN(itemIdNum)) {
|
||||
// Tentar primeiro buscar na tabela unificada PostgreSQL
|
||||
// Se a tabela não existir, retornará null silenciosamente
|
||||
let item: any = null;
|
||||
item = await getItemByIdUnified(itemIdNum);
|
||||
|
||||
|
||||
// Se encontrou na tabela unificada, validar o nome se fornecido
|
||||
if (item && itemName && item.item_name) {
|
||||
if (item.item_name.toLowerCase() !== itemName.toLowerCase()) {
|
||||
@@ -30,24 +30,28 @@ export async function GET(req: NextRequest) {
|
||||
item = null;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Se não encontrou na tabela unificada, tentar SQLite legado
|
||||
// Passar itemName para buscar por ID E NOME (evita ambiguidade)
|
||||
if (!item) {
|
||||
item = getItemById(itemIdNum, itemName || undefined);
|
||||
}
|
||||
|
||||
|
||||
if (item) {
|
||||
// PRIORIDADE: 1. image_local_path (local), 2. image_url (externa), 3. image (fallback)
|
||||
const imageLocalPath = item.image_local_path || "";
|
||||
const imageUrl = item.image_url || "";
|
||||
const image = item.image || "";
|
||||
|
||||
|
||||
// Tentar primeiro a imagem local
|
||||
if (imageLocalPath) {
|
||||
const normalizedPath = imageLocalPath.replace(/\\/g, "/");
|
||||
const category = (item.categoria || item.category || "other").toLowerCase();
|
||||
|
||||
const category = (
|
||||
item.categoria ||
|
||||
item.category ||
|
||||
"other"
|
||||
).toLowerCase();
|
||||
|
||||
// Se já for um caminho completo (começa com data/), usar diretamente
|
||||
if (normalizedPath.startsWith("data/")) {
|
||||
imagePath = normalizedPath;
|
||||
@@ -55,12 +59,12 @@ export async function GET(req: NextRequest) {
|
||||
// Construir o caminho usando a função helper
|
||||
imagePath = buildItemImagePath(category, normalizedPath);
|
||||
}
|
||||
|
||||
|
||||
// Verificar se o arquivo existe antes de usar
|
||||
if (imagePath) {
|
||||
const baseDir = path.join(process.cwd(), "data", "images");
|
||||
const resolved = path.join(process.cwd(), imagePath);
|
||||
|
||||
|
||||
if (resolved.startsWith(baseDir)) {
|
||||
try {
|
||||
await fs.promises.access(resolved);
|
||||
@@ -72,11 +76,14 @@ export async function GET(req: NextRequest) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Se não encontrou imagem local ou não existe, usar URL externa como fallback
|
||||
if (!imagePath && (imageUrl || image)) {
|
||||
const externalUrl = imageUrl || image;
|
||||
if (externalUrl.startsWith("http://") || externalUrl.startsWith("https://")) {
|
||||
if (
|
||||
externalUrl.startsWith("http://") ||
|
||||
externalUrl.startsWith("https://")
|
||||
) {
|
||||
return Response.redirect(externalUrl, 302);
|
||||
}
|
||||
}
|
||||
@@ -99,7 +106,7 @@ export async function GET(req: NextRequest) {
|
||||
// Permitir apenas caminhos sob data/images para segurança
|
||||
const baseDir = path.join(process.cwd(), "data", "images");
|
||||
const resolved = path.join(process.cwd(), imagePath);
|
||||
|
||||
|
||||
if (!resolved.startsWith(baseDir)) {
|
||||
return new Response("Forbidden", { status: 403 });
|
||||
}
|
||||
@@ -111,18 +118,19 @@ export async function GET(req: NextRequest) {
|
||||
ext === ".gif"
|
||||
? "image/gif"
|
||||
: ext === ".png"
|
||||
? "image/png"
|
||||
: ext === ".webp"
|
||||
? "image/webp"
|
||||
: ext === ".jpg" || ext === ".jpeg"
|
||||
? "image/jpeg"
|
||||
: "application/octet-stream";
|
||||
return new Response(file, { status: 200, headers: { "Content-Type": contentType } });
|
||||
? "image/png"
|
||||
: ext === ".webp"
|
||||
? "image/webp"
|
||||
: ext === ".jpg" || ext === ".jpeg"
|
||||
? "image/jpeg"
|
||||
: "application/octet-stream";
|
||||
return new Response(file, {
|
||||
status: 200,
|
||||
headers: { "Content-Type": contentType },
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("[api/images] Erro ao ler arquivo:", error);
|
||||
// Retornar 404 silenciosamente para não gerar erros no console
|
||||
return new Response("Not Found", { status: 404 });
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { NextRequest } from "next/server";
|
||||
import type { NextRequest } from "next/server";
|
||||
import { prisma } from "@/lib/prisma/app";
|
||||
|
||||
export async function GET(_req: NextRequest) {
|
||||
@@ -15,11 +15,6 @@ export async function GET(_req: NextRequest) {
|
||||
.filter((category): category is string => Boolean(category)),
|
||||
});
|
||||
} catch (e: any) {
|
||||
return Response.json(
|
||||
{ error: e?.message || "Erro" },
|
||||
{ status: 500 }
|
||||
);
|
||||
return Response.json({ error: e?.message || "Erro" }, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+19
-9
@@ -1,6 +1,6 @@
|
||||
import { NextRequest } from "next/server";
|
||||
import { searchItems, type ItemSort } from "@/lib/prisma/queries/items";
|
||||
import type { NextRequest } from "next/server";
|
||||
import { buildPublicItemImageUrl } from "@/lib/item-images";
|
||||
import { type ItemSort, searchItems } from "@/lib/prisma/queries/items";
|
||||
|
||||
function parseArray(param: string | null): string[] {
|
||||
if (!param) return [];
|
||||
@@ -28,7 +28,8 @@ export async function GET(req: NextRequest) {
|
||||
const rarities = parseArray(searchParams.get("rarity"));
|
||||
const server = searchParams.get("server") || undefined;
|
||||
const sortParamRaw = (searchParams.get("sort") || "name").toLowerCase();
|
||||
const sortParam = (allowedSorts.find((sort) => sort === sortParamRaw) || "name") as ItemSort;
|
||||
const sortParam = (allowedSorts.find((sort) => sort === sortParamRaw) ||
|
||||
"name") as ItemSort;
|
||||
const page = Number(searchParams.get("page") || "1");
|
||||
const pageSize = Number(searchParams.get("pageSize") || "24");
|
||||
|
||||
@@ -48,7 +49,11 @@ export async function GET(req: NextRequest) {
|
||||
return { ...item, image_url: img, image: img };
|
||||
}
|
||||
if (img.startsWith("data/")) {
|
||||
return { ...item, image_url: img, image: `/api/images?path=${encodeURIComponent(img)}` };
|
||||
return {
|
||||
...item,
|
||||
image_url: img,
|
||||
image: `/api/images?path=${encodeURIComponent(img)}`,
|
||||
};
|
||||
}
|
||||
const category = item.category || "other";
|
||||
return {
|
||||
@@ -60,8 +65,15 @@ export async function GET(req: NextRequest) {
|
||||
|
||||
const totalPages = Math.max(1, Math.ceil(total / Math.max(1, pageSize)));
|
||||
return Response.json(
|
||||
{ items: itemsWithImage, page, pageSize, total, totalPages, sort: sortParam },
|
||||
{ status: 200 }
|
||||
{
|
||||
items: itemsWithImage,
|
||||
page,
|
||||
pageSize,
|
||||
total,
|
||||
totalPages,
|
||||
sort: sortParam,
|
||||
},
|
||||
{ status: 200 },
|
||||
);
|
||||
} catch (error: any) {
|
||||
console.error("[api/items] Erro:", error?.message, error?.stack);
|
||||
@@ -70,9 +82,7 @@ export async function GET(req: NextRequest) {
|
||||
error: "Falha ao consultar itens",
|
||||
message: error?.message || "Erro desconhecido",
|
||||
},
|
||||
{ status: 500 }
|
||||
{ status: 500 },
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { secureLogger } from "@/lib/secure-logger";
|
||||
import { type NextRequest, NextResponse } from "next/server";
|
||||
import { requireAuth } from "@/lib/auth/middleware";
|
||||
import { getListingById, updateListing } from "@/lib/prisma/queries/listings";
|
||||
import { secureLogger } from "@/lib/secure-logger";
|
||||
|
||||
/**
|
||||
* POST /api/listings/:id/activate
|
||||
@@ -9,7 +9,7 @@ import { getListingById, updateListing } from "@/lib/prisma/queries/listings";
|
||||
*/
|
||||
export async function POST(
|
||||
request: NextRequest,
|
||||
{ params }: { params: Promise<{ id: string }> }
|
||||
{ params }: { params: Promise<{ id: string }> },
|
||||
) {
|
||||
try {
|
||||
// Autenticação obrigatória
|
||||
@@ -24,13 +24,13 @@ export async function POST(
|
||||
const { id } = await params;
|
||||
const listingId = parseInt(id, 10);
|
||||
|
||||
if (isNaN(listingId)) {
|
||||
if (Number.isNaN(listingId)) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: "ID de anúncio inválido",
|
||||
},
|
||||
{ status: 400 }
|
||||
{ status: 400 },
|
||||
);
|
||||
}
|
||||
|
||||
@@ -43,7 +43,7 @@ export async function POST(
|
||||
success: false,
|
||||
error: "Anúncio não encontrado",
|
||||
},
|
||||
{ status: 404 }
|
||||
{ status: 404 },
|
||||
);
|
||||
}
|
||||
|
||||
@@ -53,7 +53,7 @@ export async function POST(
|
||||
success: false,
|
||||
error: "Você não tem permissão para reativar este anúncio",
|
||||
},
|
||||
{ status: 403 }
|
||||
{ status: 403 },
|
||||
);
|
||||
}
|
||||
|
||||
@@ -63,7 +63,7 @@ export async function POST(
|
||||
success: false,
|
||||
error: "Apenas anúncios pausados podem ser reativados",
|
||||
},
|
||||
{ status: 400 }
|
||||
{ status: 400 },
|
||||
);
|
||||
}
|
||||
|
||||
@@ -78,7 +78,7 @@ export async function POST(
|
||||
success: false,
|
||||
error: "Erro ao reativar anúncio",
|
||||
},
|
||||
{ status: 500 }
|
||||
{ status: 500 },
|
||||
);
|
||||
}
|
||||
|
||||
@@ -93,7 +93,7 @@ export async function POST(
|
||||
listing: updated,
|
||||
message: "Anúncio reativado com sucesso",
|
||||
},
|
||||
{ status: 200 }
|
||||
{ status: 200 },
|
||||
);
|
||||
} catch (error) {
|
||||
secureLogger.error("❌ Erro ao reativar anúncio", {
|
||||
@@ -105,8 +105,7 @@ export async function POST(
|
||||
success: false,
|
||||
error: "Erro ao reativar anúncio",
|
||||
},
|
||||
{ status: 500 }
|
||||
{ status: 500 },
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { secureLogger } from "@/lib/secure-logger";
|
||||
import { type NextRequest, NextResponse } from "next/server";
|
||||
import { requireAuth } from "@/lib/auth/middleware";
|
||||
import { placeBid } from "@/lib/prisma/queries/listings";
|
||||
import { createNotification } from "@/lib/prisma/queries/notifications";
|
||||
import { secureLogger } from "@/lib/secure-logger";
|
||||
|
||||
/**
|
||||
* POST /api/listings/[id]/bid
|
||||
@@ -10,7 +10,7 @@ import { createNotification } from "@/lib/prisma/queries/notifications";
|
||||
*/
|
||||
export async function POST(
|
||||
request: NextRequest,
|
||||
{ params }: { params: Promise<{ id: string }> }
|
||||
{ params }: { params: Promise<{ id: string }> },
|
||||
) {
|
||||
try {
|
||||
const authResult = await requireAuth(request);
|
||||
@@ -19,10 +19,10 @@ export async function POST(
|
||||
|
||||
const { id } = await params;
|
||||
const listingId = parseInt(id, 10);
|
||||
if (isNaN(listingId)) {
|
||||
if (Number.isNaN(listingId)) {
|
||||
return NextResponse.json(
|
||||
{ success: false, error: "ID de anúncio inválido" },
|
||||
{ status: 400 }
|
||||
{ status: 400 },
|
||||
);
|
||||
}
|
||||
|
||||
@@ -31,34 +31,42 @@ export async function POST(
|
||||
|
||||
if (!amount || typeof amount !== "number" || amount <= 0) {
|
||||
return NextResponse.json(
|
||||
{ success: false, error: "Valor do lance é obrigatório e deve ser maior que zero" },
|
||||
{ status: 400 }
|
||||
{
|
||||
success: false,
|
||||
error: "Valor do lance é obrigatório e deve ser maior que zero",
|
||||
},
|
||||
{ status: 400 },
|
||||
);
|
||||
}
|
||||
|
||||
if (!payment_method || !["pix", "tc", "gp"].includes(payment_method)) {
|
||||
return NextResponse.json(
|
||||
{ success: false, error: "Método de pagamento inválido" },
|
||||
{ status: 400 }
|
||||
{ status: 400 },
|
||||
);
|
||||
}
|
||||
|
||||
// Colocar lance
|
||||
const result = await placeBid(listingId, auth.userId, amount, payment_method);
|
||||
const result = await placeBid(
|
||||
listingId,
|
||||
auth.userId,
|
||||
amount,
|
||||
payment_method,
|
||||
);
|
||||
|
||||
if (!result.success) {
|
||||
return NextResponse.json(
|
||||
{ success: false, error: result.error },
|
||||
{ status: 400 }
|
||||
{ status: 400 },
|
||||
);
|
||||
}
|
||||
|
||||
// Criar notificação para o vendedor
|
||||
await createNotification({
|
||||
user_id: result.listing!.seller_id,
|
||||
user_id: result.listing?.seller_id,
|
||||
notification_type: "bid",
|
||||
title: "Novo lance!",
|
||||
message: `Novo lance de R$ ${amount.toFixed(2)} no item "${result.listing!.item_name}"`,
|
||||
message: `Novo lance de R$ ${amount.toFixed(2)} no item "${result.listing?.item_name}"`,
|
||||
related_id: listingId,
|
||||
send_whatsapp: false,
|
||||
});
|
||||
@@ -69,7 +77,7 @@ export async function POST(
|
||||
user_id: result.previousBidderId,
|
||||
notification_type: "bid",
|
||||
title: "Lance superado",
|
||||
message: `Seu lance foi superado no item "${result.listing!.item_name}"`,
|
||||
message: `Seu lance foi superado no item "${result.listing?.item_name}"`,
|
||||
related_id: listingId,
|
||||
send_whatsapp: false,
|
||||
});
|
||||
@@ -84,8 +92,12 @@ export async function POST(
|
||||
metadata: {
|
||||
bid_amount: amount,
|
||||
payment_method: payment_method,
|
||||
previous_bid: result.listing!.current_bid_amount ? Number(result.listing!.current_bid_amount) : null,
|
||||
min_increment: result.listing!.min_bid_increment ? Number(result.listing!.min_bid_increment) : null,
|
||||
previous_bid: result.listing?.current_bid_amount
|
||||
? Number(result.listing?.current_bid_amount)
|
||||
: null,
|
||||
min_increment: result.listing?.min_bid_increment
|
||||
? Number(result.listing?.min_bid_increment)
|
||||
: null,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -102,7 +114,7 @@ export async function POST(
|
||||
listing: result.listing,
|
||||
message: "Lance realizado com sucesso",
|
||||
},
|
||||
{ status: 200 }
|
||||
{ status: 200 },
|
||||
);
|
||||
} catch (error) {
|
||||
secureLogger.error("❌ Erro ao processar lance", {
|
||||
@@ -112,8 +124,7 @@ export async function POST(
|
||||
|
||||
return NextResponse.json(
|
||||
{ success: false, error: "Erro ao processar lance" },
|
||||
{ status: 500 }
|
||||
{ status: 500 },
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { secureLogger } from "@/lib/secure-logger";
|
||||
import { type NextRequest, NextResponse } from "next/server";
|
||||
import { requireAuth } from "@/lib/auth/middleware";
|
||||
import { getListingById } from "@/lib/prisma/queries/listings";
|
||||
import { prisma } from "@/lib/prisma/app";
|
||||
import { getListingById } from "@/lib/prisma/queries/listings";
|
||||
import { trackEvent } from "@/lib/prisma/queries/telemetry";
|
||||
import { secureLogger } from "@/lib/secure-logger";
|
||||
|
||||
/**
|
||||
* GET /api/listings/[id]/bids
|
||||
@@ -11,7 +11,7 @@ import { trackEvent } from "@/lib/prisma/queries/telemetry";
|
||||
*/
|
||||
export async function GET(
|
||||
request: NextRequest,
|
||||
{ params }: { params: Promise<{ id: string }> }
|
||||
{ params }: { params: Promise<{ id: string }> },
|
||||
) {
|
||||
try {
|
||||
const authResult = await requireAuth(request);
|
||||
@@ -19,10 +19,10 @@ export async function GET(
|
||||
|
||||
const { id } = await params;
|
||||
const listingId = parseInt(id, 10);
|
||||
if (isNaN(listingId)) {
|
||||
if (Number.isNaN(listingId)) {
|
||||
return NextResponse.json(
|
||||
{ success: false, error: "ID de anúncio inválido" },
|
||||
{ status: 400 }
|
||||
{ status: 400 },
|
||||
);
|
||||
}
|
||||
|
||||
@@ -31,14 +31,14 @@ export async function GET(
|
||||
if (!listing) {
|
||||
return NextResponse.json(
|
||||
{ success: false, error: "Anúncio não encontrado" },
|
||||
{ status: 404 }
|
||||
{ status: 404 },
|
||||
);
|
||||
}
|
||||
|
||||
if (!listing.is_auction) {
|
||||
return NextResponse.json(
|
||||
{ success: false, error: "Este anúncio não é um leilão" },
|
||||
{ status: 400 }
|
||||
{ status: 400 },
|
||||
);
|
||||
}
|
||||
|
||||
@@ -92,7 +92,7 @@ export async function GET(
|
||||
bids: formattedBids,
|
||||
total: formattedBids.length,
|
||||
},
|
||||
{ status: 200 }
|
||||
{ status: 200 },
|
||||
);
|
||||
} catch (error) {
|
||||
secureLogger.error("❌ Erro ao buscar histórico de lances", {
|
||||
@@ -102,8 +102,7 @@ export async function GET(
|
||||
|
||||
return NextResponse.json(
|
||||
{ success: false, error: "Erro ao buscar histórico de lances" },
|
||||
{ status: 500 }
|
||||
{ status: 500 },
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { secureLogger } from "@/lib/secure-logger";
|
||||
import { type NextRequest, NextResponse } from "next/server";
|
||||
import { requireAuth } from "@/lib/auth/middleware";
|
||||
import { getListingById } from "@/lib/prisma/queries/listings";
|
||||
import { isFavorite, toggleFavorite } from "@/lib/prisma/queries/favorites";
|
||||
import { getListingById } from "@/lib/prisma/queries/listings";
|
||||
import { secureLogger } from "@/lib/secure-logger";
|
||||
|
||||
/**
|
||||
* POST /api/listings/:id/favorite
|
||||
@@ -10,7 +10,7 @@ import { isFavorite, toggleFavorite } from "@/lib/prisma/queries/favorites";
|
||||
*/
|
||||
export async function POST(
|
||||
request: NextRequest,
|
||||
{ params }: { params: Promise<{ id: string }> }
|
||||
{ params }: { params: Promise<{ id: string }> },
|
||||
) {
|
||||
try {
|
||||
// Autenticação obrigatória
|
||||
@@ -25,13 +25,13 @@ export async function POST(
|
||||
const { id } = await params;
|
||||
const listingId = parseInt(id, 10);
|
||||
|
||||
if (isNaN(listingId)) {
|
||||
if (Number.isNaN(listingId)) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: "ID de anúncio inválido",
|
||||
},
|
||||
{ status: 400 }
|
||||
{ status: 400 },
|
||||
);
|
||||
}
|
||||
|
||||
@@ -44,17 +44,20 @@ export async function POST(
|
||||
success: false,
|
||||
error: "Anúncio não encontrado",
|
||||
},
|
||||
{ status: 404 }
|
||||
{ status: 404 },
|
||||
);
|
||||
}
|
||||
|
||||
// Toggle favorite
|
||||
const favorited = await toggleFavorite(auth.userId, listingId);
|
||||
|
||||
secureLogger.info(`✅ Anúncio ${favorited ? "favoritado" : "desfavoritado"}`, {
|
||||
listingId,
|
||||
userId: auth.userId,
|
||||
});
|
||||
secureLogger.info(
|
||||
`✅ Anúncio ${favorited ? "favoritado" : "desfavoritado"}`,
|
||||
{
|
||||
listingId,
|
||||
userId: auth.userId,
|
||||
},
|
||||
);
|
||||
|
||||
return NextResponse.json(
|
||||
{
|
||||
@@ -62,7 +65,7 @@ export async function POST(
|
||||
favorited,
|
||||
message: favorited ? "Anúncio favoritado" : "Anúncio desfavoritado",
|
||||
},
|
||||
{ status: 200 }
|
||||
{ status: 200 },
|
||||
);
|
||||
} catch (error) {
|
||||
secureLogger.error("❌ Erro ao favoritar/desfavoritar anúncio", {
|
||||
@@ -75,7 +78,7 @@ export async function POST(
|
||||
success: false,
|
||||
error: "Erro ao favoritar/desfavoritar anúncio",
|
||||
},
|
||||
{ status: 500 }
|
||||
{ status: 500 },
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -86,7 +89,7 @@ export async function POST(
|
||||
*/
|
||||
export async function GET(
|
||||
request: NextRequest,
|
||||
{ params }: { params: Promise<{ id: string }> }
|
||||
{ params }: { params: Promise<{ id: string }> },
|
||||
) {
|
||||
try {
|
||||
// Autenticação obrigatória
|
||||
@@ -101,13 +104,13 @@ export async function GET(
|
||||
const { id } = await params;
|
||||
const listingId = parseInt(id, 10);
|
||||
|
||||
if (isNaN(listingId)) {
|
||||
if (Number.isNaN(listingId)) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: "ID de anúncio inválido",
|
||||
},
|
||||
{ status: 400 }
|
||||
{ status: 400 },
|
||||
);
|
||||
}
|
||||
|
||||
@@ -118,7 +121,7 @@ export async function GET(
|
||||
success: true,
|
||||
favorited,
|
||||
},
|
||||
{ status: 200 }
|
||||
{ status: 200 },
|
||||
);
|
||||
} catch (error) {
|
||||
secureLogger.error("❌ Erro ao verificar favorito", {
|
||||
@@ -130,8 +133,7 @@ export async function GET(
|
||||
success: false,
|
||||
error: "Erro ao verificar favorito",
|
||||
},
|
||||
{ status: 500 }
|
||||
{ status: 500 },
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { secureLogger } from "@/lib/secure-logger";
|
||||
import { type NextRequest, NextResponse } from "next/server";
|
||||
import { requireAuth } from "@/lib/auth/middleware";
|
||||
import { connectMongoDB } from "@/lib/mongodb/connection";
|
||||
import { Chat } from "@/lib/mongodb/models/Chat";
|
||||
import { Message } from "@/lib/mongodb/models/Message";
|
||||
import { getMainCharacter } from "@/lib/prisma/queries/characters";
|
||||
import { getListingById } from "@/lib/prisma/queries/listings";
|
||||
import { createNotification } from "@/lib/prisma/queries/notifications";
|
||||
import {
|
||||
getPurchaseIntentionById,
|
||||
updatePurchaseIntentionStatus,
|
||||
} from "@/lib/prisma/queries/purchase-intentions";
|
||||
import { createNotification } from "@/lib/prisma/queries/notifications";
|
||||
import { connectMongoDB } from "@/lib/mongodb/connection";
|
||||
import { Chat } from "@/lib/mongodb/models/Chat";
|
||||
import { Message } from "@/lib/mongodb/models/Message";
|
||||
import { getUserById } from "@/lib/prisma/queries/users";
|
||||
import { getMainCharacter } from "@/lib/prisma/queries/characters";
|
||||
import { secureLogger } from "@/lib/secure-logger";
|
||||
|
||||
/**
|
||||
* GET /api/listings/[id]/intentions/[intentionId]
|
||||
@@ -19,7 +19,7 @@ import { getMainCharacter } from "@/lib/prisma/queries/characters";
|
||||
*/
|
||||
export async function GET(
|
||||
request: NextRequest,
|
||||
{ params }: { params: Promise<{ id: string; intentionId: string }> }
|
||||
{ params }: { params: Promise<{ id: string; intentionId: string }> },
|
||||
) {
|
||||
try {
|
||||
const authResult = await requireAuth(request);
|
||||
@@ -27,10 +27,10 @@ export async function GET(
|
||||
|
||||
const { intentionId: intentionIdParam } = await params;
|
||||
const intentionId = parseInt(intentionIdParam, 10);
|
||||
if (isNaN(intentionId)) {
|
||||
if (Number.isNaN(intentionId)) {
|
||||
return NextResponse.json(
|
||||
{ success: false, error: "ID de intenção inválido" },
|
||||
{ status: 400 }
|
||||
{ status: 400 },
|
||||
);
|
||||
}
|
||||
|
||||
@@ -38,15 +38,21 @@ export async function GET(
|
||||
if (!intention) {
|
||||
return NextResponse.json(
|
||||
{ success: false, error: "Intenção não encontrada" },
|
||||
{ status: 404 }
|
||||
{ status: 404 },
|
||||
);
|
||||
}
|
||||
|
||||
// Verificar permissões: vendedor ou comprador
|
||||
if (intention.listing?.seller_id !== authResult.userId && intention.buyer_id !== authResult.userId) {
|
||||
if (
|
||||
intention.listing?.seller_id !== authResult.userId &&
|
||||
intention.buyer_id !== authResult.userId
|
||||
) {
|
||||
return NextResponse.json(
|
||||
{ success: false, error: "Você não tem permissão para ver esta intenção" },
|
||||
{ status: 403 }
|
||||
{
|
||||
success: false,
|
||||
error: "Você não tem permissão para ver esta intenção",
|
||||
},
|
||||
{ status: 403 },
|
||||
);
|
||||
}
|
||||
|
||||
@@ -55,7 +61,7 @@ export async function GET(
|
||||
success: true,
|
||||
intention,
|
||||
},
|
||||
{ status: 200 }
|
||||
{ status: 200 },
|
||||
);
|
||||
} catch (error) {
|
||||
secureLogger.error("❌ Erro ao buscar intenção", {
|
||||
@@ -65,7 +71,7 @@ export async function GET(
|
||||
|
||||
return NextResponse.json(
|
||||
{ success: false, error: "Erro ao buscar intenção" },
|
||||
{ status: 500 }
|
||||
{ status: 500 },
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -76,7 +82,7 @@ export async function GET(
|
||||
*/
|
||||
export async function PATCH(
|
||||
request: NextRequest,
|
||||
{ params }: { params: Promise<{ id: string; intentionId: string }> }
|
||||
{ params }: { params: Promise<{ id: string; intentionId: string }> },
|
||||
) {
|
||||
try {
|
||||
const authResult = await requireAuth(request);
|
||||
@@ -84,10 +90,10 @@ export async function PATCH(
|
||||
|
||||
const { intentionId: intentionIdParam } = await params;
|
||||
const intentionId = parseInt(intentionIdParam, 10);
|
||||
if (isNaN(intentionId)) {
|
||||
if (Number.isNaN(intentionId)) {
|
||||
return NextResponse.json(
|
||||
{ success: false, error: "ID de intenção inválido" },
|
||||
{ status: 400 }
|
||||
{ status: 400 },
|
||||
);
|
||||
}
|
||||
|
||||
@@ -100,8 +106,11 @@ export async function PATCH(
|
||||
|
||||
if (!status || !["accepted", "rejected"].includes(status)) {
|
||||
return NextResponse.json(
|
||||
{ success: false, error: "Status inválido. Use 'accepted' ou 'rejected'" },
|
||||
{ status: 400 }
|
||||
{
|
||||
success: false,
|
||||
error: "Status inválido. Use 'accepted' ou 'rejected'",
|
||||
},
|
||||
{ status: 400 },
|
||||
);
|
||||
}
|
||||
|
||||
@@ -110,15 +119,18 @@ export async function PATCH(
|
||||
if (!intention) {
|
||||
return NextResponse.json(
|
||||
{ success: false, error: "Intenção não encontrada" },
|
||||
{ status: 404 }
|
||||
{ status: 404 },
|
||||
);
|
||||
}
|
||||
|
||||
// Verificar se o usuário é o vendedor
|
||||
if (intention.listing?.seller_id !== authResult.userId) {
|
||||
return NextResponse.json(
|
||||
{ success: false, error: "Apenas o vendedor pode aceitar ou rejeitar intenções" },
|
||||
{ status: 403 }
|
||||
{
|
||||
success: false,
|
||||
error: "Apenas o vendedor pode aceitar ou rejeitar intenções",
|
||||
},
|
||||
{ status: 403 },
|
||||
);
|
||||
}
|
||||
|
||||
@@ -126,16 +138,20 @@ export async function PATCH(
|
||||
if (intention.status !== "pending") {
|
||||
return NextResponse.json(
|
||||
{ success: false, error: `Esta intenção já foi ${intention.status}` },
|
||||
{ status: 400 }
|
||||
{ status: 400 },
|
||||
);
|
||||
}
|
||||
|
||||
// Verificar se o anúncio ainda está disponível (ativo ou em escrow)
|
||||
const listing = await getListingById(intention.listing_id);
|
||||
if (!listing || (listing.listing_status !== "active" && listing.listing_status !== "in_escrow")) {
|
||||
if (
|
||||
!listing ||
|
||||
(listing.listing_status !== "active" &&
|
||||
listing.listing_status !== "in_escrow")
|
||||
) {
|
||||
return NextResponse.json(
|
||||
{ success: false, error: "Anúncio não está mais disponível" },
|
||||
{ status: 400 }
|
||||
{ status: 400 },
|
||||
);
|
||||
}
|
||||
|
||||
@@ -143,7 +159,7 @@ export async function PATCH(
|
||||
const updatedIntention = await updatePurchaseIntentionStatus(
|
||||
intentionId,
|
||||
status,
|
||||
sellerResponse
|
||||
sellerResponse,
|
||||
);
|
||||
|
||||
await sendSellerResponseToChat({
|
||||
@@ -159,19 +175,23 @@ export async function PATCH(
|
||||
user_id: intention.buyer_id,
|
||||
notification_type: status === "accepted" ? "purchase" : "system",
|
||||
title: status === "accepted" ? "Intenção aceita!" : "Intenção rejeitada",
|
||||
message: status === "accepted"
|
||||
? `O vendedor aceitou sua intenção de compra do item "${intention.listing?.item_name}"`
|
||||
: `O vendedor rejeitou sua intenção de compra do item "${intention.listing?.item_name}"`,
|
||||
message:
|
||||
status === "accepted"
|
||||
? `O vendedor aceitou sua intenção de compra do item "${intention.listing?.item_name}"`
|
||||
: `O vendedor rejeitou sua intenção de compra do item "${intention.listing?.item_name}"`,
|
||||
related_id: intention.listing_id,
|
||||
send_whatsapp: false,
|
||||
});
|
||||
|
||||
secureLogger.info(`✅ Intenção ${status === "accepted" ? "aceita" : "rejeitada"}`, {
|
||||
userId: authResult.userId,
|
||||
intentionId,
|
||||
listingId: intention.listing_id,
|
||||
buyerId: intention.buyer_id,
|
||||
});
|
||||
secureLogger.info(
|
||||
`✅ Intenção ${status === "accepted" ? "aceita" : "rejeitada"}`,
|
||||
{
|
||||
userId: authResult.userId,
|
||||
intentionId,
|
||||
listingId: intention.listing_id,
|
||||
buyerId: intention.buyer_id,
|
||||
},
|
||||
);
|
||||
|
||||
return NextResponse.json(
|
||||
{
|
||||
@@ -179,7 +199,7 @@ export async function PATCH(
|
||||
intention: updatedIntention,
|
||||
message: `Intenção ${status === "accepted" ? "aceita" : "rejeitada"} com sucesso`,
|
||||
},
|
||||
{ status: 200 }
|
||||
{ status: 200 },
|
||||
);
|
||||
} catch (error) {
|
||||
secureLogger.error("❌ Erro ao atualizar intenção", {
|
||||
@@ -189,7 +209,7 @@ export async function PATCH(
|
||||
|
||||
return NextResponse.json(
|
||||
{ success: false, error: "Erro ao atualizar intenção" },
|
||||
{ status: 500 }
|
||||
{ status: 500 },
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -202,7 +222,10 @@ async function sendSellerResponseToChat({
|
||||
sellerId,
|
||||
}: {
|
||||
listing: Awaited<ReturnType<typeof getListingById>>;
|
||||
intention: Exclude<Awaited<ReturnType<typeof getPurchaseIntentionById>>, null>;
|
||||
intention: Exclude<
|
||||
Awaited<ReturnType<typeof getPurchaseIntentionById>>,
|
||||
null
|
||||
>;
|
||||
status: "accepted" | "rejected";
|
||||
sellerResponse?: string;
|
||||
sellerId: number;
|
||||
@@ -225,12 +248,13 @@ async function sendSellerResponseToChat({
|
||||
let chat = await Chat.findOne({ chat_id: chatId });
|
||||
|
||||
if (!chat) {
|
||||
const [buyer, seller, buyerCharacter, sellerCharacter] = await Promise.all([
|
||||
getUserById(intention.buyer_id),
|
||||
getUserById(listing.seller_id),
|
||||
getMainCharacter(intention.buyer_id),
|
||||
getMainCharacter(listing.seller_id),
|
||||
]);
|
||||
const [buyer, seller, buyerCharacter, sellerCharacter] =
|
||||
await Promise.all([
|
||||
getUserById(intention.buyer_id),
|
||||
getUserById(listing.seller_id),
|
||||
getMainCharacter(intention.buyer_id),
|
||||
getMainCharacter(listing.seller_id),
|
||||
]);
|
||||
|
||||
chat = new Chat({
|
||||
chat_id: chatId,
|
||||
@@ -284,4 +308,3 @@ async function sendSellerResponseToChat({
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,15 +1,14 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { secureLogger } from "@/lib/secure-logger";
|
||||
import { type NextRequest, NextResponse } from "next/server";
|
||||
import { requireAuth } from "@/lib/auth/middleware";
|
||||
import { getListingById } from "@/lib/prisma/queries/listings";
|
||||
import { createNotification } from "@/lib/prisma/queries/notifications";
|
||||
import {
|
||||
createPurchaseIntention,
|
||||
getPurchaseIntentionsByListing,
|
||||
hasPendingIntention,
|
||||
getPendingIntentionByBuyerAndListing,
|
||||
getPurchaseIntentionsByListing,
|
||||
} from "@/lib/prisma/queries/purchase-intentions";
|
||||
import { createNotification } from "@/lib/prisma/queries/notifications";
|
||||
import { trackEvent } from "@/lib/prisma/queries/telemetry";
|
||||
import { secureLogger } from "@/lib/secure-logger";
|
||||
|
||||
/**
|
||||
* POST /api/listings/[id]/intentions
|
||||
@@ -17,7 +16,7 @@ import { trackEvent } from "@/lib/prisma/queries/telemetry";
|
||||
*/
|
||||
export async function POST(
|
||||
request: NextRequest,
|
||||
{ params }: { params: Promise<{ id: string }> }
|
||||
{ params }: { params: Promise<{ id: string }> },
|
||||
) {
|
||||
try {
|
||||
const authResult = await requireAuth(request);
|
||||
@@ -26,10 +25,10 @@ export async function POST(
|
||||
|
||||
const { id } = await params;
|
||||
const listingId = parseInt(id, 10);
|
||||
if (isNaN(listingId)) {
|
||||
if (Number.isNaN(listingId)) {
|
||||
return NextResponse.json(
|
||||
{ success: false, error: "ID de anúncio inválido" },
|
||||
{ status: 400 }
|
||||
{ status: 400 },
|
||||
);
|
||||
}
|
||||
|
||||
@@ -41,10 +40,13 @@ export async function POST(
|
||||
custom_offer_currency,
|
||||
} = body;
|
||||
|
||||
if (!payment_method || !["pix", "tibia_coins", "gold_coins", "trade"].includes(payment_method)) {
|
||||
if (
|
||||
!payment_method ||
|
||||
!["pix", "tibia_coins", "gold_coins", "trade"].includes(payment_method)
|
||||
) {
|
||||
return NextResponse.json(
|
||||
{ success: false, error: "Método de pagamento inválido" },
|
||||
{ status: 400 }
|
||||
{ status: 400 },
|
||||
);
|
||||
}
|
||||
|
||||
@@ -53,15 +55,19 @@ export async function POST(
|
||||
if (!listing) {
|
||||
return NextResponse.json(
|
||||
{ success: false, error: "Anúncio não encontrado" },
|
||||
{ status: 404 }
|
||||
{ status: 404 },
|
||||
);
|
||||
}
|
||||
|
||||
// Validar se não é o vendedor
|
||||
if (listing.seller_id === auth.userId) {
|
||||
return NextResponse.json(
|
||||
{ success: false, error: "Você não pode criar uma intenção de compra para seu próprio anúncio" },
|
||||
{ status: 403 }
|
||||
{
|
||||
success: false,
|
||||
error:
|
||||
"Você não pode criar uma intenção de compra para seu próprio anúncio",
|
||||
},
|
||||
{ status: 403 },
|
||||
);
|
||||
}
|
||||
|
||||
@@ -69,15 +75,18 @@ export async function POST(
|
||||
if (listing.listing_status !== "active") {
|
||||
return NextResponse.json(
|
||||
{ success: false, error: "Anúncio não está disponível para compra" },
|
||||
{ status: 400 }
|
||||
{ status: 400 },
|
||||
);
|
||||
}
|
||||
|
||||
// Validar se é leilão (leilões não podem ter intenções, apenas lances)
|
||||
if (listing.is_auction) {
|
||||
return NextResponse.json(
|
||||
{ success: false, error: "Este item está em leilão. Use a funcionalidade de lance." },
|
||||
{ status: 400 }
|
||||
{
|
||||
success: false,
|
||||
error: "Este item está em leilão. Use a funcionalidade de lance.",
|
||||
},
|
||||
{ status: 400 },
|
||||
);
|
||||
}
|
||||
|
||||
@@ -95,7 +104,7 @@ export async function POST(
|
||||
error: `Método de pagamento "${payment_method}" não está disponível para este anúncio`,
|
||||
available_methods: availableMethods,
|
||||
},
|
||||
{ status: 400 }
|
||||
{ status: 400 },
|
||||
);
|
||||
}
|
||||
|
||||
@@ -107,9 +116,10 @@ export async function POST(
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: "Ofertas personalizadas só são permitidas para métodos monetários (PIX, Tibia Coins ou Gold Coins).",
|
||||
error:
|
||||
"Ofertas personalizadas só são permitidas para métodos monetários (PIX, Tibia Coins ou Gold Coins).",
|
||||
},
|
||||
{ status: 400 }
|
||||
{ status: 400 },
|
||||
);
|
||||
}
|
||||
|
||||
@@ -118,14 +128,16 @@ export async function POST(
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: "Valor da oferta personalizada deve ser numérico e maior que zero.",
|
||||
error:
|
||||
"Valor da oferta personalizada deve ser numérico e maior que zero.",
|
||||
},
|
||||
{ status: 400 }
|
||||
{ status: 400 },
|
||||
);
|
||||
}
|
||||
|
||||
normalizedOfferCurrency =
|
||||
custom_offer_currency && ["pix", "tibia_coins", "gold_coins"].includes(custom_offer_currency)
|
||||
custom_offer_currency &&
|
||||
["pix", "tibia_coins", "gold_coins"].includes(custom_offer_currency)
|
||||
? custom_offer_currency
|
||||
: payment_method;
|
||||
|
||||
@@ -136,7 +148,10 @@ export async function POST(
|
||||
}
|
||||
|
||||
// Verificar se já existe uma intenção pendente
|
||||
const existingIntention = await getPendingIntentionByBuyerAndListing(listingId, auth.userId);
|
||||
const existingIntention = await getPendingIntentionByBuyerAndListing(
|
||||
listingId,
|
||||
auth.userId,
|
||||
);
|
||||
if (existingIntention) {
|
||||
// Se já existe e ainda pode trocar o método
|
||||
if (existingIntention.payment_method_change_count < 1) {
|
||||
@@ -144,23 +159,27 @@ export async function POST(
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: "Você já possui uma intenção de compra pendente para este anúncio",
|
||||
error:
|
||||
"Você já possui uma intenção de compra pendente para este anúncio",
|
||||
existing_intention_id: existingIntention.intention_id,
|
||||
can_change_payment_method: true,
|
||||
payment_method_change_count: existingIntention.payment_method_change_count,
|
||||
message: "Use o endpoint PATCH /api/purchase-intentions/[intentionId]/payment-method para trocar o método de pagamento",
|
||||
payment_method_change_count:
|
||||
existingIntention.payment_method_change_count,
|
||||
message:
|
||||
"Use o endpoint PATCH /api/purchase-intentions/[intentionId]/payment-method para trocar o método de pagamento",
|
||||
},
|
||||
{ status: 400 }
|
||||
{ status: 400 },
|
||||
);
|
||||
} else {
|
||||
// Já atingiu o limite de trocas
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: "Você já possui uma intenção de compra pendente para este anúncio e atingiu o limite de trocas de método de pagamento",
|
||||
error:
|
||||
"Você já possui uma intenção de compra pendente para este anúncio e atingiu o limite de trocas de método de pagamento",
|
||||
existing_intention_id: existingIntention.intention_id,
|
||||
},
|
||||
{ status: 400 }
|
||||
{ status: 400 },
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -197,7 +216,9 @@ export async function POST(
|
||||
payment_method,
|
||||
listing_price_pix: listing.price_pix ? Number(listing.price_pix) : null,
|
||||
listing_price_tc: listing.price_tibia_coins || null,
|
||||
listing_price_gp: listing.price_gold_coins ? Number(listing.price_gold_coins) : null,
|
||||
listing_price_gp: listing.price_gold_coins
|
||||
? Number(listing.price_gold_coins)
|
||||
: null,
|
||||
custom_offer_amount: normalizedOfferValue,
|
||||
custom_offer_currency: normalizedOfferCurrency,
|
||||
},
|
||||
@@ -214,9 +235,10 @@ export async function POST(
|
||||
{
|
||||
success: true,
|
||||
intention,
|
||||
message: "Intenção de compra criada com sucesso. O vendedor será notificado.",
|
||||
message:
|
||||
"Intenção de compra criada com sucesso. O vendedor será notificado.",
|
||||
},
|
||||
{ status: 201 }
|
||||
{ status: 201 },
|
||||
);
|
||||
} catch (error) {
|
||||
secureLogger.error("❌ Erro ao criar intenção de compra", {
|
||||
@@ -226,7 +248,7 @@ export async function POST(
|
||||
|
||||
return NextResponse.json(
|
||||
{ success: false, error: "Erro ao criar intenção de compra" },
|
||||
{ status: 500 }
|
||||
{ status: 500 },
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -237,19 +259,19 @@ export async function POST(
|
||||
*/
|
||||
export async function GET(
|
||||
request: NextRequest,
|
||||
{ params }: { params: Promise<{ id: string }> }
|
||||
{ params }: { params: Promise<{ id: string }> },
|
||||
) {
|
||||
try {
|
||||
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)) {
|
||||
if (Number.isNaN(listingId)) {
|
||||
return NextResponse.json(
|
||||
{ success: false, error: "ID de anúncio inválido" },
|
||||
{ status: 400 }
|
||||
{ status: 400 },
|
||||
);
|
||||
}
|
||||
|
||||
@@ -258,15 +280,18 @@ export async function GET(
|
||||
if (!listing) {
|
||||
return NextResponse.json(
|
||||
{ success: false, error: "Anúncio não encontrado" },
|
||||
{ status: 404 }
|
||||
{ status: 404 },
|
||||
);
|
||||
}
|
||||
|
||||
// Apenas o vendedor pode ver as intenções
|
||||
if (listing.seller_id !== auth.userId) {
|
||||
return NextResponse.json(
|
||||
{ success: false, error: "Você não tem permissão para ver as intenções deste anúncio" },
|
||||
{ status: 403 }
|
||||
{
|
||||
success: false,
|
||||
error: "Você não tem permissão para ver as intenções deste anúncio",
|
||||
},
|
||||
{ status: 403 },
|
||||
);
|
||||
}
|
||||
|
||||
@@ -297,7 +322,7 @@ export async function GET(
|
||||
intentions,
|
||||
count: intentions.length,
|
||||
},
|
||||
{ status: 200 }
|
||||
{ status: 200 },
|
||||
);
|
||||
} catch (error) {
|
||||
secureLogger.error("❌ Erro ao buscar intenções de compra", {
|
||||
@@ -307,8 +332,7 @@ export async function GET(
|
||||
|
||||
return NextResponse.json(
|
||||
{ success: false, error: "Erro ao buscar intenções de compra" },
|
||||
{ status: 500 }
|
||||
{ status: 500 },
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { secureLogger } from "@/lib/secure-logger";
|
||||
import { type NextRequest, NextResponse } from "next/server";
|
||||
import { requireAuth } from "@/lib/auth/middleware";
|
||||
import { getListingById, updateListing } from "@/lib/prisma/queries/listings";
|
||||
import { getListingById } from "@/lib/prisma/queries/listings";
|
||||
import { secureLogger } from "@/lib/secure-logger";
|
||||
|
||||
/**
|
||||
* POST /api/listings/:id/mark-sold
|
||||
@@ -9,7 +9,7 @@ import { getListingById, updateListing } from "@/lib/prisma/queries/listings";
|
||||
*/
|
||||
export async function POST(
|
||||
request: NextRequest,
|
||||
{ params }: { params: Promise<{ id: string }> }
|
||||
{ params }: { params: Promise<{ id: string }> },
|
||||
) {
|
||||
try {
|
||||
// Autenticação obrigatória
|
||||
@@ -24,13 +24,13 @@ export async function POST(
|
||||
const { id } = await params;
|
||||
const listingId = parseInt(id, 10);
|
||||
|
||||
if (isNaN(listingId)) {
|
||||
if (Number.isNaN(listingId)) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: "ID de anúncio inválido",
|
||||
},
|
||||
{ status: 400 }
|
||||
{ status: 400 },
|
||||
);
|
||||
}
|
||||
|
||||
@@ -43,7 +43,7 @@ export async function POST(
|
||||
success: false,
|
||||
error: "Anúncio não encontrado",
|
||||
},
|
||||
{ status: 404 }
|
||||
{ status: 404 },
|
||||
);
|
||||
}
|
||||
|
||||
@@ -53,18 +53,22 @@ export async function POST(
|
||||
success: false,
|
||||
error: "Você não tem permissão para marcar este anúncio como vendido",
|
||||
},
|
||||
{ status: 403 }
|
||||
{ status: 403 },
|
||||
);
|
||||
}
|
||||
|
||||
// Verificar se o anúncio pode ser marcado como vendido
|
||||
if (listing.listing_status !== "active" && listing.listing_status !== "in_escrow") {
|
||||
if (
|
||||
listing.listing_status !== "active" &&
|
||||
listing.listing_status !== "in_escrow"
|
||||
) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: "Apenas anúncios ativos ou em escrow podem ser marcados como vendidos",
|
||||
error:
|
||||
"Apenas anúncios ativos ou em escrow podem ser marcados como vendidos",
|
||||
},
|
||||
{ status: 400 }
|
||||
{ status: 400 },
|
||||
);
|
||||
}
|
||||
|
||||
@@ -90,12 +94,24 @@ export async function POST(
|
||||
// Converter campos para formato esperado
|
||||
const updated = {
|
||||
...updatedListing,
|
||||
price_pix: updatedListing.price_pix ? Number(updatedListing.price_pix) : null,
|
||||
current_bid_amount: updatedListing.current_bid_amount ? Number(updatedListing.current_bid_amount) : null,
|
||||
min_bid_increment: updatedListing.min_bid_increment ? Number(updatedListing.min_bid_increment) : null,
|
||||
initial_price_pix: updatedListing.initial_price_pix ? Number(updatedListing.initial_price_pix) : null,
|
||||
price_gold_coins: updatedListing.price_gold_coins ? Number(updatedListing.price_gold_coins) : null,
|
||||
initial_price_gold_coins: updatedListing.initial_price_gold_coins ? Number(updatedListing.initial_price_gold_coins) : null,
|
||||
price_pix: updatedListing.price_pix
|
||||
? Number(updatedListing.price_pix)
|
||||
: null,
|
||||
current_bid_amount: updatedListing.current_bid_amount
|
||||
? Number(updatedListing.current_bid_amount)
|
||||
: null,
|
||||
min_bid_increment: updatedListing.min_bid_increment
|
||||
? Number(updatedListing.min_bid_increment)
|
||||
: null,
|
||||
initial_price_pix: updatedListing.initial_price_pix
|
||||
? Number(updatedListing.initial_price_pix)
|
||||
: null,
|
||||
price_gold_coins: updatedListing.price_gold_coins
|
||||
? Number(updatedListing.price_gold_coins)
|
||||
: null,
|
||||
initial_price_gold_coins: updatedListing.initial_price_gold_coins
|
||||
? Number(updatedListing.initial_price_gold_coins)
|
||||
: null,
|
||||
seller_character_name: character?.character_name || null,
|
||||
};
|
||||
|
||||
@@ -105,7 +121,7 @@ export async function POST(
|
||||
success: false,
|
||||
error: "Erro ao marcar anúncio como vendido",
|
||||
},
|
||||
{ status: 500 }
|
||||
{ status: 500 },
|
||||
);
|
||||
}
|
||||
|
||||
@@ -120,7 +136,7 @@ export async function POST(
|
||||
listing: updated,
|
||||
message: "Anúncio marcado como vendido com sucesso",
|
||||
},
|
||||
{ status: 200 }
|
||||
{ status: 200 },
|
||||
);
|
||||
} catch (error) {
|
||||
secureLogger.error("❌ Erro ao marcar anúncio como vendido", {
|
||||
@@ -132,8 +148,7 @@ export async function POST(
|
||||
success: false,
|
||||
error: "Erro ao marcar anúncio como vendido",
|
||||
},
|
||||
{ status: 500 }
|
||||
{ status: 500 },
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { secureLogger } from "@/lib/secure-logger";
|
||||
import { type NextRequest, NextResponse } from "next/server";
|
||||
import { requireAuth } from "@/lib/auth/middleware";
|
||||
import { getListingById, updateListing } from "@/lib/prisma/queries/listings";
|
||||
import { secureLogger } from "@/lib/secure-logger";
|
||||
|
||||
/**
|
||||
* POST /api/listings/:id/pause
|
||||
@@ -9,7 +9,7 @@ import { getListingById, updateListing } from "@/lib/prisma/queries/listings";
|
||||
*/
|
||||
export async function POST(
|
||||
request: NextRequest,
|
||||
{ params }: { params: Promise<{ id: string }> }
|
||||
{ params }: { params: Promise<{ id: string }> },
|
||||
) {
|
||||
try {
|
||||
// Autenticação obrigatória
|
||||
@@ -24,13 +24,13 @@ export async function POST(
|
||||
const { id } = await params;
|
||||
const listingId = parseInt(id, 10);
|
||||
|
||||
if (isNaN(listingId)) {
|
||||
if (Number.isNaN(listingId)) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: "ID de anúncio inválido",
|
||||
},
|
||||
{ status: 400 }
|
||||
{ status: 400 },
|
||||
);
|
||||
}
|
||||
|
||||
@@ -43,7 +43,7 @@ export async function POST(
|
||||
success: false,
|
||||
error: "Anúncio não encontrado",
|
||||
},
|
||||
{ status: 404 }
|
||||
{ status: 404 },
|
||||
);
|
||||
}
|
||||
|
||||
@@ -53,7 +53,7 @@ export async function POST(
|
||||
success: false,
|
||||
error: "Você não tem permissão para pausar este anúncio",
|
||||
},
|
||||
{ status: 403 }
|
||||
{ status: 403 },
|
||||
);
|
||||
}
|
||||
|
||||
@@ -63,7 +63,7 @@ export async function POST(
|
||||
success: false,
|
||||
error: "Apenas anúncios ativos podem ser pausados",
|
||||
},
|
||||
{ status: 400 }
|
||||
{ status: 400 },
|
||||
);
|
||||
}
|
||||
|
||||
@@ -79,7 +79,7 @@ export async function POST(
|
||||
success: false,
|
||||
error: "Erro ao pausar anúncio",
|
||||
},
|
||||
{ status: 500 }
|
||||
{ status: 500 },
|
||||
);
|
||||
}
|
||||
|
||||
@@ -94,7 +94,7 @@ export async function POST(
|
||||
listing: updated,
|
||||
message: "Anúncio pausado com sucesso",
|
||||
},
|
||||
{ status: 200 }
|
||||
{ status: 200 },
|
||||
);
|
||||
} catch (error) {
|
||||
secureLogger.error("❌ Erro ao pausar anúncio", {
|
||||
@@ -106,8 +106,7 @@ export async function POST(
|
||||
success: false,
|
||||
error: "Erro ao pausar anúncio",
|
||||
},
|
||||
{ status: 500 }
|
||||
{ status: 500 },
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,14 +1,13 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { secureLogger } from "@/lib/secure-logger";
|
||||
import { type NextRequest, NextResponse } from "next/server";
|
||||
import { requireAuth } from "@/lib/auth/middleware";
|
||||
import { getListingById } from "@/lib/prisma/queries/listings";
|
||||
import { createNotification } from "@/lib/prisma/queries/notifications";
|
||||
import {
|
||||
createPurchaseIntention,
|
||||
hasPendingIntention,
|
||||
getPendingIntentionByBuyerAndListing,
|
||||
} from "@/lib/prisma/queries/purchase-intentions";
|
||||
import { trackEvent } from "@/lib/prisma/queries/telemetry";
|
||||
import { secureLogger } from "@/lib/secure-logger";
|
||||
|
||||
/**
|
||||
* POST /api/listings/[id]/purchase
|
||||
@@ -16,7 +15,7 @@ import { trackEvent } from "@/lib/prisma/queries/telemetry";
|
||||
*/
|
||||
export async function POST(
|
||||
request: NextRequest,
|
||||
{ params }: { params: Promise<{ id: string }> }
|
||||
{ params }: { params: Promise<{ id: string }> },
|
||||
) {
|
||||
try {
|
||||
const authResult = await requireAuth(request);
|
||||
@@ -25,10 +24,10 @@ export async function POST(
|
||||
|
||||
const { id } = await params;
|
||||
const listingId = parseInt(id, 10);
|
||||
if (isNaN(listingId)) {
|
||||
if (Number.isNaN(listingId)) {
|
||||
return NextResponse.json(
|
||||
{ success: false, error: "ID de anúncio inválido" },
|
||||
{ status: 400 }
|
||||
{ status: 400 },
|
||||
);
|
||||
}
|
||||
|
||||
@@ -43,7 +42,7 @@ export async function POST(
|
||||
if (!payment_method) {
|
||||
return NextResponse.json(
|
||||
{ success: false, error: "Método de pagamento é obrigatório" },
|
||||
{ status: 400 }
|
||||
{ status: 400 },
|
||||
);
|
||||
}
|
||||
|
||||
@@ -52,7 +51,7 @@ export async function POST(
|
||||
if (!listing) {
|
||||
return NextResponse.json(
|
||||
{ success: false, error: "Anúncio não encontrado" },
|
||||
{ status: 404 }
|
||||
{ status: 404 },
|
||||
);
|
||||
}
|
||||
|
||||
@@ -60,7 +59,7 @@ export async function POST(
|
||||
if (listing.seller_id === auth.userId) {
|
||||
return NextResponse.json(
|
||||
{ success: false, error: "Você não pode comprar seu próprio anúncio" },
|
||||
{ status: 403 }
|
||||
{ status: 403 },
|
||||
);
|
||||
}
|
||||
|
||||
@@ -68,15 +67,18 @@ export async function POST(
|
||||
if (listing.listing_status !== "active") {
|
||||
return NextResponse.json(
|
||||
{ success: false, error: "Anúncio não está disponível para compra" },
|
||||
{ status: 400 }
|
||||
{ status: 400 },
|
||||
);
|
||||
}
|
||||
|
||||
// Validar se é leilão (leilões não podem ser comprados diretamente, apenas com lances)
|
||||
if (listing.is_auction) {
|
||||
return NextResponse.json(
|
||||
{ success: false, error: "Este item está em leilão. Use a funcionalidade de lance." },
|
||||
{ status: 400 }
|
||||
{
|
||||
success: false,
|
||||
error: "Este item está em leilão. Use a funcionalidade de lance.",
|
||||
},
|
||||
{ status: 400 },
|
||||
);
|
||||
}
|
||||
|
||||
@@ -99,7 +101,7 @@ export async function POST(
|
||||
error: `Método de pagamento "${payment_method}" não está disponível para este anúncio`,
|
||||
available_methods: availableMethods,
|
||||
},
|
||||
{ status: 400 }
|
||||
{ status: 400 },
|
||||
);
|
||||
}
|
||||
|
||||
@@ -111,9 +113,10 @@ export async function POST(
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: "Ofertas personalizadas só são permitidas para métodos monetários (PIX, Tibia Coins ou Gold Coins).",
|
||||
error:
|
||||
"Ofertas personalizadas só são permitidas para métodos monetários (PIX, Tibia Coins ou Gold Coins).",
|
||||
},
|
||||
{ status: 400 }
|
||||
{ status: 400 },
|
||||
);
|
||||
}
|
||||
|
||||
@@ -122,14 +125,16 @@ export async function POST(
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: "Valor da oferta personalizada deve ser numérico e maior que zero.",
|
||||
error:
|
||||
"Valor da oferta personalizada deve ser numérico e maior que zero.",
|
||||
},
|
||||
{ status: 400 }
|
||||
{ status: 400 },
|
||||
);
|
||||
}
|
||||
|
||||
normalizedOfferCurrency =
|
||||
custom_offer_currency && ["pix", "tibia_coins", "gold_coins"].includes(custom_offer_currency)
|
||||
custom_offer_currency &&
|
||||
["pix", "tibia_coins", "gold_coins"].includes(custom_offer_currency)
|
||||
? custom_offer_currency
|
||||
: normalizedMethod;
|
||||
|
||||
@@ -140,7 +145,10 @@ export async function POST(
|
||||
}
|
||||
|
||||
// Verificar se já existe uma intenção pendente
|
||||
const existingIntention = await getPendingIntentionByBuyerAndListing(listingId, auth.userId);
|
||||
const existingIntention = await getPendingIntentionByBuyerAndListing(
|
||||
listingId,
|
||||
auth.userId,
|
||||
);
|
||||
if (existingIntention) {
|
||||
// Se já existe e ainda pode trocar o método
|
||||
if (existingIntention.payment_method_change_count < 1) {
|
||||
@@ -148,23 +156,27 @@ export async function POST(
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: "Você já possui uma intenção de compra pendente para este anúncio",
|
||||
error:
|
||||
"Você já possui uma intenção de compra pendente para este anúncio",
|
||||
existing_intention_id: existingIntention.intention_id,
|
||||
can_change_payment_method: true,
|
||||
payment_method_change_count: existingIntention.payment_method_change_count,
|
||||
message: "Use o endpoint PATCH /api/purchase-intentions/[intentionId]/payment-method para trocar o método de pagamento",
|
||||
payment_method_change_count:
|
||||
existingIntention.payment_method_change_count,
|
||||
message:
|
||||
"Use o endpoint PATCH /api/purchase-intentions/[intentionId]/payment-method para trocar o método de pagamento",
|
||||
},
|
||||
{ status: 400 }
|
||||
{ status: 400 },
|
||||
);
|
||||
} else {
|
||||
// Já atingiu o limite de trocas
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: "Você já possui uma intenção de compra pendente para este anúncio e atingiu o limite de trocas de método de pagamento",
|
||||
error:
|
||||
"Você já possui uma intenção de compra pendente para este anúncio e atingiu o limite de trocas de método de pagamento",
|
||||
existing_intention_id: existingIntention.intention_id,
|
||||
},
|
||||
{ status: 400 }
|
||||
{ status: 400 },
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -201,7 +213,9 @@ export async function POST(
|
||||
payment_method: normalizedMethod,
|
||||
listing_price_pix: listing.price_pix ? Number(listing.price_pix) : null,
|
||||
listing_price_tc: listing.price_tibia_coins || null,
|
||||
listing_price_gp: listing.price_gold_coins ? Number(listing.price_gold_coins) : null,
|
||||
listing_price_gp: listing.price_gold_coins
|
||||
? Number(listing.price_gold_coins)
|
||||
: null,
|
||||
custom_offer_amount: normalizedOfferValue,
|
||||
custom_offer_currency: normalizedOfferCurrency,
|
||||
},
|
||||
@@ -218,9 +232,10 @@ export async function POST(
|
||||
{
|
||||
success: true,
|
||||
intention,
|
||||
message: "Intenção de compra criada com sucesso. O vendedor será notificado.",
|
||||
message:
|
||||
"Intenção de compra criada com sucesso. O vendedor será notificado.",
|
||||
},
|
||||
{ status: 201 }
|
||||
{ status: 201 },
|
||||
);
|
||||
} catch (error) {
|
||||
secureLogger.error("❌ Erro ao criar intenção de compra", {
|
||||
@@ -230,8 +245,7 @@ export async function POST(
|
||||
|
||||
return NextResponse.json(
|
||||
{ success: false, error: "Erro ao criar intenção de compra" },
|
||||
{ status: 500 }
|
||||
{ status: 500 },
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { secureLogger } from "@/lib/secure-logger";
|
||||
import { requireAuth, optionalAuth } from "@/lib/auth/middleware";
|
||||
import { type NextRequest, NextResponse } from "next/server";
|
||||
import { optionalAuth, requireAuth } from "@/lib/auth/middleware";
|
||||
import {
|
||||
deleteListing,
|
||||
getListingById,
|
||||
updateListing,
|
||||
deleteListing,
|
||||
} 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";
|
||||
|
||||
/**
|
||||
@@ -15,20 +15,20 @@ import { updateListingSchema } from "@/lib/validation-schemas";
|
||||
*/
|
||||
export async function GET(
|
||||
request: NextRequest,
|
||||
{ params }: { params: Promise<{ id: string }> }
|
||||
{ params }: { params: Promise<{ id: string }> },
|
||||
) {
|
||||
try {
|
||||
const auth = await optionalAuth(request);
|
||||
const { id } = await params;
|
||||
const listingId = parseInt(id, 10);
|
||||
|
||||
if (isNaN(listingId)) {
|
||||
if (Number.isNaN(listingId)) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: "ID de anúncio inválido",
|
||||
},
|
||||
{ status: 400 }
|
||||
{ status: 400 },
|
||||
);
|
||||
}
|
||||
|
||||
@@ -40,14 +40,13 @@ export async function GET(
|
||||
success: false,
|
||||
error: "Anúncio não encontrado",
|
||||
},
|
||||
{ status: 404 }
|
||||
{ status: 404 },
|
||||
);
|
||||
}
|
||||
|
||||
const hasPendingForUser =
|
||||
auth && auth.userId
|
||||
? await hasPendingIntention(listingId, auth.userId)
|
||||
: false;
|
||||
const hasPendingForUser = auth?.userId
|
||||
? await hasPendingIntention(listingId, auth.userId)
|
||||
: false;
|
||||
|
||||
return NextResponse.json(
|
||||
{
|
||||
@@ -57,7 +56,7 @@ export async function GET(
|
||||
has_pending_intention_for_current_user: hasPendingForUser,
|
||||
},
|
||||
},
|
||||
{ status: 200 }
|
||||
{ status: 200 },
|
||||
);
|
||||
} catch (error) {
|
||||
secureLogger.error("❌ Erro ao buscar anúncio", {
|
||||
@@ -70,7 +69,7 @@ export async function GET(
|
||||
success: false,
|
||||
error: "Erro ao buscar anúncio",
|
||||
},
|
||||
{ status: 500 }
|
||||
{ status: 500 },
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -81,7 +80,7 @@ export async function GET(
|
||||
*/
|
||||
export async function PATCH(
|
||||
request: NextRequest,
|
||||
{ params }: { params: Promise<{ id: string }> }
|
||||
{ params }: { params: Promise<{ id: string }> },
|
||||
) {
|
||||
try {
|
||||
// Autenticação obrigatória
|
||||
@@ -96,13 +95,13 @@ export async function PATCH(
|
||||
const { id } = await params;
|
||||
const listingId = parseInt(id, 10);
|
||||
|
||||
if (isNaN(listingId)) {
|
||||
if (Number.isNaN(listingId)) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: "ID de anúncio inválido",
|
||||
},
|
||||
{ status: 400 }
|
||||
{ status: 400 },
|
||||
);
|
||||
}
|
||||
|
||||
@@ -115,7 +114,7 @@ export async function PATCH(
|
||||
success: false,
|
||||
error: "Anúncio não encontrado",
|
||||
},
|
||||
{ status: 404 }
|
||||
{ status: 404 },
|
||||
);
|
||||
}
|
||||
|
||||
@@ -125,7 +124,7 @@ export async function PATCH(
|
||||
success: false,
|
||||
error: "Você não tem permissão para editar este anúncio",
|
||||
},
|
||||
{ status: 403 }
|
||||
{ status: 403 },
|
||||
);
|
||||
}
|
||||
|
||||
@@ -146,7 +145,7 @@ export async function PATCH(
|
||||
error: "Dados de entrada inválidos",
|
||||
details: errors,
|
||||
},
|
||||
{ status: 400 }
|
||||
{ status: 400 },
|
||||
);
|
||||
}
|
||||
|
||||
@@ -159,16 +158,37 @@ export async function PATCH(
|
||||
price_tibia_coins?: number;
|
||||
price_gold_coins?: number;
|
||||
description?: string;
|
||||
listing_status?: "active" | "sold" | "cancelled" | "in_escrow" | "expired";
|
||||
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 }),
|
||||
...(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
|
||||
@@ -180,7 +200,7 @@ export async function PATCH(
|
||||
success: false,
|
||||
error: "Erro ao atualizar anúncio",
|
||||
},
|
||||
{ status: 500 }
|
||||
{ status: 500 },
|
||||
);
|
||||
}
|
||||
|
||||
@@ -195,7 +215,7 @@ export async function PATCH(
|
||||
success: true,
|
||||
listing: updatedListing,
|
||||
},
|
||||
{ status: 200 }
|
||||
{ status: 200 },
|
||||
);
|
||||
} catch (error) {
|
||||
secureLogger.error("❌ Erro ao atualizar anúncio", {
|
||||
@@ -208,7 +228,7 @@ export async function PATCH(
|
||||
success: false,
|
||||
error: "Erro ao atualizar anúncio",
|
||||
},
|
||||
{ status: 500 }
|
||||
{ status: 500 },
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -219,7 +239,7 @@ export async function PATCH(
|
||||
*/
|
||||
export async function DELETE(
|
||||
request: NextRequest,
|
||||
{ params }: { params: Promise<{ id: string }> }
|
||||
{ params }: { params: Promise<{ id: string }> },
|
||||
) {
|
||||
try {
|
||||
// Autenticação obrigatória
|
||||
@@ -234,13 +254,13 @@ export async function DELETE(
|
||||
const { id } = await params;
|
||||
const listingId = parseInt(id, 10);
|
||||
|
||||
if (isNaN(listingId)) {
|
||||
if (Number.isNaN(listingId)) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: "ID de anúncio inválido",
|
||||
},
|
||||
{ status: 400 }
|
||||
{ status: 400 },
|
||||
);
|
||||
}
|
||||
|
||||
@@ -253,7 +273,7 @@ export async function DELETE(
|
||||
success: false,
|
||||
error: "Anúncio não encontrado",
|
||||
},
|
||||
{ status: 404 }
|
||||
{ status: 404 },
|
||||
);
|
||||
}
|
||||
|
||||
@@ -263,7 +283,7 @@ export async function DELETE(
|
||||
success: false,
|
||||
error: "Você não tem permissão para cancelar este anúncio",
|
||||
},
|
||||
{ status: 403 }
|
||||
{ status: 403 },
|
||||
);
|
||||
}
|
||||
|
||||
@@ -276,7 +296,7 @@ export async function DELETE(
|
||||
success: false,
|
||||
error: "Erro ao cancelar anúncio",
|
||||
},
|
||||
{ status: 500 }
|
||||
{ status: 500 },
|
||||
);
|
||||
}
|
||||
|
||||
@@ -291,7 +311,7 @@ export async function DELETE(
|
||||
success: true,
|
||||
message: "Anúncio cancelado com sucesso",
|
||||
},
|
||||
{ status: 200 }
|
||||
{ status: 200 },
|
||||
);
|
||||
} catch (error) {
|
||||
secureLogger.error("❌ Erro ao cancelar anúncio", {
|
||||
@@ -304,8 +324,7 @@ export async function DELETE(
|
||||
success: false,
|
||||
error: "Erro ao cancelar anúncio",
|
||||
},
|
||||
{ status: 500 }
|
||||
{ status: 500 },
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,23 +1,23 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { secureLogger } from "@/lib/secure-logger";
|
||||
import { getListingById } from "@/lib/prisma/queries/listings";
|
||||
import { type NextRequest, NextResponse } from "next/server";
|
||||
import { getMainCharacterByUserId } from "@/lib/prisma/queries/characters";
|
||||
import { getListingById } from "@/lib/prisma/queries/listings";
|
||||
import { secureLogger } from "@/lib/secure-logger";
|
||||
|
||||
/**
|
||||
* GET /api/listings/[id]/seller
|
||||
* Retorna informações do vendedor (nome do personagem)
|
||||
*/
|
||||
export async function GET(
|
||||
request: NextRequest,
|
||||
{ params }: { params: Promise<{ id: string }> }
|
||||
_request: NextRequest,
|
||||
{ params }: { params: Promise<{ id: string }> },
|
||||
) {
|
||||
try {
|
||||
const { id } = await params;
|
||||
const listingId = parseInt(id, 10);
|
||||
if (isNaN(listingId)) {
|
||||
if (Number.isNaN(listingId)) {
|
||||
return NextResponse.json(
|
||||
{ success: false, error: "ID de anúncio inválido" },
|
||||
{ status: 400 }
|
||||
{ status: 400 },
|
||||
);
|
||||
}
|
||||
|
||||
@@ -25,12 +25,15 @@ export async function GET(
|
||||
if (!listing) {
|
||||
return NextResponse.json(
|
||||
{ success: false, error: "Anúncio não encontrado" },
|
||||
{ status: 404 }
|
||||
{ status: 404 },
|
||||
);
|
||||
}
|
||||
|
||||
// Buscar personagem principal do vendedor no servidor do listing
|
||||
const character = await getMainCharacterByUserId(listing.seller_id, listing.server_name);
|
||||
const character = await getMainCharacterByUserId(
|
||||
listing.seller_id,
|
||||
listing.server_name,
|
||||
);
|
||||
|
||||
return NextResponse.json(
|
||||
{
|
||||
@@ -41,7 +44,7 @@ export async function GET(
|
||||
server_name: listing.server_name,
|
||||
},
|
||||
},
|
||||
{ status: 200 }
|
||||
{ status: 200 },
|
||||
);
|
||||
} catch (error) {
|
||||
secureLogger.error("❌ Erro ao buscar informações do vendedor", {
|
||||
@@ -51,8 +54,7 @@ export async function GET(
|
||||
|
||||
return NextResponse.json(
|
||||
{ success: false, error: "Erro ao buscar informações do vendedor" },
|
||||
{ status: 500 }
|
||||
{ status: 500 },
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { secureLogger } from "@/lib/secure-logger";
|
||||
import { type NextRequest, NextResponse } from "next/server";
|
||||
import { incrementViews } from "@/lib/prisma/queries/listings";
|
||||
import { secureLogger } from "@/lib/secure-logger";
|
||||
|
||||
/**
|
||||
* POST /api/listings/[id]/views
|
||||
@@ -8,20 +8,20 @@ import { incrementViews } from "@/lib/prisma/queries/listings";
|
||||
* Não requer autenticação (público)
|
||||
*/
|
||||
export async function POST(
|
||||
request: NextRequest,
|
||||
{ params }: { params: Promise<{ id: string }> }
|
||||
_request: NextRequest,
|
||||
{ params }: { params: Promise<{ id: string }> },
|
||||
) {
|
||||
try {
|
||||
const { id } = await params;
|
||||
const listingId = parseInt(id, 10);
|
||||
|
||||
if (isNaN(listingId)) {
|
||||
if (Number.isNaN(listingId)) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: "ID de anúncio inválido",
|
||||
},
|
||||
{ status: 400 }
|
||||
{ status: 400 },
|
||||
);
|
||||
}
|
||||
|
||||
@@ -33,7 +33,7 @@ export async function POST(
|
||||
success: true,
|
||||
message: "View incrementada",
|
||||
},
|
||||
{ status: 200 }
|
||||
{ status: 200 },
|
||||
);
|
||||
} catch (error) {
|
||||
secureLogger.error("❌ Erro ao incrementar views", {
|
||||
@@ -46,8 +46,7 @@ export async function POST(
|
||||
success: false,
|
||||
error: "Erro ao incrementar views",
|
||||
},
|
||||
{ status: 500 }
|
||||
{ status: 500 },
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { secureLogger } from "@/lib/secure-logger";
|
||||
import { type NextRequest, NextResponse } from "next/server";
|
||||
import { requireAuth } from "@/lib/auth/middleware";
|
||||
import { getListings } from "@/lib/prisma/queries/listings";
|
||||
import { secureLogger } from "@/lib/secure-logger";
|
||||
|
||||
/**
|
||||
* GET /api/listings/my
|
||||
@@ -35,8 +35,7 @@ export async function GET(request: NextRequest) {
|
||||
pageSize: searchParams.get("pageSize")
|
||||
? parseInt(searchParams.get("pageSize")!, 10)
|
||||
: 20,
|
||||
sortBy: (searchParams.get("sortBy") ||
|
||||
"created_at") as
|
||||
sortBy: (searchParams.get("sortBy") || "created_at") as
|
||||
| "created_at"
|
||||
| "price_pix"
|
||||
| "price_tibia_coins"
|
||||
@@ -58,7 +57,7 @@ export async function GET(request: NextRequest) {
|
||||
totalPages: Math.ceil(listingsData.total / filters.pageSize),
|
||||
},
|
||||
},
|
||||
{ status: 200 }
|
||||
{ status: 200 },
|
||||
);
|
||||
} catch (error) {
|
||||
secureLogger.error("❌ Erro ao buscar anúncios do usuário", {
|
||||
@@ -71,8 +70,7 @@ export async function GET(request: NextRequest) {
|
||||
success: false,
|
||||
error: "Erro ao buscar anúncios",
|
||||
},
|
||||
{ status: 500 }
|
||||
{ status: 500 },
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
+22
-22
@@ -1,19 +1,19 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
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,
|
||||
getClientIP,
|
||||
createRateLimitResponse,
|
||||
getClientIP,
|
||||
} 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/prisma/queries/listings";
|
||||
import { isServerActive } from "@/lib/prisma/queries/servers";
|
||||
import { getPendingIntentionListingIdsForBuyer } from "@/lib/prisma/queries/purchase-intentions";
|
||||
|
||||
/**
|
||||
* GET /api/listings
|
||||
@@ -52,8 +52,7 @@ export async function GET(request: NextRequest) {
|
||||
pageSize: searchParams.get("pageSize")
|
||||
? parseInt(searchParams.get("pageSize")!, 10)
|
||||
: 20,
|
||||
sortBy: (searchParams.get("sortBy") ||
|
||||
"created_at") as
|
||||
sortBy: (searchParams.get("sortBy") || "created_at") as
|
||||
| "created_at"
|
||||
| "price_pix"
|
||||
| "price_tibia_coins"
|
||||
@@ -73,13 +72,15 @@ export async function GET(request: NextRequest) {
|
||||
const listingIds = listingsResponse.map((listing) => listing.listing_id);
|
||||
const pendingListingIds = await getPendingIntentionListingIdsForBuyer(
|
||||
auth.userId,
|
||||
listingIds
|
||||
listingIds,
|
||||
);
|
||||
const pendingSet = new Set(pendingListingIds);
|
||||
|
||||
listingsResponse = listingsResponse.map((listing) => ({
|
||||
...listing,
|
||||
has_pending_intention_for_current_user: pendingSet.has(listing.listing_id),
|
||||
has_pending_intention_for_current_user: pendingSet.has(
|
||||
listing.listing_id,
|
||||
),
|
||||
}));
|
||||
}
|
||||
|
||||
@@ -94,7 +95,7 @@ export async function GET(request: NextRequest) {
|
||||
totalPages: Math.ceil(listingsData.total / filters.pageSize),
|
||||
},
|
||||
},
|
||||
{ status: 200 }
|
||||
{ status: 200 },
|
||||
);
|
||||
} catch (error) {
|
||||
secureLogger.error("❌ Erro ao buscar anúncios", {
|
||||
@@ -107,7 +108,7 @@ export async function GET(request: NextRequest) {
|
||||
success: false,
|
||||
error: "Erro ao buscar anúncios",
|
||||
},
|
||||
{ status: 500 }
|
||||
{ status: 500 },
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -158,7 +159,7 @@ export async function POST(request: NextRequest) {
|
||||
error: "Dados de entrada inválidos",
|
||||
details: errors,
|
||||
},
|
||||
{ status: 400 }
|
||||
{ status: 400 },
|
||||
);
|
||||
}
|
||||
|
||||
@@ -172,7 +173,7 @@ export async function POST(request: NextRequest) {
|
||||
success: false,
|
||||
error: `Servidor "${validatedData.server_name}" não existe ou não está ativo`,
|
||||
},
|
||||
{ status: 400 }
|
||||
{ status: 400 },
|
||||
);
|
||||
}
|
||||
|
||||
@@ -181,7 +182,7 @@ export async function POST(request: NextRequest) {
|
||||
// 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,
|
||||
@@ -210,7 +211,7 @@ export async function POST(request: NextRequest) {
|
||||
success: true,
|
||||
listing,
|
||||
},
|
||||
{ status: 201 }
|
||||
{ status: 201 },
|
||||
);
|
||||
} catch (error) {
|
||||
secureLogger.error("❌ Erro ao criar anúncio", {
|
||||
@@ -223,8 +224,7 @@ export async function POST(request: NextRequest) {
|
||||
success: false,
|
||||
error: "Erro ao criar anúncio",
|
||||
},
|
||||
{ status: 500 }
|
||||
{ status: 500 },
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { secureLogger } from "@/lib/secure-logger";
|
||||
import { type NextRequest, NextResponse } from "next/server";
|
||||
import { requireAuth } from "@/lib/auth/middleware";
|
||||
import { markAsRead } from "@/lib/prisma/queries/notifications";
|
||||
import { secureLogger } from "@/lib/secure-logger";
|
||||
|
||||
/**
|
||||
* POST /api/notifications/[id]/read
|
||||
@@ -9,7 +9,7 @@ import { markAsRead } from "@/lib/prisma/queries/notifications";
|
||||
*/
|
||||
export async function POST(
|
||||
request: NextRequest,
|
||||
{ params }: { params: Promise<{ id: string }> }
|
||||
{ params }: { params: Promise<{ id: string }> },
|
||||
) {
|
||||
try {
|
||||
const authResult = await requireAuth(request);
|
||||
@@ -18,10 +18,10 @@ export async function POST(
|
||||
|
||||
const { id } = await params;
|
||||
const notificationId = parseInt(id, 10);
|
||||
if (isNaN(notificationId)) {
|
||||
if (Number.isNaN(notificationId)) {
|
||||
return NextResponse.json(
|
||||
{ success: false, error: "ID de notificação inválido" },
|
||||
{ status: 400 }
|
||||
{ status: 400 },
|
||||
);
|
||||
}
|
||||
|
||||
@@ -30,7 +30,7 @@ export async function POST(
|
||||
if (!success) {
|
||||
return NextResponse.json(
|
||||
{ success: false, error: "Notificação não encontrada ou já está lida" },
|
||||
{ status: 404 }
|
||||
{ status: 404 },
|
||||
);
|
||||
}
|
||||
|
||||
@@ -39,7 +39,7 @@ export async function POST(
|
||||
success: true,
|
||||
message: "Notificação marcada como lida",
|
||||
},
|
||||
{ status: 200 }
|
||||
{ status: 200 },
|
||||
);
|
||||
} catch (error) {
|
||||
secureLogger.error("❌ Erro ao marcar notificação como lida", {
|
||||
@@ -48,8 +48,7 @@ export async function POST(
|
||||
|
||||
return NextResponse.json(
|
||||
{ success: false, error: "Erro ao marcar notificação como lida" },
|
||||
{ status: 500 }
|
||||
{ status: 500 },
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { secureLogger } from "@/lib/secure-logger";
|
||||
import { type NextRequest, NextResponse } from "next/server";
|
||||
import { requireAuth } from "@/lib/auth/middleware";
|
||||
import {
|
||||
getUserNotifications,
|
||||
getUnreadCount,
|
||||
markAsRead,
|
||||
getUserNotifications,
|
||||
markAllAsRead,
|
||||
} from "@/lib/prisma/queries/notifications";
|
||||
import { secureLogger } from "@/lib/secure-logger";
|
||||
|
||||
/**
|
||||
* GET /api/notifications
|
||||
@@ -25,7 +24,8 @@ export async function GET(request: NextRequest) {
|
||||
: 50;
|
||||
|
||||
const notifications = await getUserNotifications(auth.userId, {
|
||||
is_read: isRead === "true" ? true : isRead === "false" ? false : undefined,
|
||||
is_read:
|
||||
isRead === "true" ? true : isRead === "false" ? false : undefined,
|
||||
limit,
|
||||
});
|
||||
|
||||
@@ -37,7 +37,7 @@ export async function GET(request: NextRequest) {
|
||||
notifications,
|
||||
unread_count: unreadCount,
|
||||
},
|
||||
{ status: 200 }
|
||||
{ status: 200 },
|
||||
);
|
||||
} catch (error) {
|
||||
secureLogger.error("❌ Erro ao buscar notificações", {
|
||||
@@ -47,7 +47,7 @@ export async function GET(request: NextRequest) {
|
||||
|
||||
return NextResponse.json(
|
||||
{ success: false, error: "Erro ao buscar notificações" },
|
||||
{ status: 500 }
|
||||
{ status: 500 },
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -73,13 +73,13 @@ export async function POST(request: NextRequest) {
|
||||
message: `${count} notificações marcadas como lidas`,
|
||||
count,
|
||||
},
|
||||
{ status: 200 }
|
||||
{ status: 200 },
|
||||
);
|
||||
}
|
||||
|
||||
return NextResponse.json(
|
||||
{ success: false, error: "Ação inválida" },
|
||||
{ status: 400 }
|
||||
{ status: 400 },
|
||||
);
|
||||
} catch (error) {
|
||||
secureLogger.error("❌ Erro ao marcar notificações como lidas", {
|
||||
@@ -88,8 +88,7 @@ export async function POST(request: NextRequest) {
|
||||
|
||||
return NextResponse.json(
|
||||
{ success: false, error: "Erro ao marcar notificações como lidas" },
|
||||
{ status: 500 }
|
||||
{ status: 500 },
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { type NextRequest, NextResponse } from "next/server";
|
||||
import { requireAuth } from "@/lib/auth/middleware";
|
||||
import { getUnreadCount } from "@/lib/prisma/queries/notifications";
|
||||
|
||||
@@ -18,13 +18,12 @@ export async function GET(request: NextRequest) {
|
||||
success: true,
|
||||
count,
|
||||
},
|
||||
{ status: 200 }
|
||||
{ status: 200 },
|
||||
);
|
||||
} catch (error) {
|
||||
} catch (_error) {
|
||||
return NextResponse.json(
|
||||
{ success: false, error: "Erro ao buscar contagem de notificações" },
|
||||
{ status: 500 }
|
||||
{ status: 500 },
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,14 +2,14 @@
|
||||
* PATCH /api/purchase-intentions/[intentionId]/payment-method
|
||||
* Permite ao comprador trocar o método de pagamento (máximo 1 vez)
|
||||
*/
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { type NextRequest, NextResponse } from "next/server";
|
||||
import { requireAuth } from "@/lib/auth/middleware";
|
||||
import { createSystemMessageInChat } from "@/lib/chat/system-messages";
|
||||
import { getListingById } from "@/lib/prisma/queries/listings";
|
||||
import {
|
||||
getPurchaseIntentionById,
|
||||
updatePurchaseIntentionPaymentMethod,
|
||||
} from "@/lib/prisma/queries/purchase-intentions";
|
||||
import { getListingById } from "@/lib/prisma/queries/listings";
|
||||
import { createSystemMessageInChat } from "@/lib/chat/system-messages";
|
||||
import { secureLogger } from "@/lib/secure-logger";
|
||||
|
||||
function getPaymentMethodLabel(method: string): string {
|
||||
@@ -29,7 +29,7 @@ function getPaymentMethodLabel(method: string): string {
|
||||
|
||||
export async function PATCH(
|
||||
request: NextRequest,
|
||||
{ params }: { params: Promise<{ intentionId: string }> }
|
||||
{ params }: { params: Promise<{ intentionId: string }> },
|
||||
) {
|
||||
try {
|
||||
const authResult = await requireAuth(request);
|
||||
@@ -38,21 +38,24 @@ export async function PATCH(
|
||||
|
||||
const { intentionId: intentionIdParam } = await params;
|
||||
const intentionId = parseInt(intentionIdParam, 10);
|
||||
|
||||
if (isNaN(intentionId)) {
|
||||
|
||||
if (Number.isNaN(intentionId)) {
|
||||
return NextResponse.json(
|
||||
{ success: false, error: "ID de intenção inválido" },
|
||||
{ status: 400 }
|
||||
{ status: 400 },
|
||||
);
|
||||
}
|
||||
|
||||
const body = await request.json();
|
||||
const { payment_method } = body;
|
||||
|
||||
if (!payment_method || !["pix", "tibia_coins", "gold_coins", "trade"].includes(payment_method)) {
|
||||
if (
|
||||
!payment_method ||
|
||||
!["pix", "tibia_coins", "gold_coins", "trade"].includes(payment_method)
|
||||
) {
|
||||
return NextResponse.json(
|
||||
{ success: false, error: "Método de pagamento inválido" },
|
||||
{ status: 400 }
|
||||
{ status: 400 },
|
||||
);
|
||||
}
|
||||
|
||||
@@ -61,39 +64,53 @@ export async function PATCH(
|
||||
if (!intention) {
|
||||
return NextResponse.json(
|
||||
{ success: false, error: "Intenção não encontrada" },
|
||||
{ status: 404 }
|
||||
{ status: 404 },
|
||||
);
|
||||
}
|
||||
|
||||
// Verificar se o usuário é o comprador
|
||||
if (intention.buyer_id !== auth.userId) {
|
||||
return NextResponse.json(
|
||||
{ success: false, error: "Apenas o comprador pode trocar o método de pagamento" },
|
||||
{ status: 403 }
|
||||
{
|
||||
success: false,
|
||||
error: "Apenas o comprador pode trocar o método de pagamento",
|
||||
},
|
||||
{ status: 403 },
|
||||
);
|
||||
}
|
||||
|
||||
// Verificar se a intenção está pendente
|
||||
if (intention.status !== "pending") {
|
||||
return NextResponse.json(
|
||||
{ success: false, error: "Apenas intenções pendentes podem ter o método de pagamento alterado" },
|
||||
{ status: 400 }
|
||||
{
|
||||
success: false,
|
||||
error:
|
||||
"Apenas intenções pendentes podem ter o método de pagamento alterado",
|
||||
},
|
||||
{ status: 400 },
|
||||
);
|
||||
}
|
||||
|
||||
// Verificar se já atingiu o limite de trocas
|
||||
if (intention.payment_method_change_count >= 1) {
|
||||
return NextResponse.json(
|
||||
{ success: false, error: "Você já atingiu o limite de trocas de método de pagamento (1 vez)" },
|
||||
{ status: 400 }
|
||||
{
|
||||
success: false,
|
||||
error:
|
||||
"Você já atingiu o limite de trocas de método de pagamento (1 vez)",
|
||||
},
|
||||
{ status: 400 },
|
||||
);
|
||||
}
|
||||
|
||||
// Verificar se o novo método é diferente
|
||||
if (intention.payment_method === payment_method) {
|
||||
return NextResponse.json(
|
||||
{ success: false, error: "O novo método de pagamento deve ser diferente do atual" },
|
||||
{ status: 400 }
|
||||
{
|
||||
success: false,
|
||||
error: "O novo método de pagamento deve ser diferente do atual",
|
||||
},
|
||||
{ status: 400 },
|
||||
);
|
||||
}
|
||||
|
||||
@@ -102,7 +119,7 @@ export async function PATCH(
|
||||
if (!listing) {
|
||||
return NextResponse.json(
|
||||
{ success: false, error: "Anúncio não encontrado" },
|
||||
{ status: 404 }
|
||||
{ status: 404 },
|
||||
);
|
||||
}
|
||||
|
||||
@@ -120,7 +137,7 @@ export async function PATCH(
|
||||
error: `Método de pagamento "${payment_method}" não está disponível para este anúncio`,
|
||||
available_methods: availableMethods,
|
||||
},
|
||||
{ status: 400 }
|
||||
{ status: 400 },
|
||||
);
|
||||
}
|
||||
|
||||
@@ -128,7 +145,7 @@ export async function PATCH(
|
||||
const oldPaymentMethod = intention.payment_method;
|
||||
const updatedIntention = await updatePurchaseIntentionPaymentMethod(
|
||||
intentionId,
|
||||
payment_method as any
|
||||
payment_method as any,
|
||||
);
|
||||
|
||||
// Criar mensagem do sistema no chat
|
||||
@@ -156,9 +173,10 @@ export async function PATCH(
|
||||
{
|
||||
success: true,
|
||||
intention: updatedIntention,
|
||||
message: "Método de pagamento atualizado com sucesso. O vendedor foi notificado.",
|
||||
message:
|
||||
"Método de pagamento atualizado com sucesso. O vendedor foi notificado.",
|
||||
},
|
||||
{ status: 200 }
|
||||
{ status: 200 },
|
||||
);
|
||||
} catch (error) {
|
||||
secureLogger.error("❌ Erro ao atualizar método de pagamento", {
|
||||
@@ -167,9 +185,14 @@ export async function PATCH(
|
||||
});
|
||||
|
||||
return NextResponse.json(
|
||||
{ success: false, error: error instanceof Error ? error.message : "Erro ao atualizar método de pagamento" },
|
||||
{ status: 500 }
|
||||
{
|
||||
success: false,
|
||||
error:
|
||||
error instanceof Error
|
||||
? error.message
|
||||
: "Erro ao atualizar método de pagamento",
|
||||
},
|
||||
{ status: 500 },
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { secureLogger } from "@/lib/secure-logger";
|
||||
import { type NextRequest, NextResponse } from "next/server";
|
||||
import { requireAuth } from "@/lib/auth/middleware";
|
||||
import { getPurchaseIntentionsByBuyer } from "@/lib/prisma/queries/purchase-intentions";
|
||||
import { prisma } from "@/lib/prisma/app";
|
||||
import { getPurchaseIntentionsByBuyer } from "@/lib/prisma/queries/purchase-intentions";
|
||||
import { trackEvent } from "@/lib/prisma/queries/telemetry";
|
||||
import { secureLogger } from "@/lib/secure-logger";
|
||||
|
||||
/**
|
||||
* GET /api/purchase-intentions/my
|
||||
@@ -78,9 +78,7 @@ export async function GET(request: NextRequest) {
|
||||
|
||||
// Buscar character names dos vendedores em batch
|
||||
const sellerServerPairs = [
|
||||
...new Set(
|
||||
listings.map((l) => `${l.seller_id}:${l.server_name}`)
|
||||
),
|
||||
...new Set(listings.map((l) => `${l.seller_id}:${l.server_name}`)),
|
||||
];
|
||||
|
||||
const characterMap = new Map<string, string | null>();
|
||||
@@ -98,7 +96,7 @@ export async function GET(request: NextRequest) {
|
||||
},
|
||||
});
|
||||
characterMap.set(pair, character?.character_name || null);
|
||||
})
|
||||
}),
|
||||
);
|
||||
|
||||
// Serializar datas para string
|
||||
@@ -123,8 +121,10 @@ export async function GET(request: NextRequest) {
|
||||
|
||||
return {
|
||||
...intention,
|
||||
created_at: serializeDate(intention.created_at as Date) || intention.created_at,
|
||||
updated_at: serializeDate(intention.updated_at as Date) || intention.updated_at,
|
||||
created_at:
|
||||
serializeDate(intention.created_at as Date) || intention.created_at,
|
||||
updated_at:
|
||||
serializeDate(intention.updated_at as Date) || intention.updated_at,
|
||||
expires_at: serializeDate(intention.expires_at as Date | null),
|
||||
accepted_at: serializeDate(intention.accepted_at as Date | null),
|
||||
rejected_at: serializeDate(intention.rejected_at as Date | null),
|
||||
@@ -140,7 +140,8 @@ export async function GET(request: NextRequest) {
|
||||
seller_id: listing.seller_id,
|
||||
price_pix: listing.price_pix ? Number(listing.price_pix) : null,
|
||||
price_tibia_coins:
|
||||
listing.price_tibia_coins !== null && listing.price_tibia_coins !== undefined
|
||||
listing.price_tibia_coins !== null &&
|
||||
listing.price_tibia_coins !== undefined
|
||||
? Number(listing.price_tibia_coins)
|
||||
: null,
|
||||
price_gold_coins: listing.price_gold_coins
|
||||
@@ -185,7 +186,7 @@ export async function GET(request: NextRequest) {
|
||||
intentions: intentionsWithListingDetails,
|
||||
count: intentionsWithListingDetails.length,
|
||||
},
|
||||
{ status: 200 }
|
||||
{ status: 200 },
|
||||
);
|
||||
} catch (error) {
|
||||
secureLogger.error("❌ Erro ao buscar intenções de compra do usuário", {
|
||||
@@ -195,8 +196,7 @@ export async function GET(request: NextRequest) {
|
||||
|
||||
return NextResponse.json(
|
||||
{ success: false, error: "Erro ao buscar intenções de compra" },
|
||||
{ status: 500 }
|
||||
{ status: 500 },
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { type NextRequest, NextResponse } from "next/server";
|
||||
import { getActiveServers } from "@/lib/prisma/queries/servers";
|
||||
import { secureLogger } from "@/lib/secure-logger";
|
||||
|
||||
@@ -6,7 +6,7 @@ import { secureLogger } from "@/lib/secure-logger";
|
||||
* GET /api/servers
|
||||
* Lista todos os servidores ativos disponíveis
|
||||
*/
|
||||
export async function GET(request: NextRequest) {
|
||||
export async function GET(_request: NextRequest) {
|
||||
try {
|
||||
const servers = await getActiveServers();
|
||||
|
||||
@@ -30,7 +30,7 @@ export async function GET(request: NextRequest) {
|
||||
},
|
||||
})),
|
||||
},
|
||||
{ status: 200 }
|
||||
{ status: 200 },
|
||||
);
|
||||
} catch (error) {
|
||||
secureLogger.error("❌ Erro ao buscar servidores", {
|
||||
@@ -43,8 +43,7 @@ export async function GET(request: NextRequest) {
|
||||
success: false,
|
||||
error: "Erro ao buscar servidores",
|
||||
},
|
||||
{ status: 500 }
|
||||
{ status: 500 },
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,4 +13,3 @@ export async function GET() {
|
||||
message: "Socket.io está ativo",
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { type NextRequest, NextResponse } from "next/server";
|
||||
import { requireAdminUser } from "@/lib/auth/middleware";
|
||||
import {
|
||||
countUsersInSubscriptionTier,
|
||||
deleteSubscriptionTier,
|
||||
getSubscriptionTierById,
|
||||
updateSubscriptionTier,
|
||||
deleteSubscriptionTier,
|
||||
countUsersInSubscriptionTier,
|
||||
} from "@/lib/prisma/queries/subscription-tiers";
|
||||
import { updateSubscriptionTierSchema } from "@/lib/validation-schemas";
|
||||
import { secureLogger } from "@/lib/secure-logger";
|
||||
import { updateSubscriptionTierSchema } from "@/lib/validation-schemas";
|
||||
|
||||
export async function GET(
|
||||
request: NextRequest,
|
||||
{ params }: { params: Promise<{ id: string }> }
|
||||
{ params }: { params: Promise<{ id: string }> },
|
||||
) {
|
||||
const auth = await requireAdminUser(request);
|
||||
if (auth instanceof NextResponse) {
|
||||
@@ -25,7 +25,7 @@ export async function GET(
|
||||
if (Number.isNaN(tierId) || tierId <= 0) {
|
||||
return NextResponse.json(
|
||||
{ success: false, error: "ID de tier inválido" },
|
||||
{ status: 400 }
|
||||
{ status: 400 },
|
||||
);
|
||||
}
|
||||
|
||||
@@ -34,7 +34,7 @@ export async function GET(
|
||||
if (!tier) {
|
||||
return NextResponse.json(
|
||||
{ success: false, error: "Plano não encontrado" },
|
||||
{ status: 404 }
|
||||
{ status: 404 },
|
||||
);
|
||||
}
|
||||
|
||||
@@ -43,7 +43,7 @@ export async function GET(
|
||||
success: true,
|
||||
tier,
|
||||
},
|
||||
{ status: 200 }
|
||||
{ status: 200 },
|
||||
);
|
||||
} catch (error) {
|
||||
secureLogger.error("❌ Erro ao consultar subscription tier", {
|
||||
@@ -55,14 +55,14 @@ export async function GET(
|
||||
success: false,
|
||||
error: "Erro ao consultar plano de assinatura",
|
||||
},
|
||||
{ status: 500 }
|
||||
{ status: 500 },
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export async function PATCH(
|
||||
request: NextRequest,
|
||||
{ params }: { params: Promise<{ id: string }> }
|
||||
{ params }: { params: Promise<{ id: string }> },
|
||||
) {
|
||||
const auth = await requireAdminUser(request);
|
||||
if (auth instanceof NextResponse) {
|
||||
@@ -76,7 +76,7 @@ export async function PATCH(
|
||||
if (Number.isNaN(tierId) || tierId <= 0) {
|
||||
return NextResponse.json(
|
||||
{ success: false, error: "ID de tier inválido" },
|
||||
{ status: 400 }
|
||||
{ status: 400 },
|
||||
);
|
||||
}
|
||||
|
||||
@@ -105,7 +105,7 @@ export async function PATCH(
|
||||
success: true,
|
||||
tier: updatedTier,
|
||||
},
|
||||
{ status: 200 }
|
||||
{ status: 200 },
|
||||
);
|
||||
} catch (error) {
|
||||
if (error instanceof Error && "issues" in error) {
|
||||
@@ -115,7 +115,7 @@ export async function PATCH(
|
||||
error: "Dados inválidos",
|
||||
details: (error as any).issues,
|
||||
},
|
||||
{ status: 400 }
|
||||
{ status: 400 },
|
||||
);
|
||||
}
|
||||
|
||||
@@ -128,14 +128,14 @@ export async function PATCH(
|
||||
success: false,
|
||||
error: "Erro ao atualizar plano de assinatura",
|
||||
},
|
||||
{ status: 500 }
|
||||
{ status: 500 },
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export async function DELETE(
|
||||
request: NextRequest,
|
||||
{ params }: { params: Promise<{ id: string }> }
|
||||
{ params }: { params: Promise<{ id: string }> },
|
||||
) {
|
||||
const auth = await requireAdminUser(request);
|
||||
if (auth instanceof NextResponse) {
|
||||
@@ -149,7 +149,7 @@ export async function DELETE(
|
||||
if (Number.isNaN(tierId) || tierId <= 0) {
|
||||
return NextResponse.json(
|
||||
{ success: false, error: "ID de tier inválido" },
|
||||
{ status: 400 }
|
||||
{ status: 400 },
|
||||
);
|
||||
}
|
||||
|
||||
@@ -158,7 +158,7 @@ export async function DELETE(
|
||||
if (!tier) {
|
||||
return NextResponse.json(
|
||||
{ success: false, error: "Plano não encontrado" },
|
||||
{ status: 404 }
|
||||
{ status: 404 },
|
||||
);
|
||||
}
|
||||
|
||||
@@ -168,7 +168,7 @@ export async function DELETE(
|
||||
success: false,
|
||||
error: "O plano padrão 'free' não pode ser removido.",
|
||||
},
|
||||
{ status: 400 }
|
||||
{ status: 400 },
|
||||
);
|
||||
}
|
||||
|
||||
@@ -178,10 +178,11 @@ export async function DELETE(
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: "Plano não pode ser removido enquanto houver usuários associados.",
|
||||
error:
|
||||
"Plano não pode ser removido enquanto houver usuários associados.",
|
||||
users: userCount,
|
||||
},
|
||||
{ status: 409 }
|
||||
{ status: 409 },
|
||||
);
|
||||
}
|
||||
|
||||
@@ -191,7 +192,7 @@ export async function DELETE(
|
||||
{
|
||||
success: true,
|
||||
},
|
||||
{ status: 200 }
|
||||
{ status: 200 },
|
||||
);
|
||||
} catch (error) {
|
||||
secureLogger.error("❌ Erro ao remover subscription tier", {
|
||||
@@ -203,8 +204,7 @@ export async function DELETE(
|
||||
success: false,
|
||||
error: "Erro ao remover plano de assinatura",
|
||||
},
|
||||
{ status: 500 }
|
||||
{ status: 500 },
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { type NextRequest, NextResponse } from "next/server";
|
||||
import { requireAdminUser } from "@/lib/auth/middleware";
|
||||
import {
|
||||
assignUserToSubscriptionTier,
|
||||
getDefaultSubscriptionTier,
|
||||
getSubscriptionTierById,
|
||||
getSubscriptionTierBySlug,
|
||||
getDefaultSubscriptionTier,
|
||||
} from "@/lib/prisma/queries/subscription-tiers";
|
||||
import { assignSubscriptionTierSchema } from "@/lib/validation-schemas";
|
||||
import { secureLogger } from "@/lib/secure-logger";
|
||||
import { getUserById } from "@/lib/prisma/queries/users";
|
||||
import { secureLogger } from "@/lib/secure-logger";
|
||||
import { assignSubscriptionTierSchema } from "@/lib/validation-schemas";
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
const auth = await requireAdminUser(request);
|
||||
@@ -27,19 +27,17 @@ export async function POST(request: NextRequest) {
|
||||
success: false,
|
||||
error: "Usuário não encontrado.",
|
||||
},
|
||||
{ status: 404 }
|
||||
{ status: 404 },
|
||||
);
|
||||
}
|
||||
|
||||
let targetTierId: number | null = null;
|
||||
let targetTier:
|
||||
| {
|
||||
subscription_tier_id: number;
|
||||
name: string;
|
||||
slug: string;
|
||||
level: number;
|
||||
}
|
||||
| null = null;
|
||||
let targetTier: {
|
||||
subscription_tier_id: number;
|
||||
name: string;
|
||||
slug: string;
|
||||
level: number;
|
||||
} | null = null;
|
||||
|
||||
if (parsed.tierId !== undefined) {
|
||||
if (parsed.tierId === null) {
|
||||
@@ -50,7 +48,7 @@ export async function POST(request: NextRequest) {
|
||||
success: false,
|
||||
error: "Plano padrão não encontrado.",
|
||||
},
|
||||
{ status: 500 }
|
||||
{ status: 500 },
|
||||
);
|
||||
}
|
||||
targetTier = defaultTier;
|
||||
@@ -62,20 +60,23 @@ export async function POST(request: NextRequest) {
|
||||
success: false,
|
||||
error: "Plano informado não existe.",
|
||||
},
|
||||
{ status: 404 }
|
||||
{ status: 404 },
|
||||
);
|
||||
}
|
||||
targetTier = tierById;
|
||||
}
|
||||
} else if (parsed.tierSlug) {
|
||||
const tierBySlug = await getSubscriptionTierBySlug(parsed.tierSlug, false);
|
||||
const tierBySlug = await getSubscriptionTierBySlug(
|
||||
parsed.tierSlug,
|
||||
false,
|
||||
);
|
||||
if (!tierBySlug) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: "Plano informado não existe.",
|
||||
},
|
||||
{ status: 404 }
|
||||
{ status: 404 },
|
||||
);
|
||||
}
|
||||
targetTier = tierBySlug;
|
||||
@@ -87,7 +88,7 @@ export async function POST(request: NextRequest) {
|
||||
success: false,
|
||||
error: "Plano de assinatura não identificado.",
|
||||
},
|
||||
{ status: 400 }
|
||||
{ status: 400 },
|
||||
);
|
||||
}
|
||||
|
||||
@@ -106,7 +107,7 @@ export async function POST(request: NextRequest) {
|
||||
level: targetTier.level,
|
||||
},
|
||||
},
|
||||
{ status: 200 }
|
||||
{ status: 200 },
|
||||
);
|
||||
} catch (error) {
|
||||
if (error instanceof Error && "issues" in error) {
|
||||
@@ -116,7 +117,7 @@ export async function POST(request: NextRequest) {
|
||||
error: "Dados inválidos",
|
||||
details: (error as any).issues,
|
||||
},
|
||||
{ status: 400 }
|
||||
{ status: 400 },
|
||||
);
|
||||
}
|
||||
|
||||
@@ -129,8 +130,7 @@ export async function POST(request: NextRequest) {
|
||||
success: false,
|
||||
error: "Erro ao atribuir plano de assinatura ao usuário",
|
||||
},
|
||||
{ status: 500 }
|
||||
{ status: 500 },
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { type NextRequest, NextResponse } from "next/server";
|
||||
import { requireAdminUser } from "@/lib/auth/middleware";
|
||||
import {
|
||||
getAllSubscriptionTiers,
|
||||
createSubscriptionTier,
|
||||
getAllSubscriptionTiers,
|
||||
} from "@/lib/prisma/queries/subscription-tiers";
|
||||
import { createSubscriptionTierSchema } from "@/lib/validation-schemas";
|
||||
import { secureLogger } from "@/lib/secure-logger";
|
||||
import { createSubscriptionTierSchema } from "@/lib/validation-schemas";
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
const auth = await requireAdminUser(request);
|
||||
@@ -20,7 +20,7 @@ export async function GET(request: NextRequest) {
|
||||
success: true,
|
||||
tiers,
|
||||
},
|
||||
{ status: 200 }
|
||||
{ status: 200 },
|
||||
);
|
||||
} catch (error) {
|
||||
secureLogger.error("❌ Erro ao listar subscription tiers", {
|
||||
@@ -32,7 +32,7 @@ export async function GET(request: NextRequest) {
|
||||
success: false,
|
||||
error: "Erro ao listar planos de assinatura",
|
||||
},
|
||||
{ status: 500 }
|
||||
{ status: 500 },
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -68,7 +68,7 @@ export async function POST(request: NextRequest) {
|
||||
success: true,
|
||||
tier,
|
||||
},
|
||||
{ status: 201 }
|
||||
{ status: 201 },
|
||||
);
|
||||
} catch (error) {
|
||||
if (error instanceof NextResponse) {
|
||||
@@ -82,7 +82,7 @@ export async function POST(request: NextRequest) {
|
||||
error: "Dados inválidos",
|
||||
details: (error as any).issues,
|
||||
},
|
||||
{ status: 400 }
|
||||
{ status: 400 },
|
||||
);
|
||||
}
|
||||
|
||||
@@ -95,8 +95,7 @@ export async function POST(request: NextRequest) {
|
||||
success: false,
|
||||
error: "Erro ao criar plano de assinatura",
|
||||
},
|
||||
{ status: 500 }
|
||||
{ status: 500 },
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
+28
-27
@@ -1,21 +1,21 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { type NextRequest, NextResponse } from "next/server";
|
||||
import { optionalAuth } from "@/lib/auth/middleware";
|
||||
import {
|
||||
getAdAnalytics,
|
||||
getMarketplaceStats,
|
||||
trackEvent,
|
||||
trackEventsBatch,
|
||||
} from "@/lib/prisma/queries/telemetry";
|
||||
import {
|
||||
checkRateLimit,
|
||||
getClientIP,
|
||||
createRateLimitResponse,
|
||||
getClientIP,
|
||||
} from "@/lib/rate-limiter";
|
||||
import { secureLogger } from "@/lib/secure-logger";
|
||||
import {
|
||||
telemetryEventSchema,
|
||||
telemetryBatchSchema,
|
||||
telemetryEventSchema,
|
||||
} from "@/lib/validation-schemas";
|
||||
import { optionalAuth } from "@/lib/auth/middleware";
|
||||
import {
|
||||
trackEvent,
|
||||
trackEventsBatch,
|
||||
getAdAnalytics,
|
||||
getMarketplaceStats,
|
||||
} from "@/lib/prisma/queries/telemetry";
|
||||
|
||||
/**
|
||||
* POST /api/telemetry/event
|
||||
@@ -33,12 +33,14 @@ export async function POST(request: NextRequest) {
|
||||
|
||||
// Autenticação opcional (usuários anônimos podem gerar eventos)
|
||||
const authResult = await optionalAuth(request);
|
||||
const userId = authResult instanceof NextResponse ? undefined : authResult?.userId;
|
||||
const userId =
|
||||
authResult instanceof NextResponse ? undefined : authResult?.userId;
|
||||
|
||||
const body = await request.json();
|
||||
|
||||
// Verificar se é batch ou evento único
|
||||
const isBatch = body.events && Array.isArray(body.events) && body.events.length > 0;
|
||||
const isBatch =
|
||||
body.events && Array.isArray(body.events) && body.events.length > 0;
|
||||
|
||||
if (isBatch) {
|
||||
// Batch de eventos
|
||||
@@ -50,7 +52,7 @@ export async function POST(request: NextRequest) {
|
||||
metadata: event.metadata || {},
|
||||
})),
|
||||
};
|
||||
|
||||
|
||||
const validationResult = telemetryBatchSchema.safeParse(normalizedBody);
|
||||
|
||||
if (!validationResult.success) {
|
||||
@@ -60,7 +62,7 @@ export async function POST(request: NextRequest) {
|
||||
error: "Dados de entrada inválidos",
|
||||
details: validationResult.error.issues,
|
||||
},
|
||||
{ status: 400 }
|
||||
{ status: 400 },
|
||||
);
|
||||
}
|
||||
|
||||
@@ -88,7 +90,7 @@ export async function POST(request: NextRequest) {
|
||||
success: true,
|
||||
events: trackedEvents,
|
||||
},
|
||||
{ status: 201 }
|
||||
{ status: 201 },
|
||||
);
|
||||
} else {
|
||||
// Evento único
|
||||
@@ -97,7 +99,7 @@ export async function POST(request: NextRequest) {
|
||||
...body,
|
||||
metadata: body.metadata || {},
|
||||
};
|
||||
|
||||
|
||||
const validationResult = telemetryEventSchema.safeParse(normalizedBody);
|
||||
|
||||
if (!validationResult.success) {
|
||||
@@ -107,7 +109,7 @@ export async function POST(request: NextRequest) {
|
||||
error: "Dados de entrada inválidos",
|
||||
details: validationResult.error.issues,
|
||||
},
|
||||
{ status: 400 }
|
||||
{ status: 400 },
|
||||
);
|
||||
}
|
||||
|
||||
@@ -134,7 +136,7 @@ export async function POST(request: NextRequest) {
|
||||
success: true,
|
||||
event: trackedEvent,
|
||||
},
|
||||
{ status: 201 }
|
||||
{ status: 201 },
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
@@ -148,7 +150,7 @@ export async function POST(request: NextRequest) {
|
||||
success: false,
|
||||
error: "Erro ao registrar evento",
|
||||
},
|
||||
{ status: 500 }
|
||||
{ status: 500 },
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -178,7 +180,7 @@ export async function GET(request: NextRequest) {
|
||||
success: true,
|
||||
stats,
|
||||
},
|
||||
{ status: 200 }
|
||||
{ status: 200 },
|
||||
);
|
||||
}
|
||||
|
||||
@@ -186,13 +188,13 @@ export async function GET(request: NextRequest) {
|
||||
if (adIdParam) {
|
||||
const adId = parseInt(adIdParam, 10);
|
||||
|
||||
if (isNaN(adId)) {
|
||||
if (Number.isNaN(adId)) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: "ID do anúncio inválido",
|
||||
},
|
||||
{ status: 400 }
|
||||
{ status: 400 },
|
||||
);
|
||||
}
|
||||
|
||||
@@ -204,7 +206,7 @@ export async function GET(request: NextRequest) {
|
||||
success: false,
|
||||
error: "Analytics não encontrado para este anúncio",
|
||||
},
|
||||
{ status: 404 }
|
||||
{ status: 404 },
|
||||
);
|
||||
}
|
||||
|
||||
@@ -213,7 +215,7 @@ export async function GET(request: NextRequest) {
|
||||
success: true,
|
||||
analytics,
|
||||
},
|
||||
{ status: 200 }
|
||||
{ status: 200 },
|
||||
);
|
||||
}
|
||||
|
||||
@@ -222,7 +224,7 @@ export async function GET(request: NextRequest) {
|
||||
success: false,
|
||||
error: "Parâmetro 'adId' ou 'dashboard' é obrigatório",
|
||||
},
|
||||
{ status: 400 }
|
||||
{ status: 400 },
|
||||
);
|
||||
} catch (error) {
|
||||
secureLogger.error("❌ Erro ao buscar analytics", {
|
||||
@@ -235,8 +237,7 @@ export async function GET(request: NextRequest) {
|
||||
success: false,
|
||||
error: "Erro ao buscar analytics",
|
||||
},
|
||||
{ status: 500 }
|
||||
{ status: 500 },
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,22 +1,22 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { secureLogger } from "@/lib/secure-logger";
|
||||
import { type NextRequest, NextResponse } from "next/server";
|
||||
import { getUserById } from "@/lib/prisma/queries/users";
|
||||
import { secureLogger } from "@/lib/secure-logger";
|
||||
|
||||
export async function GET(
|
||||
request: NextRequest,
|
||||
{ params }: { params: Promise<{ id: string }> }
|
||||
_request: NextRequest,
|
||||
{ params }: { params: Promise<{ id: string }> },
|
||||
) {
|
||||
try {
|
||||
const { id } = await params;
|
||||
const userId = parseInt(id, 10);
|
||||
|
||||
if (isNaN(userId)) {
|
||||
if (Number.isNaN(userId)) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: "ID de usuário inválido",
|
||||
},
|
||||
{ status: 400 }
|
||||
{ status: 400 },
|
||||
);
|
||||
}
|
||||
|
||||
@@ -28,7 +28,7 @@ export async function GET(
|
||||
success: false,
|
||||
error: "Usuário não encontrado",
|
||||
},
|
||||
{ status: 404 }
|
||||
{ status: 404 },
|
||||
);
|
||||
}
|
||||
|
||||
@@ -46,7 +46,7 @@ export async function GET(
|
||||
createdAt: user.created_at,
|
||||
},
|
||||
},
|
||||
{ status: 200 }
|
||||
{ status: 200 },
|
||||
);
|
||||
} catch (error) {
|
||||
secureLogger.error("❌ Erro ao buscar perfil público", {
|
||||
@@ -60,8 +60,7 @@ export async function GET(
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : "Erro interno",
|
||||
},
|
||||
{ status: 500 }
|
||||
{ status: 500 },
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,37 +1,36 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { secureLogger } from "@/lib/secure-logger";
|
||||
import { type NextRequest, NextResponse } from "next/server";
|
||||
import { requireAuth } from "@/lib/auth/middleware";
|
||||
import {
|
||||
updateCharacter,
|
||||
deleteCharacter,
|
||||
getCharacterById,
|
||||
updateCharacter,
|
||||
} from "@/lib/prisma/queries/characters";
|
||||
import { secureLogger } from "@/lib/secure-logger";
|
||||
import { characterSchema } from "@/lib/validation-schemas";
|
||||
|
||||
export async function PUT(
|
||||
request: NextRequest,
|
||||
{ params }: { params: Promise<{ id: string }> }
|
||||
{ params }: { params: Promise<{ id: string }> },
|
||||
) {
|
||||
try {
|
||||
// Middleware de autenticação
|
||||
const authResult = await requireAuth(request);
|
||||
|
||||
|
||||
if (authResult instanceof NextResponse) {
|
||||
return authResult;
|
||||
}
|
||||
|
||||
|
||||
const auth = authResult;
|
||||
|
||||
const { id } = await params;
|
||||
const characterId = parseInt(id, 10);
|
||||
|
||||
if (isNaN(characterId)) {
|
||||
if (Number.isNaN(characterId)) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: "ID de personagem inválido",
|
||||
},
|
||||
{ status: 400 }
|
||||
{ status: 400 },
|
||||
);
|
||||
}
|
||||
|
||||
@@ -52,22 +51,18 @@ export async function PUT(
|
||||
error: "Dados de entrada inválidos",
|
||||
details: errors,
|
||||
},
|
||||
{ status: 400 }
|
||||
{ status: 400 },
|
||||
);
|
||||
}
|
||||
|
||||
const validatedData = validationResult.data;
|
||||
|
||||
// Atualizar personagem (verifica ownership internamente)
|
||||
const updatedCharacter = await updateCharacter(
|
||||
characterId,
|
||||
auth.userId,
|
||||
{
|
||||
character_name: validatedData.characterName,
|
||||
server_name: validatedData.serverName,
|
||||
is_main: validatedData.isMain,
|
||||
}
|
||||
);
|
||||
const updatedCharacter = await updateCharacter(characterId, auth.userId, {
|
||||
character_name: validatedData.characterName,
|
||||
server_name: validatedData.serverName,
|
||||
is_main: validatedData.isMain,
|
||||
});
|
||||
|
||||
secureLogger.info("✅ Personagem atualizado com sucesso", {
|
||||
userId: auth.userId,
|
||||
@@ -87,7 +82,7 @@ export async function PUT(
|
||||
createdAt: updatedCharacter.created_at,
|
||||
},
|
||||
},
|
||||
{ status: 200 }
|
||||
{ status: 200 },
|
||||
);
|
||||
} catch (error) {
|
||||
// Se for NextResponse (erro de autenticação), re-lançar
|
||||
@@ -96,16 +91,13 @@ export async function PUT(
|
||||
}
|
||||
|
||||
// Verificar se é erro de permissão
|
||||
if (
|
||||
error instanceof Error &&
|
||||
error.message.includes("não tem permissão")
|
||||
) {
|
||||
if (error instanceof Error && error.message.includes("não tem permissão")) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: error.message,
|
||||
},
|
||||
{ status: 403 }
|
||||
{ status: 403 },
|
||||
);
|
||||
}
|
||||
|
||||
@@ -120,35 +112,35 @@ export async function PUT(
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : "Erro interno",
|
||||
},
|
||||
{ status: 500 }
|
||||
{ status: 500 },
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export async function DELETE(
|
||||
request: NextRequest,
|
||||
{ params }: { params: Promise<{ id: string }> }
|
||||
{ params }: { params: Promise<{ id: string }> },
|
||||
) {
|
||||
try {
|
||||
// Middleware de autenticação
|
||||
const authResult = await requireAuth(request);
|
||||
|
||||
|
||||
if (authResult instanceof NextResponse) {
|
||||
return authResult;
|
||||
}
|
||||
|
||||
|
||||
const auth = authResult;
|
||||
|
||||
const { id } = await params;
|
||||
const characterId = parseInt(id, 10);
|
||||
|
||||
if (isNaN(characterId)) {
|
||||
if (Number.isNaN(characterId)) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: "ID de personagem inválido",
|
||||
},
|
||||
{ status: 400 }
|
||||
{ status: 400 },
|
||||
);
|
||||
}
|
||||
|
||||
@@ -165,7 +157,7 @@ export async function DELETE(
|
||||
{
|
||||
success: true,
|
||||
},
|
||||
{ status: 200 }
|
||||
{ status: 200 },
|
||||
);
|
||||
} catch (error) {
|
||||
// Se for NextResponse (erro de autenticação), re-lançar
|
||||
@@ -174,16 +166,13 @@ export async function DELETE(
|
||||
}
|
||||
|
||||
// Verificar se é erro de permissão
|
||||
if (
|
||||
error instanceof Error &&
|
||||
error.message.includes("não tem permissão")
|
||||
) {
|
||||
if (error instanceof Error && error.message.includes("não tem permissão")) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: error.message,
|
||||
},
|
||||
{ status: 403 }
|
||||
{ status: 403 },
|
||||
);
|
||||
}
|
||||
|
||||
@@ -198,8 +187,7 @@ export async function DELETE(
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : "Erro interno",
|
||||
},
|
||||
{ status: 500 }
|
||||
{ status: 500 },
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,18 +1,21 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { secureLogger } from "@/lib/secure-logger";
|
||||
import { type NextRequest, NextResponse } from "next/server";
|
||||
import { requireAuth } from "@/lib/auth/middleware";
|
||||
import { getUserCharacters, createCharacter } from "@/lib/prisma/queries/characters";
|
||||
import {
|
||||
createCharacter,
|
||||
getUserCharacters,
|
||||
} from "@/lib/prisma/queries/characters";
|
||||
import { secureLogger } from "@/lib/secure-logger";
|
||||
import { characterSchema } from "@/lib/validation-schemas";
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
try {
|
||||
// Middleware de autenticação
|
||||
const authResult = await requireAuth(request);
|
||||
|
||||
|
||||
if (authResult instanceof NextResponse) {
|
||||
return authResult;
|
||||
}
|
||||
|
||||
|
||||
const auth = authResult;
|
||||
|
||||
// Listar personagens do usuário
|
||||
@@ -30,7 +33,7 @@ export async function GET(request: NextRequest) {
|
||||
createdAt: char.created_at,
|
||||
})),
|
||||
},
|
||||
{ status: 200 }
|
||||
{ status: 200 },
|
||||
);
|
||||
} catch (error) {
|
||||
// Se for NextResponse (erro de autenticação), re-lançar
|
||||
@@ -49,7 +52,7 @@ export async function GET(request: NextRequest) {
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : "Erro interno",
|
||||
},
|
||||
{ status: 500 }
|
||||
{ status: 500 },
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -58,11 +61,11 @@ export async function POST(request: NextRequest) {
|
||||
try {
|
||||
// Middleware de autenticação
|
||||
const authResult = await requireAuth(request);
|
||||
|
||||
|
||||
if (authResult instanceof NextResponse) {
|
||||
return authResult;
|
||||
}
|
||||
|
||||
|
||||
const auth = authResult;
|
||||
|
||||
const body = await request.json();
|
||||
@@ -82,7 +85,7 @@ export async function POST(request: NextRequest) {
|
||||
error: "Dados de entrada inválidos",
|
||||
details: errors,
|
||||
},
|
||||
{ status: 400 }
|
||||
{ status: 400 },
|
||||
);
|
||||
}
|
||||
|
||||
@@ -113,7 +116,7 @@ export async function POST(request: NextRequest) {
|
||||
createdAt: newCharacter.created_at,
|
||||
},
|
||||
},
|
||||
{ status: 201 }
|
||||
{ status: 201 },
|
||||
);
|
||||
} catch (error) {
|
||||
// Se for NextResponse (erro de autenticação), re-lançar
|
||||
@@ -128,7 +131,7 @@ export async function POST(request: NextRequest) {
|
||||
success: false,
|
||||
error: "Este personagem já está cadastrado neste servidor",
|
||||
},
|
||||
{ status: 409 }
|
||||
{ status: 409 },
|
||||
);
|
||||
}
|
||||
|
||||
@@ -143,8 +146,7 @@ export async function POST(request: NextRequest) {
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : "Erro interno",
|
||||
},
|
||||
{ status: 500 }
|
||||
{ status: 500 },
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
+13
-14
@@ -1,19 +1,19 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { secureLogger } from "@/lib/secure-logger";
|
||||
import { type NextRequest, NextResponse } from "next/server";
|
||||
import { requireAuth } from "@/lib/auth/middleware";
|
||||
import { getUserById, updateUser } from "@/lib/prisma/queries/users";
|
||||
import { getUserCharacters } from "@/lib/prisma/queries/characters";
|
||||
import { getUserById, updateUser } from "@/lib/prisma/queries/users";
|
||||
import { secureLogger } from "@/lib/secure-logger";
|
||||
import { updateUserSchema } from "@/lib/validation-schemas";
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
try {
|
||||
// Middleware de autenticação
|
||||
const authResult = await requireAuth(request);
|
||||
|
||||
|
||||
if (authResult instanceof NextResponse) {
|
||||
return authResult;
|
||||
}
|
||||
|
||||
|
||||
const auth = authResult;
|
||||
|
||||
// Buscar dados do usuário
|
||||
@@ -25,7 +25,7 @@ export async function GET(request: NextRequest) {
|
||||
success: false,
|
||||
error: "Usuário não encontrado",
|
||||
},
|
||||
{ status: 404 }
|
||||
{ status: 404 },
|
||||
);
|
||||
}
|
||||
|
||||
@@ -61,7 +61,7 @@ export async function GET(request: NextRequest) {
|
||||
createdAt: char.created_at,
|
||||
})),
|
||||
},
|
||||
{ status: 200 }
|
||||
{ status: 200 },
|
||||
);
|
||||
} catch (error) {
|
||||
// Se for NextResponse (erro de autenticação), re-lançar
|
||||
@@ -80,7 +80,7 @@ export async function GET(request: NextRequest) {
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : "Erro interno",
|
||||
},
|
||||
{ status: 500 }
|
||||
{ status: 500 },
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -89,11 +89,11 @@ export async function PUT(request: NextRequest) {
|
||||
try {
|
||||
// Middleware de autenticação
|
||||
const authResult = await requireAuth(request);
|
||||
|
||||
|
||||
if (authResult instanceof NextResponse) {
|
||||
return authResult;
|
||||
}
|
||||
|
||||
|
||||
const auth = authResult;
|
||||
|
||||
const body = await request.json();
|
||||
@@ -113,7 +113,7 @@ export async function PUT(request: NextRequest) {
|
||||
error: "Dados de entrada inválidos",
|
||||
details: errors,
|
||||
},
|
||||
{ status: 400 }
|
||||
{ status: 400 },
|
||||
);
|
||||
}
|
||||
|
||||
@@ -153,7 +153,7 @@ export async function PUT(request: NextRequest) {
|
||||
isVerified: updatedUser.is_verified,
|
||||
},
|
||||
},
|
||||
{ status: 200 }
|
||||
{ status: 200 },
|
||||
);
|
||||
} catch (error) {
|
||||
// Se for NextResponse (erro de autenticação), re-lançar
|
||||
@@ -172,8 +172,7 @@ export async function PUT(request: NextRequest) {
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : "Erro interno",
|
||||
},
|
||||
{ status: 500 }
|
||||
{ status: 500 },
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { abacatePay } from "@/lib/abacatepay";
|
||||
import { type NextRequest, NextResponse } from "next/server";
|
||||
import { markAsPaid } from "@/lib/payment-cache";
|
||||
import {
|
||||
checkRateLimit,
|
||||
getClientIP,
|
||||
createRateLimitResponse,
|
||||
getClientIP,
|
||||
} from "@/lib/rate-limiter";
|
||||
import { secureLogger } from "@/lib/secure-logger";
|
||||
|
||||
@@ -56,11 +55,11 @@ export async function POST(request: NextRequest) {
|
||||
// Rejeitar eventos com mais de 5 minutos de diferença
|
||||
if (timeDiff > 5 * 60 * 1000) {
|
||||
secureLogger.warn("Webhook rejeitado: timestamp inválido", {
|
||||
timeDiff: timeDiff / 1000 + "s",
|
||||
timeDiff: `${timeDiff / 1000}s`,
|
||||
});
|
||||
return NextResponse.json(
|
||||
{ error: "Timestamp inválido" },
|
||||
{ status: 400 }
|
||||
{ status: 400 },
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -119,7 +118,7 @@ export async function POST(request: NextRequest) {
|
||||
|
||||
secureLogger.info(
|
||||
"Registro de pagamento criado",
|
||||
secureLogger.mask(donationRecord)
|
||||
secureLogger.mask(donationRecord),
|
||||
);
|
||||
|
||||
// Salvar no cache para frontend detectar via polling
|
||||
@@ -150,7 +149,7 @@ export async function POST(request: NextRequest) {
|
||||
secureLogger.error("Webhook error", error);
|
||||
return NextResponse.json(
|
||||
{ error: "Erro ao processar webhook" },
|
||||
{ status: 500 }
|
||||
{ status: 500 },
|
||||
);
|
||||
}
|
||||
}
|
||||
+11
-5
@@ -1,5 +1,5 @@
|
||||
import type { Metadata, Viewport } from "next";
|
||||
import { Nunito_Sans, Geist_Mono } from "next/font/google";
|
||||
import { Geist_Mono, Nunito_Sans } from "next/font/google";
|
||||
import "./globals.css";
|
||||
|
||||
const nunitoSans = Nunito_Sans({
|
||||
@@ -15,7 +15,8 @@ const geistMono = Geist_Mono({
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Tibia Trade",
|
||||
description: "Marketplace seguro para negociação de itens e serviços de Tibia.",
|
||||
description:
|
||||
"Marketplace seguro para negociação de itens e serviços de Tibia.",
|
||||
};
|
||||
|
||||
export const viewport: Viewport = {
|
||||
@@ -24,13 +25,18 @@ export const viewport: Viewport = {
|
||||
initialScale: 1,
|
||||
};
|
||||
|
||||
export default function RootLayout({ children }: { children: React.ReactNode }) {
|
||||
export default function RootLayout({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<html lang="pt" suppressHydrationWarning>
|
||||
<body className={`${nunitoSans.variable} ${geistMono.variable} antialiased`}>
|
||||
<body
|
||||
className={`${nunitoSans.variable} ${geistMono.variable} antialiased`}
|
||||
>
|
||||
{children}
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
import { MetadataRoute } from "next";
|
||||
import type { MetadataRoute } from "next";
|
||||
|
||||
export default function sitemap(): MetadataRoute.Sitemap {
|
||||
const baseUrl = "https://tibiatrade.evandro.dev.br";
|
||||
|
||||
Reference in new issue
Block a user