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

+23 -2
View File
@@ -9,6 +9,8 @@ interface TokenPayload {
userId: number;
username: string;
type: "access" | "refresh";
tierSlug?: string;
tierLevel?: number;
}
/**
@@ -45,14 +47,22 @@ const REFRESH_TOKEN_EXPIRY = "7d"; // 7 dias
* @param username - Nome de usuário
* @returns Access token
*/
interface TokenOptions {
tierSlug?: string | null;
tierLevel?: number | null;
}
export function generateAccessToken(
userId: number,
username: string
username: string,
options?: TokenOptions
): string {
const payload: TokenPayload = {
userId,
username,
type: "access",
tierSlug: options?.tierSlug ?? undefined,
tierLevel: options?.tierLevel ?? undefined,
};
return jwt.sign(payload, getJWTSecret(), {
@@ -68,12 +78,15 @@ export function generateAccessToken(
*/
export function generateRefreshToken(
userId: number,
username: string
username: string,
options?: TokenOptions
): string {
const payload: TokenPayload = {
userId,
username,
type: "refresh",
tierSlug: options?.tierSlug ?? undefined,
tierLevel: options?.tierLevel ?? undefined,
};
return jwt.sign(payload, getJWTRefreshSecret(), {
@@ -89,6 +102,8 @@ export function generateRefreshToken(
export function verifyAccessToken(token: string): {
userId: number;
username: string;
tierSlug?: string;
tierLevel?: number;
} | null {
try {
const decoded = jwt.verify(token, getJWTSecret()) as TokenPayload;
@@ -100,6 +115,8 @@ export function verifyAccessToken(token: string): {
return {
userId: decoded.userId,
username: decoded.username,
tierSlug: decoded.tierSlug,
tierLevel: decoded.tierLevel,
};
} catch (error) {
return null;
@@ -114,6 +131,8 @@ export function verifyAccessToken(token: string): {
export function verifyRefreshToken(token: string): {
userId: number;
username: string;
tierSlug?: string;
tierLevel?: number;
} | null {
try {
const decoded = jwt.verify(token, getJWTRefreshSecret()) as TokenPayload;
@@ -125,6 +144,8 @@ export function verifyRefreshToken(token: string): {
return {
userId: decoded.userId,
username: decoded.username,
tierSlug: decoded.tierSlug,
tierLevel: decoded.tierLevel,
};
} catch (error) {
return null;
+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;
}