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.
This commit is contained in:
Evandro Fonseca Junior committed 2025-11-07 21:03:19 -03:00
1 parent 7f42def478
commit d27526e026
20 files changed
+2133 -40

No files matched your search

+76 -15
View File
@@ -8,6 +8,13 @@ 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
@@ -24,18 +31,23 @@ function extractToken(request: NextRequest): string | null {
}
/**
* 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
*/
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<
| { userId: number; username: string }
| NextResponse<{ success: false; error: string }>
> {
): Promise<AuthenticatedUser | NextResponse<{ success: false; error: string }>> {
// Tentar primeiro com Bearer token
const token = extractToken(request);
@@ -43,7 +55,7 @@ export async function requireAuth(
const payload = verifyAccessToken(token);
if (payload) {
return payload;
return toAuthenticatedUser(payload);
}
}
@@ -59,7 +71,7 @@ export async function requireAuth(
const tokenPayload = verifyAccessToken(session.accessToken);
if (tokenPayload) {
return tokenPayload;
return toAuthenticatedUser(tokenPayload);
}
}
}
@@ -85,14 +97,14 @@ export async function requireAuth(
*/
export async function optionalAuth(
request: NextRequest
): Promise<{ userId: number; username: string } | null> {
): Promise<AuthenticatedUser | null> {
// Tentar primeiro com Bearer token
const token = extractToken(request);
if (token) {
const payload = verifyAccessToken(token);
if (payload) {
return payload;
return toAuthenticatedUser(payload);
}
}
@@ -106,7 +118,9 @@ export async function optionalAuth(
if (session) {
const tokenPayload = verifyAccessToken(session.accessToken);
return tokenPayload;
if (tokenPayload) {
return toAuthenticatedUser(tokenPayload);
}
}
}
} catch (error) {
@@ -115,3 +129,50 @@ export async function optionalAuth(
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;
}