style: clean up code formatting and remove trailing whitespace

refactor: reorganize imports and fix type imports
This commit is contained in:
Evandro Fonseca Junior committed 2025-11-11 03:08:36 -03:00
1 parent d3704602fd
commit 47c04de6fe
232 files changed
+7140 -4798

No files matched your search

@@ -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 },
);
}
}
+28 -12
View File
@@ -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 },
);
}
+6 -6
View File
@@ -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 },
);
}
+91 -41
View File
@@ -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
View File
@@ -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 },
);
}
}
+8 -9
View File
@@ -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 },
);
}
}
+7 -8
View File
@@ -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 },
);
}
}
+21 -30
View File
@@ -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 },
);
}
}
+30 -23
View File
@@ -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 },
);
}
}
+40 -34
View File
@@ -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 },
);
}
}
+10 -8
View File
@@ -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 },
);
}
}
+6 -6
View File
@@ -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
View File
@@ -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 },
);
}
}
+3 -3
View File
@@ -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 },
);
}
}
+8 -8
View File
@@ -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 },
);
}
}
+4 -4
View File
@@ -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 },
);
}
}
+4 -4
View File
@@ -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
View File
@@ -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 });
}
}
+2 -7
View File
@@ -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
View File
@@ -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 },
);
}
}
+11 -12
View File
@@ -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 },
);
}
}
+29 -18
View File
@@ -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 },
);
}
}
+10 -11
View File
@@ -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 },
);
}
}
+21 -19
View File
@@ -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({
});
}
}
+67 -43
View File
@@ -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 },
);
}
}
+36 -21
View File
@@ -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 },
);
}
}
+11 -12
View File
@@ -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 },
);
}
}
+44 -30
View File
@@ -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 },
);
}
}
+58 -39
View File
@@ -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 },
);
}
}
+14 -12
View File
@@ -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 },
);
}
}
+8 -9
View File
@@ -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 },
);
}
}
+5 -7
View File
@@ -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
View File
@@ -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 },
);
}
}
+8 -9
View File
@@ -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 },
);
}
}
+10 -11
View File
@@ -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 },
);
}
}
+4 -5
View File
@@ -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 },
);
}
}
+13 -13
View File
@@ -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 },
);
}
}
+4 -5
View File
@@ -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 },
);
}
}
-1
View File
@@ -13,4 +13,3 @@ export async function GET() {
message: "Socket.io está ativo",
});
}
+23 -23
View File
@@ -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 },
);
}
}
+22 -22
View File
@@ -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 },
);
}
}
+8 -9
View File
@@ -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
View File
@@ -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 },
);
}
}
+9 -10
View File
@@ -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 },
);
}
}
+27 -39
View File
@@ -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 },
);
}
}
+16 -14
View File
@@ -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
View File
@@ -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 },
);
}
}
+6 -7
View File
@@ -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 },
);
}
}