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.
271 lines
7.4 KiB
TypeScript
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 }
|
|
);
|
|
}
|
|
}
|
|
|