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
+133
@@ -0,0 +1,133 @@
|
||||
/**
|
||||
* Módulo de geração e validação de tokens JWT
|
||||
* Access tokens (15min) e Refresh tokens (7 dias)
|
||||
*/
|
||||
|
||||
import jwt from "jsonwebtoken";
|
||||
|
||||
interface TokenPayload {
|
||||
userId: number;
|
||||
username: string;
|
||||
type: "access" | "refresh";
|
||||
}
|
||||
|
||||
/**
|
||||
* Obtém o secret do JWT (lazy loading)
|
||||
*/
|
||||
function getJWTSecret(): string {
|
||||
const secret = process.env.JWT_SECRET;
|
||||
if (!secret) {
|
||||
throw new Error("JWT_SECRET não está definido nas variáveis de ambiente");
|
||||
}
|
||||
return secret;
|
||||
}
|
||||
|
||||
/**
|
||||
* Obtém o refresh secret do JWT (lazy loading)
|
||||
*/
|
||||
function getJWTRefreshSecret(): string {
|
||||
const secret = process.env.JWT_REFRESH_SECRET;
|
||||
if (!secret) {
|
||||
throw new Error(
|
||||
"JWT_REFRESH_SECRET não está definido nas variáveis de ambiente"
|
||||
);
|
||||
}
|
||||
return secret;
|
||||
}
|
||||
|
||||
// Tempos de expiração
|
||||
const ACCESS_TOKEN_EXPIRY = "15m"; // 15 minutos
|
||||
const REFRESH_TOKEN_EXPIRY = "7d"; // 7 dias
|
||||
|
||||
/**
|
||||
* Gera access token JWT
|
||||
* @param userId - ID do usuário
|
||||
* @param username - Nome de usuário
|
||||
* @returns Access token
|
||||
*/
|
||||
export function generateAccessToken(
|
||||
userId: number,
|
||||
username: string
|
||||
): string {
|
||||
const payload: TokenPayload = {
|
||||
userId,
|
||||
username,
|
||||
type: "access",
|
||||
};
|
||||
|
||||
return jwt.sign(payload, getJWTSecret(), {
|
||||
expiresIn: ACCESS_TOKEN_EXPIRY,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Gera refresh token JWT
|
||||
* @param userId - ID do usuário
|
||||
* @param username - Nome de usuário
|
||||
* @returns Refresh token
|
||||
*/
|
||||
export function generateRefreshToken(
|
||||
userId: number,
|
||||
username: string
|
||||
): string {
|
||||
const payload: TokenPayload = {
|
||||
userId,
|
||||
username,
|
||||
type: "refresh",
|
||||
};
|
||||
|
||||
return jwt.sign(payload, getJWTRefreshSecret(), {
|
||||
expiresIn: REFRESH_TOKEN_EXPIRY,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Valida e decodifica access token
|
||||
* @param token - Access token
|
||||
* @returns Payload do token ou null se inválido
|
||||
*/
|
||||
export function verifyAccessToken(token: string): {
|
||||
userId: number;
|
||||
username: string;
|
||||
} | null {
|
||||
try {
|
||||
const decoded = jwt.verify(token, getJWTSecret()) as TokenPayload;
|
||||
|
||||
if (decoded.type !== "access") {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
userId: decoded.userId,
|
||||
username: decoded.username,
|
||||
};
|
||||
} catch (error) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Valida e decodifica refresh token
|
||||
* @param token - Refresh token
|
||||
* @returns Payload do token ou null se inválido
|
||||
*/
|
||||
export function verifyRefreshToken(token: string): {
|
||||
userId: number;
|
||||
username: string;
|
||||
} | null {
|
||||
try {
|
||||
const decoded = jwt.verify(token, getJWTRefreshSecret()) as TokenPayload;
|
||||
|
||||
if (decoded.type !== "refresh") {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
userId: decoded.userId,
|
||||
username: decoded.username,
|
||||
};
|
||||
} catch (error) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,117 @@
|
||||
/**
|
||||
* Middleware de autenticação
|
||||
* Extrai e valida tokens JWT das requisições
|
||||
*/
|
||||
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { cookies } from "next/headers";
|
||||
import { verifyAccessToken } from "./jwt";
|
||||
import { getSession } from "@/lib/redis/session";
|
||||
|
||||
/**
|
||||
* Extrai token do header Authorization
|
||||
* @param request - Request do Next.js
|
||||
* @returns Token ou null
|
||||
*/
|
||||
function extractToken(request: NextRequest): string | null {
|
||||
const authHeader = request.headers.get("authorization");
|
||||
|
||||
if (!authHeader || !authHeader.startsWith("Bearer ")) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return authHeader.substring(7); // Remove "Bearer "
|
||||
}
|
||||
|
||||
/**
|
||||
* Middleware que requer autenticação
|
||||
* Retorna dados do usuário ou NextResponse com erro
|
||||
* Suporta tanto Bearer token quanto cookie sessionId
|
||||
* @param request - Request do Next.js
|
||||
* @returns Dados do usuário autenticado ou NextResponse com erro
|
||||
*/
|
||||
export async function requireAuth(
|
||||
request: NextRequest
|
||||
): Promise<
|
||||
| { userId: number; username: string }
|
||||
| NextResponse<{ success: false; error: string }>
|
||||
> {
|
||||
// Tentar primeiro com Bearer token
|
||||
const token = extractToken(request);
|
||||
|
||||
if (token) {
|
||||
const payload = verifyAccessToken(token);
|
||||
|
||||
if (payload) {
|
||||
return payload;
|
||||
}
|
||||
}
|
||||
|
||||
// Se não tiver token no header, tentar com cookie
|
||||
try {
|
||||
const cookieStore = await cookies();
|
||||
const sessionId = cookieStore.get("sessionId")?.value;
|
||||
|
||||
if (sessionId) {
|
||||
const session = await getSession(sessionId);
|
||||
|
||||
if (session) {
|
||||
const tokenPayload = verifyAccessToken(session.accessToken);
|
||||
|
||||
if (tokenPayload) {
|
||||
return tokenPayload;
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
// Se der erro ao acessar cookies, continuar com o erro de token
|
||||
}
|
||||
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: "Token de autenticação não fornecido ou inválido",
|
||||
},
|
||||
{ status: 401 }
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Middleware opcional de autenticação
|
||||
* Retorna dados do usuário se autenticado, null caso contrário
|
||||
* Não lança erro se não autenticado
|
||||
* @param request - Request do Next.js
|
||||
* @returns Dados do usuário ou null
|
||||
*/
|
||||
export async function optionalAuth(
|
||||
request: NextRequest
|
||||
): Promise<{ userId: number; username: string } | null> {
|
||||
// Tentar primeiro com Bearer token
|
||||
const token = extractToken(request);
|
||||
|
||||
if (token) {
|
||||
const payload = verifyAccessToken(token);
|
||||
if (payload) {
|
||||
return payload;
|
||||
}
|
||||
}
|
||||
|
||||
// Tentar com cookie
|
||||
try {
|
||||
const cookieStore = await cookies();
|
||||
const sessionId = cookieStore.get("sessionId")?.value;
|
||||
|
||||
if (sessionId) {
|
||||
const session = await getSession(sessionId);
|
||||
|
||||
if (session) {
|
||||
const tokenPayload = verifyAccessToken(session.accessToken);
|
||||
return tokenPayload;
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
// Ignorar erros de cookie
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
/**
|
||||
* Módulo de hash e verificação de senhas
|
||||
* Usa bcrypt para hashing seguro
|
||||
*/
|
||||
|
||||
import bcrypt from "bcrypt";
|
||||
|
||||
const SALT_ROUNDS = 12;
|
||||
|
||||
/**
|
||||
* Gera hash de senha usando bcrypt
|
||||
* @param password - Senha em texto plano
|
||||
* @returns Hash da senha
|
||||
*/
|
||||
export async function hashPassword(password: string): Promise<string> {
|
||||
try {
|
||||
const hash = await bcrypt.hash(password, SALT_ROUNDS);
|
||||
return hash;
|
||||
} catch (error) {
|
||||
console.error("❌ Erro ao gerar hash de senha:", error);
|
||||
throw new Error("Falha ao processar senha");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Compara senha em texto plano com hash
|
||||
* @param password - Senha em texto plano
|
||||
* @param hash - Hash armazenado
|
||||
* @returns true se a senha corresponde, false caso contrário
|
||||
*/
|
||||
export async function comparePassword(
|
||||
password: string,
|
||||
hash: string
|
||||
): Promise<boolean> {
|
||||
try {
|
||||
const match = await bcrypt.compare(password, hash);
|
||||
return match;
|
||||
} catch (error) {
|
||||
console.error("❌ Erro ao comparar senha:", error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user