feat: add user authentication UI to header component
Add login and register dialogs, user dropdown menu with profile link and logout option, and integrate with session management hook. Prevent hydration mismatches by conditionally rendering auth elements after mount. Update package-lock.json with new @esbuild dependencies for build tooling.
This commit is contained in:
1 parent
e641138887
commit
55eebde0ba
55 files changed
+7353
-7
No files matched your search
@@ -0,0 +1,199 @@
|
||||
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 { comparePassword } from "@/lib/auth/password";
|
||||
import {
|
||||
generateAccessToken,
|
||||
generateRefreshToken,
|
||||
} from "@/lib/auth/jwt";
|
||||
import {
|
||||
getUserByEmail,
|
||||
getUserByUsername,
|
||||
updateLastLogin,
|
||||
} from "@/lib/db/queries/users";
|
||||
import { setSession } from "@/lib/redis/session";
|
||||
import { randomUUID } from "crypto";
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
// Rate limiting
|
||||
const ip = getClientIP(request);
|
||||
const rateLimit = checkRateLimit(ip, "/api/auth/login");
|
||||
|
||||
if (!rateLimit.allowed) {
|
||||
return createRateLimitResponse(rateLimit.resetAt);
|
||||
}
|
||||
|
||||
const body = await request.json();
|
||||
|
||||
secureLogger.info("📥 POST /api/auth/login - Recebido", {
|
||||
ip,
|
||||
timestamp: new Date().toISOString(),
|
||||
});
|
||||
|
||||
// Validação com Zod
|
||||
const validationResult = loginSchema.safeParse(body);
|
||||
|
||||
if (!validationResult.success) {
|
||||
const errors = validationResult.error.issues.map((err) => ({
|
||||
field: err.path.join("."),
|
||||
message: err.message,
|
||||
}));
|
||||
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: "Dados de entrada inválidos",
|
||||
details: errors,
|
||||
},
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
const { emailOrUsername, password } = validationResult.data;
|
||||
|
||||
// Buscar usuário por email ou username
|
||||
const user =
|
||||
(await getUserByEmail(emailOrUsername)) ||
|
||||
(await getUserByUsername(emailOrUsername));
|
||||
|
||||
if (!user) {
|
||||
secureLogger.warn("⚠️ Tentativa de login com credenciais inválidas", {
|
||||
emailOrUsername: secureLogger.mask({ emailOrUsername }),
|
||||
timestamp: new Date().toISOString(),
|
||||
});
|
||||
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: "Email/username ou senha incorretos",
|
||||
},
|
||||
{ status: 401 }
|
||||
);
|
||||
}
|
||||
|
||||
// Verificar se usuário está ativo
|
||||
if (!user.is_active) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: "Conta desativada. Entre em contato com o suporte.",
|
||||
},
|
||||
{ status: 403 }
|
||||
);
|
||||
}
|
||||
|
||||
// Verificar senha
|
||||
if (!user.password_hash) {
|
||||
secureLogger.error("❌ Usuário sem password_hash", {
|
||||
userId: user.user_id,
|
||||
timestamp: new Date().toISOString(),
|
||||
});
|
||||
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: "Erro interno. Tente novamente mais tarde.",
|
||||
},
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
|
||||
const passwordMatch = await comparePassword(password, user.password_hash);
|
||||
|
||||
if (!passwordMatch) {
|
||||
secureLogger.warn("⚠️ Senha incorreta", {
|
||||
userId: user.user_id,
|
||||
timestamp: new Date().toISOString(),
|
||||
});
|
||||
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: "Email/username ou senha incorretos",
|
||||
},
|
||||
{ status: 401 }
|
||||
);
|
||||
}
|
||||
|
||||
// Atualizar último login
|
||||
await updateLastLogin(user.user_id);
|
||||
|
||||
// Gerar tokens
|
||||
const accessToken = generateAccessToken(user.user_id, user.username);
|
||||
const refreshToken = generateRefreshToken(user.user_id, user.username);
|
||||
|
||||
// Criar sessão no Redis
|
||||
const sessionId = randomUUID();
|
||||
await setSession(sessionId, {
|
||||
userId: user.user_id,
|
||||
username: user.username,
|
||||
email: user.email,
|
||||
accessToken,
|
||||
createdAt: Date.now(),
|
||||
});
|
||||
|
||||
// Criar resposta
|
||||
const response = NextResponse.json(
|
||||
{
|
||||
success: true,
|
||||
user: {
|
||||
userId: user.user_id,
|
||||
username: user.username,
|
||||
email: user.email,
|
||||
firstName: user.first_name,
|
||||
lastName: user.last_name,
|
||||
accountLevel: user.account_level,
|
||||
hasDonated: user.has_donated,
|
||||
},
|
||||
accessToken,
|
||||
},
|
||||
{ status: 200 }
|
||||
);
|
||||
|
||||
// Adicionar refresh token e sessionId como cookies httpOnly
|
||||
response.cookies.set("refreshToken", refreshToken, {
|
||||
httpOnly: true,
|
||||
secure: process.env.NODE_ENV === "production",
|
||||
sameSite: "lax",
|
||||
maxAge: 60 * 60 * 24 * 7, // 7 dias
|
||||
path: "/",
|
||||
});
|
||||
|
||||
response.cookies.set("sessionId", sessionId, {
|
||||
httpOnly: true,
|
||||
secure: process.env.NODE_ENV === "production",
|
||||
sameSite: "lax",
|
||||
maxAge: 60 * 60 * 24 * 7, // 7 dias
|
||||
path: "/",
|
||||
});
|
||||
|
||||
secureLogger.info("✅ Login realizado com sucesso", {
|
||||
userId: user.user_id,
|
||||
username: user.username,
|
||||
timestamp: new Date().toISOString(),
|
||||
});
|
||||
|
||||
return response;
|
||||
} catch (error) {
|
||||
secureLogger.error("❌ Erro ao fazer login", {
|
||||
error: error instanceof Error ? error.message : "Erro desconhecido",
|
||||
stack: error instanceof Error ? error.stack : undefined,
|
||||
timestamp: new Date().toISOString(),
|
||||
});
|
||||
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : "Erro interno",
|
||||
},
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,92 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { secureLogger } from "@/lib/secure-logger";
|
||||
import { requireAuth } from "@/lib/auth/middleware";
|
||||
import { getUserById } from "@/lib/db/queries/users";
|
||||
import { getUserCharacters } from "@/lib/db/queries/characters";
|
||||
|
||||
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
|
||||
const user = await getUserById(auth.userId);
|
||||
|
||||
if (!user) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: "Usuário não encontrado",
|
||||
},
|
||||
{ status: 404 }
|
||||
);
|
||||
}
|
||||
|
||||
// Buscar personagens do usuário
|
||||
const characters = await getUserCharacters(auth.userId);
|
||||
|
||||
secureLogger.info("✅ Perfil do usuário recuperado", {
|
||||
userId: auth.userId,
|
||||
timestamp: new Date().toISOString(),
|
||||
});
|
||||
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: true,
|
||||
user: {
|
||||
userId: user.user_id,
|
||||
username: user.username,
|
||||
email: user.email,
|
||||
firstName: user.first_name,
|
||||
lastName: user.last_name,
|
||||
phone: user.phone,
|
||||
cpf: user.cpf,
|
||||
accountLevel: user.account_level,
|
||||
hasDonated: user.has_donated,
|
||||
whatsappNotifications: user.whatsapp_notifications,
|
||||
createdAt: user.created_at,
|
||||
updatedAt: user.updated_at,
|
||||
lastLogin: user.last_login,
|
||||
isActive: user.is_active,
|
||||
isVerified: user.is_verified,
|
||||
},
|
||||
characters: characters.map((char) => ({
|
||||
characterId: char.character_id,
|
||||
characterName: char.character_name,
|
||||
serverName: char.server_name,
|
||||
isMain: char.is_main,
|
||||
verified: char.verified,
|
||||
createdAt: char.created_at,
|
||||
})),
|
||||
},
|
||||
{ status: 200 }
|
||||
);
|
||||
} catch (error) {
|
||||
// Se for NextResponse (erro de autenticação), re-lançar
|
||||
if (error instanceof NextResponse) {
|
||||
return error;
|
||||
}
|
||||
|
||||
secureLogger.error("❌ Erro ao buscar perfil do usuário", {
|
||||
error: error instanceof Error ? error.message : "Erro desconhecido",
|
||||
stack: error instanceof Error ? error.stack : undefined,
|
||||
timestamp: new Date().toISOString(),
|
||||
});
|
||||
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : "Erro interno",
|
||||
},
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import {
|
||||
checkRateLimit,
|
||||
getClientIP,
|
||||
createRateLimitResponse,
|
||||
} from "@/lib/rate-limiter";
|
||||
import { secureLogger } from "@/lib/secure-logger";
|
||||
import { verifyRefreshToken, generateAccessToken } from "@/lib/auth/jwt";
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
// Rate limiting
|
||||
const ip = getClientIP(request);
|
||||
const rateLimit = checkRateLimit(ip, "/api/auth/refresh");
|
||||
|
||||
if (!rateLimit.allowed) {
|
||||
return createRateLimitResponse(rateLimit.resetAt);
|
||||
}
|
||||
|
||||
// Extrair refresh token do cookie
|
||||
const refreshToken = request.cookies.get("refreshToken")?.value;
|
||||
|
||||
if (!refreshToken) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: "Refresh token não fornecido",
|
||||
},
|
||||
{ status: 401 }
|
||||
);
|
||||
}
|
||||
|
||||
// Validar refresh token
|
||||
const payload = verifyRefreshToken(refreshToken);
|
||||
|
||||
if (!payload) {
|
||||
// Token inválido - limpar cookie
|
||||
const response = NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: "Refresh token inválido ou expirado",
|
||||
},
|
||||
{ status: 401 }
|
||||
);
|
||||
|
||||
response.cookies.delete("refreshToken");
|
||||
return response;
|
||||
}
|
||||
|
||||
// Gerar novo access token
|
||||
const accessToken = generateAccessToken(payload.userId, payload.username);
|
||||
|
||||
secureLogger.info("✅ Token renovado com sucesso", {
|
||||
userId: payload.userId,
|
||||
timestamp: new Date().toISOString(),
|
||||
});
|
||||
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: true,
|
||||
accessToken,
|
||||
},
|
||||
{ status: 200 }
|
||||
);
|
||||
} catch (error) {
|
||||
secureLogger.error("❌ Erro ao renovar token", {
|
||||
error: error instanceof Error ? error.message : "Erro desconhecido",
|
||||
stack: error instanceof Error ? error.stack : undefined,
|
||||
timestamp: new Date().toISOString(),
|
||||
});
|
||||
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : "Erro interno",
|
||||
},
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,270 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import {
|
||||
checkRateLimit,
|
||||
getClientIP,
|
||||
createRateLimitResponse,
|
||||
} from "@/lib/rate-limiter";
|
||||
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/db/queries/users";
|
||||
import {
|
||||
isServerActive,
|
||||
incrementUsersForServers,
|
||||
} from "@/lib/db/queries/servers";
|
||||
import { setSession } from "@/lib/redis/session";
|
||||
import { randomUUID } from "crypto";
|
||||
import { getPool } from "@/lib/db";
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
// Rate limiting
|
||||
const ip = getClientIP(request);
|
||||
const rateLimit = checkRateLimit(ip, "/api/auth/register");
|
||||
|
||||
if (!rateLimit.allowed) {
|
||||
return createRateLimitResponse(rateLimit.resetAt);
|
||||
}
|
||||
|
||||
const body = await request.json();
|
||||
|
||||
secureLogger.info("📥 POST /api/auth/register - Recebido", {
|
||||
ip,
|
||||
timestamp: new Date().toISOString(),
|
||||
});
|
||||
|
||||
// Validação com Zod
|
||||
const validationResult = registerSchema.safeParse(body);
|
||||
|
||||
if (!validationResult.success) {
|
||||
const errors = validationResult.error.issues.map((err) => ({
|
||||
field: err.path.join("."),
|
||||
message: err.message,
|
||||
}));
|
||||
|
||||
secureLogger.error("❌ Validação falhou", {
|
||||
errors: JSON.stringify(errors),
|
||||
timestamp: new Date().toISOString(),
|
||||
});
|
||||
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: "Dados de entrada inválidos",
|
||||
details: errors,
|
||||
},
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
const validatedData = validationResult.data;
|
||||
|
||||
// Verificar duplicidade: email
|
||||
const existingEmail = await getUserByEmail(validatedData.email);
|
||||
if (existingEmail) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: "Email já está em uso",
|
||||
},
|
||||
{ status: 409 }
|
||||
);
|
||||
}
|
||||
|
||||
// Verificar duplicidade: username
|
||||
const existingUsername = await getUserByUsername(validatedData.username);
|
||||
if (existingUsername) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: "Username já está em uso",
|
||||
},
|
||||
{ status: 409 }
|
||||
);
|
||||
}
|
||||
|
||||
// Validar servidores: verificar se existem e estão ativos
|
||||
const serverNames = validatedData.servers.map((s) => s.serverName);
|
||||
const uniqueServerNames = [...new Set(serverNames)]; // Remover duplicatas
|
||||
|
||||
for (const serverName of uniqueServerNames) {
|
||||
const isActive = await isServerActive(serverName);
|
||||
if (!isActive) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: `Servidor "${serverName}" não existe ou não está ativo`,
|
||||
},
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Hash da senha
|
||||
const passwordHash = await hashPassword(validatedData.password);
|
||||
|
||||
// Criar usuário e personagens em transação atômica
|
||||
const pool = getPool();
|
||||
const client = await pool.connect();
|
||||
|
||||
let newUser: { userId: number; username: string; email: string };
|
||||
const createdCharacters: Array<{ characterName: string; serverName: string }> = [];
|
||||
|
||||
try {
|
||||
await client.query("BEGIN");
|
||||
|
||||
// Criar usuário
|
||||
const userResult = await client.query<{
|
||||
user_id: number;
|
||||
username: string;
|
||||
email: string;
|
||||
}>(
|
||||
`
|
||||
INSERT INTO users (username, email, password_hash, first_name, last_name, phone, cpf)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7)
|
||||
RETURNING user_id, username, email
|
||||
`,
|
||||
[
|
||||
validatedData.username,
|
||||
validatedData.email,
|
||||
passwordHash,
|
||||
validatedData.firstName,
|
||||
validatedData.lastName,
|
||||
validatedData.phone || null,
|
||||
null, // CPF não é mais obrigatório
|
||||
]
|
||||
);
|
||||
|
||||
newUser = {
|
||||
userId: userResult.rows[0].user_id,
|
||||
username: userResult.rows[0].username,
|
||||
email: userResult.rows[0].email,
|
||||
};
|
||||
|
||||
// Criar personagens (primeiro é main por padrão)
|
||||
for (let i = 0; i < validatedData.servers.length; i++) {
|
||||
const serverData = validatedData.servers[i];
|
||||
const isMain = i === 0 || serverData.isMain === true;
|
||||
|
||||
// Se este personagem será main e não é o primeiro, desmarcar outros
|
||||
if (isMain && i > 0) {
|
||||
await client.query(
|
||||
`
|
||||
UPDATE user_characters
|
||||
SET is_main = FALSE
|
||||
WHERE user_id = $1
|
||||
`,
|
||||
[newUser.userId]
|
||||
);
|
||||
}
|
||||
|
||||
// Inserir personagem
|
||||
const charResult = await client.query<{
|
||||
character_id: number;
|
||||
character_name: string;
|
||||
server_name: string;
|
||||
}>(
|
||||
`
|
||||
INSERT INTO user_characters (user_id, character_name, server_name, is_main)
|
||||
VALUES ($1, $2, $3, $4)
|
||||
RETURNING character_id, character_name, server_name
|
||||
`,
|
||||
[newUser.userId, serverData.characterName, serverData.serverName, isMain]
|
||||
);
|
||||
|
||||
createdCharacters.push({
|
||||
characterName: charResult.rows[0].character_name,
|
||||
serverName: charResult.rows[0].server_name,
|
||||
});
|
||||
}
|
||||
|
||||
// Incrementar total_users dos servidores
|
||||
await incrementUsersForServers(uniqueServerNames);
|
||||
|
||||
await client.query("COMMIT");
|
||||
} catch (error) {
|
||||
await client.query("ROLLBACK");
|
||||
throw error;
|
||||
} finally {
|
||||
client.release();
|
||||
}
|
||||
|
||||
// Gerar tokens
|
||||
const accessToken = generateAccessToken(newUser.userId, newUser.username);
|
||||
const refreshToken = generateRefreshToken(
|
||||
newUser.userId,
|
||||
newUser.username
|
||||
);
|
||||
|
||||
// Criar sessão no Redis
|
||||
const sessionId = randomUUID();
|
||||
await setSession(sessionId, {
|
||||
userId: newUser.userId,
|
||||
username: newUser.username,
|
||||
email: newUser.email,
|
||||
accessToken,
|
||||
createdAt: Date.now(),
|
||||
});
|
||||
|
||||
// Criar resposta
|
||||
const response = NextResponse.json(
|
||||
{
|
||||
success: true,
|
||||
user: {
|
||||
userId: newUser.userId,
|
||||
username: newUser.username,
|
||||
email: newUser.email,
|
||||
},
|
||||
accessToken,
|
||||
},
|
||||
{ status: 201 }
|
||||
);
|
||||
|
||||
// Adicionar refresh token e sessionId como cookies httpOnly
|
||||
response.cookies.set("refreshToken", refreshToken, {
|
||||
httpOnly: true,
|
||||
secure: process.env.NODE_ENV === "production",
|
||||
sameSite: "lax",
|
||||
maxAge: 60 * 60 * 24 * 7, // 7 dias
|
||||
path: "/",
|
||||
});
|
||||
|
||||
response.cookies.set("sessionId", sessionId, {
|
||||
httpOnly: true,
|
||||
secure: process.env.NODE_ENV === "production",
|
||||
sameSite: "lax",
|
||||
maxAge: 60 * 60 * 24 * 7, // 7 dias
|
||||
path: "/",
|
||||
});
|
||||
|
||||
secureLogger.info("✅ Usuário registrado com sucesso", {
|
||||
userId: newUser.userId,
|
||||
username: newUser.username,
|
||||
timestamp: new Date().toISOString(),
|
||||
});
|
||||
|
||||
return response;
|
||||
} catch (error) {
|
||||
secureLogger.error("❌ Erro ao registrar usuário", {
|
||||
error: error instanceof Error ? error.message : "Erro desconhecido",
|
||||
stack: error instanceof Error ? error.stack : undefined,
|
||||
timestamp: new Date().toISOString(),
|
||||
});
|
||||
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : "Erro interno",
|
||||
},
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,136 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { cookies } from "next/headers";
|
||||
import { secureLogger } from "@/lib/secure-logger";
|
||||
import { getSession, refreshSession, deleteSession } from "@/lib/redis/session";
|
||||
import { verifyAccessToken } from "@/lib/auth/jwt";
|
||||
|
||||
/**
|
||||
* GET /api/auth/session
|
||||
* Verifica e retorna dados da sessão atual
|
||||
*/
|
||||
export async function GET(request: NextRequest) {
|
||||
try {
|
||||
// Buscar sessionId do cookie
|
||||
const cookieStore = await cookies();
|
||||
const sessionId = cookieStore.get("sessionId")?.value;
|
||||
|
||||
if (!sessionId) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
authenticated: false,
|
||||
error: "Nenhuma sessão encontrada",
|
||||
},
|
||||
{ status: 401 }
|
||||
);
|
||||
}
|
||||
|
||||
// Buscar sessão no Redis
|
||||
const session = await getSession(sessionId);
|
||||
|
||||
if (!session) {
|
||||
// Limpar cookie se sessão não existe
|
||||
const response = NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
authenticated: false,
|
||||
error: "Sessão expirada ou inválida",
|
||||
},
|
||||
{ status: 401 }
|
||||
);
|
||||
|
||||
response.cookies.delete("sessionId");
|
||||
return response;
|
||||
}
|
||||
|
||||
// Verificar se o access token ainda é válido
|
||||
const tokenPayload = verifyAccessToken(session.accessToken);
|
||||
|
||||
if (!tokenPayload) {
|
||||
// Token expirado - remover sessão
|
||||
await deleteSession(sessionId);
|
||||
const response = NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
authenticated: false,
|
||||
error: "Token expirado",
|
||||
},
|
||||
{ status: 401 }
|
||||
);
|
||||
|
||||
response.cookies.delete("sessionId");
|
||||
return response;
|
||||
}
|
||||
|
||||
// Atualizar TTL da sessão
|
||||
await refreshSession(sessionId);
|
||||
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: true,
|
||||
authenticated: true,
|
||||
user: {
|
||||
userId: session.userId,
|
||||
username: session.username,
|
||||
email: session.email,
|
||||
},
|
||||
},
|
||||
{ status: 200 }
|
||||
);
|
||||
} catch (error) {
|
||||
secureLogger.error("❌ Erro ao verificar sessão", {
|
||||
error: error instanceof Error ? error.message : "Erro desconhecido",
|
||||
stack: error instanceof Error ? error.stack : undefined,
|
||||
timestamp: new Date().toISOString(),
|
||||
});
|
||||
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
authenticated: false,
|
||||
error: error instanceof Error ? error.message : "Erro interno",
|
||||
},
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* DELETE /api/auth/session
|
||||
* Encerra a sessão atual
|
||||
*/
|
||||
export async function DELETE(request: NextRequest) {
|
||||
try {
|
||||
const cookieStore = await cookies();
|
||||
const sessionId = cookieStore.get("sessionId")?.value;
|
||||
|
||||
if (sessionId) {
|
||||
await deleteSession(sessionId);
|
||||
}
|
||||
|
||||
const response = NextResponse.json(
|
||||
{
|
||||
success: true,
|
||||
message: "Sessão encerrada",
|
||||
},
|
||||
{ status: 200 }
|
||||
);
|
||||
|
||||
response.cookies.delete("sessionId");
|
||||
return response;
|
||||
} catch (error) {
|
||||
secureLogger.error("❌ Erro ao encerrar sessão", {
|
||||
error: error instanceof Error ? error.message : "Erro desconhecido",
|
||||
timestamp: new Date().toISOString(),
|
||||
});
|
||||
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : "Erro interno",
|
||||
},
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,282 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { secureLogger } from "@/lib/secure-logger";
|
||||
import { requireAuth, optionalAuth } from "@/lib/auth/middleware";
|
||||
import {
|
||||
getListingById,
|
||||
updateListing,
|
||||
deleteListing,
|
||||
} from "@/lib/db/queries/listings";
|
||||
import { updateListingSchema } from "@/lib/validation-schemas";
|
||||
|
||||
/**
|
||||
* GET /api/listings/[id]
|
||||
* Busca anúncio por ID
|
||||
*/
|
||||
export async function GET(
|
||||
request: NextRequest,
|
||||
{ params }: { params: Promise<{ id: string }> }
|
||||
) {
|
||||
try {
|
||||
const { id } = await params;
|
||||
const listingId = parseInt(id, 10);
|
||||
|
||||
if (isNaN(listingId)) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: "ID de anúncio inválido",
|
||||
},
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
const listing = await getListingById(listingId);
|
||||
|
||||
if (!listing) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: "Anúncio não encontrado",
|
||||
},
|
||||
{ status: 404 }
|
||||
);
|
||||
}
|
||||
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: true,
|
||||
listing,
|
||||
},
|
||||
{ status: 200 }
|
||||
);
|
||||
} catch (error) {
|
||||
secureLogger.error("❌ Erro ao buscar anúncio", {
|
||||
error: error instanceof Error ? error.message : "Erro desconhecido",
|
||||
stack: error instanceof Error ? error.stack : undefined,
|
||||
});
|
||||
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: "Erro ao buscar anúncio",
|
||||
},
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* PATCH /api/listings/[id]
|
||||
* Atualiza anúncio (apenas dono)
|
||||
*/
|
||||
export async function PATCH(
|
||||
request: NextRequest,
|
||||
{ params }: { params: Promise<{ id: string }> }
|
||||
) {
|
||||
try {
|
||||
// Autenticação obrigatória
|
||||
const authResult = await requireAuth(request);
|
||||
|
||||
if (authResult instanceof NextResponse) {
|
||||
return authResult;
|
||||
}
|
||||
|
||||
const auth = authResult;
|
||||
|
||||
const { id } = await params;
|
||||
const listingId = parseInt(id, 10);
|
||||
|
||||
if (isNaN(listingId)) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: "ID de anúncio inválido",
|
||||
},
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
// Verificar se o anúncio existe e pertence ao usuário
|
||||
const listing = await getListingById(listingId);
|
||||
|
||||
if (!listing) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: "Anúncio não encontrado",
|
||||
},
|
||||
{ status: 404 }
|
||||
);
|
||||
}
|
||||
|
||||
if (listing.seller_id !== auth.userId) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: "Você não tem permissão para editar este anúncio",
|
||||
},
|
||||
{ status: 403 }
|
||||
);
|
||||
}
|
||||
|
||||
const body = await request.json();
|
||||
|
||||
// Validação com Zod
|
||||
const validationResult = updateListingSchema.safeParse(body);
|
||||
|
||||
if (!validationResult.success) {
|
||||
const errors = validationResult.error.issues.map((err) => ({
|
||||
field: err.path.join("."),
|
||||
message: err.message,
|
||||
}));
|
||||
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: "Dados de entrada inválidos",
|
||||
details: errors,
|
||||
},
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
const validatedData = validationResult.data;
|
||||
|
||||
// Atualizar anúncio
|
||||
const updatedListing = await updateListing(listingId, validatedData);
|
||||
|
||||
if (!updatedListing) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: "Erro ao atualizar anúncio",
|
||||
},
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
|
||||
secureLogger.info("✅ Anúncio atualizado com sucesso", {
|
||||
listingId,
|
||||
userId: auth.userId,
|
||||
timestamp: new Date().toISOString(),
|
||||
});
|
||||
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: true,
|
||||
listing: updatedListing,
|
||||
},
|
||||
{ status: 200 }
|
||||
);
|
||||
} catch (error) {
|
||||
secureLogger.error("❌ Erro ao atualizar anúncio", {
|
||||
error: error instanceof Error ? error.message : "Erro desconhecido",
|
||||
stack: error instanceof Error ? error.stack : undefined,
|
||||
});
|
||||
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: "Erro ao atualizar anúncio",
|
||||
},
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* DELETE /api/listings/[id]
|
||||
* Cancela anúncio (apenas dono)
|
||||
*/
|
||||
export async function DELETE(
|
||||
request: NextRequest,
|
||||
{ params }: { params: Promise<{ id: string }> }
|
||||
) {
|
||||
try {
|
||||
// Autenticação obrigatória
|
||||
const authResult = await requireAuth(request);
|
||||
|
||||
if (authResult instanceof NextResponse) {
|
||||
return authResult;
|
||||
}
|
||||
|
||||
const auth = authResult;
|
||||
|
||||
const { id } = await params;
|
||||
const listingId = parseInt(id, 10);
|
||||
|
||||
if (isNaN(listingId)) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: "ID de anúncio inválido",
|
||||
},
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
// Verificar se o anúncio existe e pertence ao usuário
|
||||
const listing = await getListingById(listingId);
|
||||
|
||||
if (!listing) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: "Anúncio não encontrado",
|
||||
},
|
||||
{ status: 404 }
|
||||
);
|
||||
}
|
||||
|
||||
if (listing.seller_id !== auth.userId) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: "Você não tem permissão para cancelar este anúncio",
|
||||
},
|
||||
{ status: 403 }
|
||||
);
|
||||
}
|
||||
|
||||
// Cancelar anúncio (soft delete)
|
||||
const deleted = await deleteListing(listingId);
|
||||
|
||||
if (!deleted) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: "Erro ao cancelar anúncio",
|
||||
},
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
|
||||
secureLogger.info("✅ Anúncio cancelado com sucesso", {
|
||||
listingId,
|
||||
userId: auth.userId,
|
||||
timestamp: new Date().toISOString(),
|
||||
});
|
||||
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: true,
|
||||
message: "Anúncio cancelado com sucesso",
|
||||
},
|
||||
{ status: 200 }
|
||||
);
|
||||
} catch (error) {
|
||||
secureLogger.error("❌ Erro ao cancelar anúncio", {
|
||||
error: error instanceof Error ? error.message : "Erro desconhecido",
|
||||
stack: error instanceof Error ? error.stack : undefined,
|
||||
});
|
||||
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: "Erro ao cancelar anúncio",
|
||||
},
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { secureLogger } from "@/lib/secure-logger";
|
||||
import { incrementViews } from "@/lib/db/queries/listings";
|
||||
|
||||
/**
|
||||
* POST /api/listings/[id]/views
|
||||
* Incrementa contador de views de um anúncio
|
||||
* Não requer autenticação (público)
|
||||
*/
|
||||
export async function POST(
|
||||
request: NextRequest,
|
||||
{ params }: { params: Promise<{ id: string }> }
|
||||
) {
|
||||
try {
|
||||
const { id } = await params;
|
||||
const listingId = parseInt(id, 10);
|
||||
|
||||
if (isNaN(listingId)) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: "ID de anúncio inválido",
|
||||
},
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
// Incrementar views
|
||||
await incrementViews(listingId);
|
||||
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: true,
|
||||
message: "View incrementada",
|
||||
},
|
||||
{ status: 200 }
|
||||
);
|
||||
} catch (error) {
|
||||
secureLogger.error("❌ Erro ao incrementar views", {
|
||||
error: error instanceof Error ? error.message : "Erro desconhecido",
|
||||
stack: error instanceof Error ? error.stack : undefined,
|
||||
});
|
||||
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: "Erro ao incrementar views",
|
||||
},
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,204 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import {
|
||||
checkRateLimit,
|
||||
getClientIP,
|
||||
createRateLimitResponse,
|
||||
} from "@/lib/rate-limiter";
|
||||
import { secureLogger } from "@/lib/secure-logger";
|
||||
import { createListingSchema } from "@/lib/validation-schemas";
|
||||
import { requireAuth, optionalAuth } from "@/lib/auth/middleware";
|
||||
import {
|
||||
getListings,
|
||||
createListing,
|
||||
getActiveListings,
|
||||
} from "@/lib/db/queries/listings";
|
||||
import { isServerActive } from "@/lib/db/queries/servers";
|
||||
|
||||
/**
|
||||
* GET /api/listings
|
||||
* Lista anúncios com filtros opcionais
|
||||
*/
|
||||
export async function GET(request: NextRequest) {
|
||||
try {
|
||||
const { searchParams } = new URL(request.url);
|
||||
|
||||
const filters = {
|
||||
status: searchParams.get("status") as
|
||||
| "active"
|
||||
| "sold"
|
||||
| "cancelled"
|
||||
| "in_escrow"
|
||||
| "expired"
|
||||
| undefined,
|
||||
server_name: searchParams.get("server_name") || undefined,
|
||||
item_id: searchParams.get("item_id")
|
||||
? parseInt(searchParams.get("item_id")!, 10)
|
||||
: undefined,
|
||||
seller_id: searchParams.get("seller_id")
|
||||
? parseInt(searchParams.get("seller_id")!, 10)
|
||||
: undefined,
|
||||
is_auction:
|
||||
searchParams.get("is_auction") === "true"
|
||||
? true
|
||||
: searchParams.get("is_auction") === "false"
|
||||
? false
|
||||
: undefined,
|
||||
search: searchParams.get("search") || undefined,
|
||||
page: searchParams.get("page")
|
||||
? parseInt(searchParams.get("page")!, 10)
|
||||
: 1,
|
||||
pageSize: searchParams.get("pageSize")
|
||||
? parseInt(searchParams.get("pageSize")!, 10)
|
||||
: 20,
|
||||
sortBy: (searchParams.get("sortBy") ||
|
||||
"created_at") as
|
||||
| "created_at"
|
||||
| "price_pix"
|
||||
| "price_tibia_coins"
|
||||
| "price_gold_coins"
|
||||
| "views_count",
|
||||
sortOrder: (searchParams.get("sortOrder") || "DESC") as "ASC" | "DESC",
|
||||
};
|
||||
|
||||
// Se não especificar status, usar apenas ativos por padrão
|
||||
const listingsData = filters.status
|
||||
? await getListings(filters)
|
||||
: await getActiveListings(filters);
|
||||
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: true,
|
||||
listings: listingsData.listings,
|
||||
pagination: {
|
||||
page: filters.page,
|
||||
pageSize: filters.pageSize,
|
||||
total: listingsData.total,
|
||||
totalPages: Math.ceil(listingsData.total / filters.pageSize),
|
||||
},
|
||||
},
|
||||
{ status: 200 }
|
||||
);
|
||||
} catch (error) {
|
||||
secureLogger.error("❌ Erro ao buscar anúncios", {
|
||||
error: error instanceof Error ? error.message : "Erro desconhecido",
|
||||
stack: error instanceof Error ? error.stack : undefined,
|
||||
});
|
||||
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: "Erro ao buscar anúncios",
|
||||
},
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* POST /api/listings
|
||||
* Cria um novo anúncio (requer autenticação)
|
||||
*/
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
// Rate limiting
|
||||
const ip = getClientIP(request);
|
||||
const rateLimit = checkRateLimit(ip, "/api/listings");
|
||||
|
||||
if (!rateLimit.allowed) {
|
||||
return createRateLimitResponse(rateLimit.resetAt);
|
||||
}
|
||||
|
||||
// Autenticação obrigatória
|
||||
const authResult = await requireAuth(request);
|
||||
|
||||
if (authResult instanceof NextResponse) {
|
||||
return authResult;
|
||||
}
|
||||
|
||||
const auth = authResult;
|
||||
|
||||
const body = await request.json();
|
||||
|
||||
secureLogger.info("📥 POST /api/listings - Recebido", {
|
||||
userId: auth.userId,
|
||||
ip,
|
||||
timestamp: new Date().toISOString(),
|
||||
});
|
||||
|
||||
// Validação com Zod
|
||||
const validationResult = createListingSchema.safeParse(body);
|
||||
|
||||
if (!validationResult.success) {
|
||||
const errors = validationResult.error.issues.map((err) => ({
|
||||
field: err.path.join("."),
|
||||
message: err.message,
|
||||
}));
|
||||
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: "Dados de entrada inválidos",
|
||||
details: errors,
|
||||
},
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
const validatedData = validationResult.data;
|
||||
|
||||
// Validar se o servidor existe e está ativo
|
||||
const serverActive = await isServerActive(validatedData.server_name);
|
||||
if (!serverActive) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: `Servidor "${validatedData.server_name}" não existe ou não está ativo`,
|
||||
},
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
// Criar anúncio
|
||||
const listing = await createListing({
|
||||
seller_id: auth.userId,
|
||||
item_id: validatedData.item_id,
|
||||
item_name: validatedData.item_name,
|
||||
quantity: validatedData.quantity,
|
||||
server_name: validatedData.server_name,
|
||||
price_pix: validatedData.price_pix ?? undefined,
|
||||
price_tibia_coins: validatedData.price_tibia_coins ?? undefined,
|
||||
price_gold_coins: validatedData.price_gold_coins ?? undefined,
|
||||
is_auction: validatedData.is_auction,
|
||||
auction_ends_at: validatedData.auction_ends_at ?? undefined,
|
||||
description: validatedData.description ?? undefined,
|
||||
});
|
||||
|
||||
secureLogger.info("✅ Anúncio criado com sucesso", {
|
||||
listingId: listing.listing_id,
|
||||
userId: auth.userId,
|
||||
timestamp: new Date().toISOString(),
|
||||
});
|
||||
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: true,
|
||||
listing,
|
||||
},
|
||||
{ status: 201 }
|
||||
);
|
||||
} catch (error) {
|
||||
secureLogger.error("❌ Erro ao criar anúncio", {
|
||||
error: error instanceof Error ? error.message : "Erro desconhecido",
|
||||
stack: error instanceof Error ? error.stack : undefined,
|
||||
});
|
||||
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: "Erro ao criar anúncio",
|
||||
},
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { getActiveServers } from "@/lib/db/queries/servers";
|
||||
import { secureLogger } from "@/lib/secure-logger";
|
||||
|
||||
/**
|
||||
* GET /api/servers
|
||||
* Lista todos os servidores ativos disponíveis
|
||||
*/
|
||||
export async function GET(request: NextRequest) {
|
||||
try {
|
||||
const servers = await getActiveServers();
|
||||
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: true,
|
||||
servers: servers.map((server) => ({
|
||||
serverId: server.server_id,
|
||||
serverName: server.server_name,
|
||||
serverType: server.server_type,
|
||||
location: server.location,
|
||||
isActive: server.is_active,
|
||||
stats: {
|
||||
totalListings: server.total_listings,
|
||||
totalUsers: server.total_users,
|
||||
totalSubscribers: server.total_subscribers,
|
||||
totalTransactions: server.total_transactions,
|
||||
totalVolume: Number(server.total_volume),
|
||||
averageListingPrice: Number(server.average_listing_price),
|
||||
lastActivityAt: server.last_activity_at,
|
||||
},
|
||||
})),
|
||||
},
|
||||
{ status: 200 }
|
||||
);
|
||||
} catch (error) {
|
||||
secureLogger.error("❌ Erro ao buscar servidores", {
|
||||
error: error instanceof Error ? error.message : "Erro desconhecido",
|
||||
stack: error instanceof Error ? error.stack : undefined,
|
||||
});
|
||||
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: "Erro ao buscar servidores",
|
||||
},
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { secureLogger } from "@/lib/secure-logger";
|
||||
import { getUserById } from "@/lib/db/queries/users";
|
||||
|
||||
export async function GET(
|
||||
request: NextRequest,
|
||||
{ params }: { params: Promise<{ id: string }> }
|
||||
) {
|
||||
try {
|
||||
const { id } = await params;
|
||||
const userId = parseInt(id, 10);
|
||||
|
||||
if (isNaN(userId)) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: "ID de usuário inválido",
|
||||
},
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
const user = await getUserById(userId);
|
||||
|
||||
if (!user) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: "Usuário não encontrado",
|
||||
},
|
||||
{ status: 404 }
|
||||
);
|
||||
}
|
||||
|
||||
// Retornar apenas dados públicos (sem informações sensíveis)
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: true,
|
||||
user: {
|
||||
userId: user.user_id,
|
||||
username: user.username,
|
||||
firstName: user.first_name,
|
||||
lastName: user.last_name,
|
||||
accountLevel: user.account_level,
|
||||
hasDonated: user.has_donated,
|
||||
createdAt: user.created_at,
|
||||
},
|
||||
},
|
||||
{ status: 200 }
|
||||
);
|
||||
} catch (error) {
|
||||
secureLogger.error("❌ Erro ao buscar perfil público", {
|
||||
error: error instanceof Error ? error.message : "Erro desconhecido",
|
||||
stack: error instanceof Error ? error.stack : undefined,
|
||||
timestamp: new Date().toISOString(),
|
||||
});
|
||||
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : "Erro interno",
|
||||
},
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,205 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { secureLogger } from "@/lib/secure-logger";
|
||||
import { requireAuth } from "@/lib/auth/middleware";
|
||||
import {
|
||||
updateCharacter,
|
||||
deleteCharacter,
|
||||
getCharacterById,
|
||||
} from "@/lib/db/queries/characters";
|
||||
import { characterSchema } from "@/lib/validation-schemas";
|
||||
|
||||
export async function PUT(
|
||||
request: NextRequest,
|
||||
{ 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)) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: "ID de personagem inválido",
|
||||
},
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
const body = await request.json();
|
||||
|
||||
// Validação com Zod
|
||||
const validationResult = characterSchema.safeParse(body);
|
||||
|
||||
if (!validationResult.success) {
|
||||
const errors = validationResult.error.issues.map((err) => ({
|
||||
field: err.path.join("."),
|
||||
message: err.message,
|
||||
}));
|
||||
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: "Dados de entrada inválidos",
|
||||
details: errors,
|
||||
},
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
const validatedData = validationResult.data;
|
||||
|
||||
// Atualizar personagem (verifica ownership internamente)
|
||||
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,
|
||||
characterId: updatedCharacter.character_id,
|
||||
timestamp: new Date().toISOString(),
|
||||
});
|
||||
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: true,
|
||||
character: {
|
||||
characterId: updatedCharacter.character_id,
|
||||
characterName: updatedCharacter.character_name,
|
||||
serverName: updatedCharacter.server_name,
|
||||
isMain: updatedCharacter.is_main,
|
||||
verified: updatedCharacter.verified,
|
||||
createdAt: updatedCharacter.created_at,
|
||||
},
|
||||
},
|
||||
{ status: 200 }
|
||||
);
|
||||
} catch (error) {
|
||||
// Se for NextResponse (erro de autenticação), re-lançar
|
||||
if (error instanceof NextResponse) {
|
||||
return error;
|
||||
}
|
||||
|
||||
// Verificar se é erro de permissão
|
||||
if (
|
||||
error instanceof Error &&
|
||||
error.message.includes("não tem permissão")
|
||||
) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: error.message,
|
||||
},
|
||||
{ status: 403 }
|
||||
);
|
||||
}
|
||||
|
||||
secureLogger.error("❌ Erro ao atualizar personagem", {
|
||||
error: error instanceof Error ? error.message : "Erro desconhecido",
|
||||
stack: error instanceof Error ? error.stack : undefined,
|
||||
timestamp: new Date().toISOString(),
|
||||
});
|
||||
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : "Erro interno",
|
||||
},
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export async function DELETE(
|
||||
request: NextRequest,
|
||||
{ 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)) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: "ID de personagem inválido",
|
||||
},
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
// Remover personagem (verifica ownership internamente)
|
||||
await deleteCharacter(characterId, auth.userId);
|
||||
|
||||
secureLogger.info("✅ Personagem removido com sucesso", {
|
||||
userId: auth.userId,
|
||||
characterId,
|
||||
timestamp: new Date().toISOString(),
|
||||
});
|
||||
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: true,
|
||||
},
|
||||
{ status: 200 }
|
||||
);
|
||||
} catch (error) {
|
||||
// Se for NextResponse (erro de autenticação), re-lançar
|
||||
if (error instanceof NextResponse) {
|
||||
return error;
|
||||
}
|
||||
|
||||
// Verificar se é erro de permissão
|
||||
if (
|
||||
error instanceof Error &&
|
||||
error.message.includes("não tem permissão")
|
||||
) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: error.message,
|
||||
},
|
||||
{ status: 403 }
|
||||
);
|
||||
}
|
||||
|
||||
secureLogger.error("❌ Erro ao remover personagem", {
|
||||
error: error instanceof Error ? error.message : "Erro desconhecido",
|
||||
stack: error instanceof Error ? error.stack : undefined,
|
||||
timestamp: new Date().toISOString(),
|
||||
});
|
||||
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : "Erro interno",
|
||||
},
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,150 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { secureLogger } from "@/lib/secure-logger";
|
||||
import { requireAuth } from "@/lib/auth/middleware";
|
||||
import { getUserCharacters, createCharacter } from "@/lib/db/queries/characters";
|
||||
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
|
||||
const characters = await getUserCharacters(auth.userId);
|
||||
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: true,
|
||||
characters: characters.map((char) => ({
|
||||
characterId: char.character_id,
|
||||
characterName: char.character_name,
|
||||
serverName: char.server_name,
|
||||
isMain: char.is_main,
|
||||
verified: char.verified,
|
||||
createdAt: char.created_at,
|
||||
})),
|
||||
},
|
||||
{ status: 200 }
|
||||
);
|
||||
} catch (error) {
|
||||
// Se for NextResponse (erro de autenticação), re-lançar
|
||||
if (error instanceof NextResponse) {
|
||||
return error;
|
||||
}
|
||||
|
||||
secureLogger.error("❌ Erro ao listar personagens", {
|
||||
error: error instanceof Error ? error.message : "Erro desconhecido",
|
||||
stack: error instanceof Error ? error.stack : undefined,
|
||||
timestamp: new Date().toISOString(),
|
||||
});
|
||||
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : "Erro interno",
|
||||
},
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
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();
|
||||
|
||||
// Validação com Zod
|
||||
const validationResult = characterSchema.safeParse(body);
|
||||
|
||||
if (!validationResult.success) {
|
||||
const errors = validationResult.error.issues.map((err) => ({
|
||||
field: err.path.join("."),
|
||||
message: err.message,
|
||||
}));
|
||||
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: "Dados de entrada inválidos",
|
||||
details: errors,
|
||||
},
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
const validatedData = validationResult.data;
|
||||
|
||||
// Criar personagem
|
||||
const newCharacter = await createCharacter(auth.userId, {
|
||||
character_name: validatedData.characterName,
|
||||
server_name: validatedData.serverName,
|
||||
is_main: validatedData.isMain,
|
||||
});
|
||||
|
||||
secureLogger.info("✅ Personagem criado com sucesso", {
|
||||
userId: auth.userId,
|
||||
characterId: newCharacter.character_id,
|
||||
timestamp: new Date().toISOString(),
|
||||
});
|
||||
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: true,
|
||||
character: {
|
||||
characterId: newCharacter.character_id,
|
||||
characterName: newCharacter.character_name,
|
||||
serverName: newCharacter.server_name,
|
||||
isMain: newCharacter.is_main,
|
||||
verified: newCharacter.verified,
|
||||
createdAt: newCharacter.created_at,
|
||||
},
|
||||
},
|
||||
{ status: 201 }
|
||||
);
|
||||
} catch (error) {
|
||||
// Se for NextResponse (erro de autenticação), re-lançar
|
||||
if (error instanceof NextResponse) {
|
||||
return error;
|
||||
}
|
||||
|
||||
// Verificar se é erro de constraint (personagem duplicado)
|
||||
if (error instanceof Error && error.message.includes("UNIQUE")) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: "Este personagem já está cadastrado neste servidor",
|
||||
},
|
||||
{ status: 409 }
|
||||
);
|
||||
}
|
||||
|
||||
secureLogger.error("❌ Erro ao criar personagem", {
|
||||
error: error instanceof Error ? error.message : "Erro desconhecido",
|
||||
stack: error instanceof Error ? error.stack : undefined,
|
||||
timestamp: new Date().toISOString(),
|
||||
});
|
||||
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : "Erro interno",
|
||||
},
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,179 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { secureLogger } from "@/lib/secure-logger";
|
||||
import { requireAuth } from "@/lib/auth/middleware";
|
||||
import { getUserById, updateUser } from "@/lib/db/queries/users";
|
||||
import { getUserCharacters } from "@/lib/db/queries/characters";
|
||||
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
|
||||
const user = await getUserById(auth.userId);
|
||||
|
||||
if (!user) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: "Usuário não encontrado",
|
||||
},
|
||||
{ status: 404 }
|
||||
);
|
||||
}
|
||||
|
||||
// Buscar personagens do usuário
|
||||
const characters = await getUserCharacters(auth.userId);
|
||||
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: true,
|
||||
user: {
|
||||
userId: user.user_id,
|
||||
username: user.username,
|
||||
email: user.email,
|
||||
firstName: user.first_name,
|
||||
lastName: user.last_name,
|
||||
phone: user.phone,
|
||||
cpf: user.cpf,
|
||||
accountLevel: user.account_level,
|
||||
hasDonated: user.has_donated,
|
||||
whatsappNotifications: user.whatsapp_notifications,
|
||||
createdAt: user.created_at,
|
||||
updatedAt: user.updated_at,
|
||||
lastLogin: user.last_login,
|
||||
isActive: user.is_active,
|
||||
isVerified: user.is_verified,
|
||||
},
|
||||
characters: characters.map((char) => ({
|
||||
characterId: char.character_id,
|
||||
characterName: char.character_name,
|
||||
serverName: char.server_name,
|
||||
isMain: char.is_main,
|
||||
verified: char.verified,
|
||||
createdAt: char.created_at,
|
||||
})),
|
||||
},
|
||||
{ status: 200 }
|
||||
);
|
||||
} catch (error) {
|
||||
// Se for NextResponse (erro de autenticação), re-lançar
|
||||
if (error instanceof NextResponse) {
|
||||
return error;
|
||||
}
|
||||
|
||||
secureLogger.error("❌ Erro ao buscar perfil do usuário", {
|
||||
error: error instanceof Error ? error.message : "Erro desconhecido",
|
||||
stack: error instanceof Error ? error.stack : undefined,
|
||||
timestamp: new Date().toISOString(),
|
||||
});
|
||||
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : "Erro interno",
|
||||
},
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
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();
|
||||
|
||||
// Validação com Zod
|
||||
const validationResult = updateUserSchema.safeParse(body);
|
||||
|
||||
if (!validationResult.success) {
|
||||
const errors = validationResult.error.issues.map((err) => ({
|
||||
field: err.path.join("."),
|
||||
message: err.message,
|
||||
}));
|
||||
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: "Dados de entrada inválidos",
|
||||
details: errors,
|
||||
},
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
const validatedData = validationResult.data;
|
||||
|
||||
// Atualizar usuário
|
||||
const updatedUser = await updateUser(auth.userId, {
|
||||
first_name: validatedData.firstName,
|
||||
last_name: validatedData.lastName,
|
||||
phone: validatedData.phone,
|
||||
whatsapp_notifications: validatedData.whatsappNotifications,
|
||||
});
|
||||
|
||||
secureLogger.info("✅ Perfil atualizado com sucesso", {
|
||||
userId: auth.userId,
|
||||
timestamp: new Date().toISOString(),
|
||||
});
|
||||
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: true,
|
||||
user: {
|
||||
userId: updatedUser.user_id,
|
||||
username: updatedUser.username,
|
||||
email: updatedUser.email,
|
||||
firstName: updatedUser.first_name,
|
||||
lastName: updatedUser.last_name,
|
||||
phone: updatedUser.phone,
|
||||
cpf: updatedUser.cpf,
|
||||
accountLevel: updatedUser.account_level,
|
||||
hasDonated: updatedUser.has_donated,
|
||||
whatsappNotifications: updatedUser.whatsapp_notifications,
|
||||
createdAt: updatedUser.created_at,
|
||||
updatedAt: updatedUser.updated_at,
|
||||
lastLogin: updatedUser.last_login,
|
||||
isActive: updatedUser.is_active,
|
||||
isVerified: updatedUser.is_verified,
|
||||
},
|
||||
},
|
||||
{ status: 200 }
|
||||
);
|
||||
} catch (error) {
|
||||
// Se for NextResponse (erro de autenticação), re-lançar
|
||||
if (error instanceof NextResponse) {
|
||||
return error;
|
||||
}
|
||||
|
||||
secureLogger.error("❌ Erro ao atualizar perfil", {
|
||||
error: error instanceof Error ? error.message : "Erro desconhecido",
|
||||
stack: error instanceof Error ? error.stack : undefined,
|
||||
timestamp: new Date().toISOString(),
|
||||
});
|
||||
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : "Erro interno",
|
||||
},
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user