Files
pagodera/lib/auth/middleware.ts
T
Evandro Fonseca Junior d27526e026 feat(auth): include subscription tier in token and session payload
- Adds subscription tier info to access/refresh tokens.
- Stores tier details in session Redis record.
- Returns tier data in login/register response.
- Uses default tier when user has none.
2025-11-07 21:03:19 -03:00

179 lines
4.0 KiB
TypeScript

/**
* 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";
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 "
}
/**
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;
}