Files
pagodera/app/api/auth/register/route.ts
T
Evandro Fonseca Junior 55eebde0ba 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.
2025-11-04 02:33:29 -03:00

271 lines
7.4 KiB
TypeScript

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 }
);
}
}