187 lines
4.1 KiB
TypeScript
187 lines
4.1 KiB
TypeScript
/**
|
|
* Middleware de autenticação
|
|
* Extrai e valida tokens JWT das requisições
|
|
*/
|
|
|
|
import { cookies } from "next/headers";
|
|
import { type NextRequest, NextResponse } from "next/server";
|
|
import { getSession } from "@/lib/redis/session";
|
|
import { verifyAccessToken } from "./jwt";
|
|
|
|
export interface AuthenticatedUser {
|
|
userId: number;
|
|
username: string;
|
|
tierSlug?: string;
|
|
tierLevel?: number;
|
|
}
|
|
|
|
/**
|
|
* 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 "
|
|
}
|
|
|
|
/**
|
|
* Converte payload de token em objeto AuthenticatedUser.
|
|
*/
|
|
function toAuthenticatedUser(payload: {
|
|
userId: number;
|
|
username: string;
|
|
tierSlug?: string;
|
|
tierLevel?: number;
|
|
}): AuthenticatedUser {
|
|
return {
|
|
userId: payload.userId,
|
|
username: payload.username,
|
|
tierSlug: payload.tierSlug,
|
|
tierLevel: payload.tierLevel,
|
|
};
|
|
}
|
|
|
|
export async function requireAuth(
|
|
request: NextRequest,
|
|
): Promise<
|
|
AuthenticatedUser | NextResponse<{ success: false; error: string }>
|
|
> {
|
|
// Tentar primeiro com Bearer token
|
|
const token = extractToken(request);
|
|
|
|
if (token) {
|
|
const payload = verifyAccessToken(token);
|
|
|
|
if (payload) {
|
|
return toAuthenticatedUser(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 toAuthenticatedUser(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<AuthenticatedUser | null> {
|
|
// Tentar primeiro com Bearer token
|
|
const token = extractToken(request);
|
|
|
|
if (token) {
|
|
const payload = verifyAccessToken(token);
|
|
if (payload) {
|
|
return toAuthenticatedUser(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);
|
|
if (tokenPayload) {
|
|
return toAuthenticatedUser(tokenPayload);
|
|
}
|
|
}
|
|
}
|
|
} catch (_error) {
|
|
// Ignorar erros de cookie
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
export async function requireAdminUser(
|
|
request: NextRequest,
|
|
): Promise<
|
|
AuthenticatedUser | NextResponse<{ success: false; error: string }>
|
|
> {
|
|
const auth = await requireAuth(request);
|
|
|
|
if (auth instanceof NextResponse) {
|
|
return auth;
|
|
}
|
|
|
|
if (auth.userId !== 1) {
|
|
return NextResponse.json(
|
|
{
|
|
success: false,
|
|
error: "Acesso restrito ao administrador.",
|
|
},
|
|
{ status: 403 },
|
|
);
|
|
}
|
|
|
|
return auth;
|
|
}
|
|
|
|
export async function requireTierLevel(
|
|
request: NextRequest,
|
|
minimumLevel: number,
|
|
): Promise<
|
|
AuthenticatedUser | NextResponse<{ success: false; error: string }>
|
|
> {
|
|
const auth = await requireAuth(request);
|
|
|
|
if (auth instanceof NextResponse) {
|
|
return auth;
|
|
}
|
|
|
|
const tierLevel = auth.tierLevel ?? 0;
|
|
|
|
if (tierLevel < minimumLevel) {
|
|
return NextResponse.json(
|
|
{
|
|
success: false,
|
|
error: "Plano atual não possui acesso a este recurso.",
|
|
},
|
|
{ status: 403 },
|
|
);
|
|
}
|
|
|
|
return auth;
|
|
}
|