Domine middleware e contexto no tRPC: autenticação robusta, autorização granular, logs detalhados, rate limiting e contexto avançado para SaaS empresarial.
Segurança Empresarial: Middleware robusto protege contra ataques e garante compliance com regulamentações.
Controle Granular: Contexto rico permite autorização baseada em roles, organizações e recursos específicos.
// 📁 src/server/trpc/context.ts
import { NextRequest } from 'next/server';
import { getServerSession } from 'next-auth';
import { authOptions } from '@/lib/auth';
import { prisma } from '@/lib/prisma';
import { Redis } from 'ioredis';
import { Logger } from '@/lib/logger';
const redis = new Redis(process.env.REDIS_URL!);
const logger = new Logger('tRPC Context');
// 🎯 Interface do contexto
export interface Context {
// 👤 Dados do usuário
session: {
user: {
id: string;
email: string;
name: string;
role: 'USER' | 'ADMIN' | 'SUPER_ADMIN';
organizationId?: string;
permissions: string[];
};
} | null;
// 🏢 Dados da organização
organization: {
id: string;
name: string;
plan: 'FREE' | 'PRO' | 'ENTERPRISE';
features: string[];
limits: {
apiCalls: number;
storage: number;
users: number;
};
} | null;
// 🔧 Serviços
prisma: typeof prisma;
redis: Redis;
logger: Logger;
// 📊 Metadados da requisição
req: NextRequest;
ip: string;
userAgent: string;
timestamp: number;
traceId: string;
// 🎯 Utilitários
utils: {
hasPermission: (permission: string) => boolean;
hasRole: (role: string) => boolean;
canAccessResource: (resource: string, action: string) => boolean;
getRateLimitInfo: () => Promise<{ remaining: number; resetTime: number }>;
};
}
// 🔧 Função para criar contexto
export async function createContext({
req,
res,
}: {
req: NextRequest;
res: Response;
}): Promise<Context> {
const startTime = Date.now();
// 📊 Extrair metadados da requisição
const ip = getClientIP(req);
const userAgent = req.headers.get('user-agent') || 'unknown';
const traceId = req.headers.get('x-trace-id') || generateTraceId();
logger.info('Creating context', {
ip,
userAgent,
traceId,
path: req.url,
});
// 👤 Obter sessão do usuário
const session = await getServerSession(authOptions);
let organization = null;
let permissions: string[] = [];
if (session?.user) {
try {
// 🏢 Buscar dados da organização
const user = await prisma.user.findUnique({
where: { id: session.user.id },
include: {
organization: {
include: {
plan: true,
features: true,
},
},
permissions: true,
},
});
if (user?.organization) {
organization = {
id: user.organization.id,
name: user.organization.name,
plan: user.organization.plan.name as 'FREE' | 'PRO' | 'ENTERPRISE',
features: user.organization.features.map(f => f.name),
limits: {
apiCalls: user.organization.plan.apiCallsLimit,
storage: user.organization.plan.storageLimit,
users: user.organization.plan.usersLimit,
},
};
}
// 🔒 Carregar permissões do usuário
permissions = user?.permissions.map(p => p.name) || [];
} catch (error) {
logger.error('Error loading user data', error);
}
}
// 🎯 Criar utilitários
const utils = {
// 🔒 Verificar permissão
hasPermission: (permission: string): boolean => {
if (!session) return false;
// 🚀 Super admin tem todas as permissões
if (session.user.role === 'SUPER_ADMIN') return true;
// 📋 Verificar permissões específicas
return permissions.includes(permission);
},
// 👤 Verificar role
hasRole: (role: string): boolean => {
if (!session) return false;
const roleHierarchy = {
USER: 1,
ADMIN: 2,
SUPER_ADMIN: 3,
};
const userLevel = roleHierarchy[session.user.role];
const requiredLevel = roleHierarchy[role as keyof typeof roleHierarchy];
return userLevel >= requiredLevel;
},
// 📊 Verificar acesso a recurso
canAccessResource: (resource: string, action: string): boolean => {
if (!session) return false;
const permission = `${resource}:${action}`;
return utils.hasPermission(permission);
},
// 🚫 Obter info de rate limit
getRateLimitInfo: async (): Promise<{ remaining: number; resetTime: number }> => {
if (!session) return { remaining: 0, resetTime: 0 };
const key = `rate_limit:${session.user.id}`;
const current = await redis.get(key);
const ttl = await redis.ttl(key);
const limit = organization?.limits.apiCalls || 1000;
const remaining = limit - (current ? parseInt(current) : 0);
const resetTime = Date.now() + (ttl * 1000);
return { remaining, resetTime };
},
};
const context: Context = {
session: session ? {
user: {
id: session.user.id,
email: session.user.email,
name: session.user.name,
role: session.user.role,
organizationId: organization?.id,
permissions,
},
} : null,
organization,
prisma,
redis,
logger,
req,
ip,
userAgent,
timestamp: startTime,
traceId,
utils,
};
const duration = Date.now() - startTime;
logger.info('Context created', {
duration,
userId: session?.user?.id,
organizationId: organization?.id,
traceId,
});
return context;
}
// 🔧 Utilitários para contexto
function getClientIP(req: NextRequest): string {
const forwarded = req.headers.get('x-forwarded-for');
const real = req.headers.get('x-real-ip');
if (forwarded) {
return forwarded.split(',')[0].trim();
}
if (real) {
return real;
}
return req.ip || 'unknown';
}
function generateTraceId(): string {
return `trace-${Date.now()}-${Math.random().toString(36).substring(2, 9)}`;
}
// 📁 src/server/trpc/middlewares/auth.ts
import { TRPCError } from '@trpc/server';
import { middleware } from '../trpc';
import type { Context } from '../context';
// 🔒 Middleware de autenticação básica
export const authMiddleware = middleware(async ({ ctx, next }) => {
if (!ctx.session) {
throw new TRPCError({
code: 'UNAUTHORIZED',
message: 'Você precisa estar logado para acessar este recurso',
});
}
return next({
ctx: ctx as Context & { session: NonNullable<Context['session']> },
});
});
// 👑 Middleware de admin
export const adminMiddleware = middleware(async ({ ctx, next }) => {
if (!ctx.session) {
throw new TRPCError({
code: 'UNAUTHORIZED',
message: 'Você precisa estar logado para acessar este recurso',
});
}
if (!ctx.utils.hasRole('ADMIN')) {
throw new TRPCError({
code: 'FORBIDDEN',
message: 'Você não tem permissão para acessar este recurso',
});
}
return next();
});
// 🔒 Middleware de permissão específica
export function permissionMiddleware(requiredPermission: string) {
return middleware(async ({ ctx, next }) => {
if (!ctx.session) {
throw new TRPCError({
code: 'UNAUTHORIZED',
message: 'Você precisa estar logado para acessar este recurso',
});
}
if (!ctx.utils.hasPermission(requiredPermission)) {
ctx.logger.warn('Permission denied', {
userId: ctx.session.user.id,
requiredPermission,
userPermissions: ctx.session.user.permissions,
});
throw new TRPCError({
code: 'FORBIDDEN',
message: `Você não tem a permissão necessária: ${requiredPermission}`,
});
}
return next();
});
}
// 🏢 Middleware de organização
export const organizationMiddleware = middleware(async ({ ctx, next }) => {
if (!ctx.session) {
throw new TRPCError({
code: 'UNAUTHORIZED',
message: 'Você precisa estar logado para acessar este recurso',
});
}
if (!ctx.organization) {
throw new TRPCError({
code: 'FORBIDDEN',
message: 'Você precisa estar associado a uma organização',
});
}
return next();
});
// 📊 Middleware de feature flag
export function featureMiddleware(featureName: string) {
return middleware(async ({ ctx, next }) => {
if (!ctx.session) {
throw new TRPCError({
code: 'UNAUTHORIZED',
message: 'Você precisa estar logado para acessar este recurso',
});
}
if (!ctx.organization) {
throw new TRPCError({
code: 'FORBIDDEN',
message: 'Você precisa estar associado a uma organização',
});
}
if (!ctx.organization.features.includes(featureName)) {
throw new TRPCError({
code: 'FORBIDDEN',
message: `Esta funcionalidade não está disponível no seu plano: ${featureName}`,
});
}
return next();
});
}
// 🎯 Middleware de propriedade de recurso
export function resourceOwnershipMiddleware(
getResourceUserId: (input: any, ctx: Context) => Promise<string | null>
) {
return middleware(async ({ ctx, input, next }) => {
if (!ctx.session) {
throw new TRPCError({
code: 'UNAUTHORIZED',
message: 'Você precisa estar logado para acessar este recurso',
});
}
// 🚀 Admin pode acessar qualquer recurso
if (ctx.utils.hasRole('ADMIN')) {
return next();
}
// 🔍 Verificar propriedade do recurso
const resourceUserId = await getResourceUserId(input, ctx);
if (!resourceUserId) {
throw new TRPCError({
code: 'NOT_FOUND',
message: 'Recurso não encontrado',
});
}
if (resourceUserId !== ctx.session.user.id) {
throw new TRPCError({
code: 'FORBIDDEN',
message: 'Você não tem permissão para acessar este recurso',
});
}
return next();
});
}
// 📁 src/server/trpc/middlewares/rateLimit.ts
import { TRPCError } from '@trpc/server';
import { middleware } from '../trpc';
import type { Context } from '../context';
// 🎯 Interface para configuração de rate limit
interface RateLimitConfig {
maxRequests: number;
windowMs: number;
keyGenerator?: (ctx: Context) => string;
skipSuccessfulRequests?: boolean;
skipFailedRequests?: boolean;
message?: string;
}
// 🚫 Middleware de rate limiting
export function rateLimitMiddleware(config: RateLimitConfig) {
return middleware(async ({ ctx, next }) => {
const {
maxRequests,
windowMs,
keyGenerator = (ctx) => ctx.ip,
skipSuccessfulRequests = false,
skipFailedRequests = false,
message = 'Muitas requisições. Tente novamente mais tarde.',
} = config;
const key = `rate_limit:${keyGenerator(ctx)}`;
const windowStart = Date.now();
const windowEnd = windowStart + windowMs;
// 🔍 Verificar limite atual
const pipeline = ctx.redis.pipeline();
// 🧹 Remover entradas antigas
pipeline.zremrangebyscore(key, 0, windowStart - windowMs);
// 📊 Contar requisições na janela atual
pipeline.zcard(key);
// ⏰ Definir expiração da chave
pipeline.expire(key, Math.ceil(windowMs / 1000));
const results = await pipeline.exec();
const currentRequests = results?.[1]?.[1] as number || 0;
// 🚫 Verificar se excedeu o limite
if (currentRequests >= maxRequests) {
const resetTime = await ctx.redis.zrange(key, 0, 0, 'WITHSCORES');
const nextReset = resetTime[1] ? parseInt(resetTime[1]) + windowMs : windowEnd;
ctx.logger.warn('Rate limit exceeded', {
key,
currentRequests,
maxRequests,
userId: ctx.session?.user?.id,
ip: ctx.ip,
});
throw new TRPCError({
code: 'TOO_MANY_REQUESTS',
message,
cause: {
retryAfter: Math.ceil((nextReset - Date.now()) / 1000),
limit: maxRequests,
remaining: 0,
reset: nextReset,
},
});
}
// 🔄 Executar próximo middleware
let success = true;
let result: any;
try {
result = await next();
} catch (error) {
success = false;
throw error;
} finally {
// 📊 Registrar requisição se necessário
const shouldRecord =
(success && !skipSuccessfulRequests) ||
(!success && !skipFailedRequests);
if (shouldRecord) {
await ctx.redis.zadd(key, Date.now(), `${Date.now()}-${Math.random()}`);
}
}
return result;
});
}
// 🎯 Rate limiting específico para usuário
export const userRateLimitMiddleware = rateLimitMiddleware({
maxRequests: 100,
windowMs: 60 * 1000, // 1 minuto
keyGenerator: (ctx) => ctx.session?.user?.id || ctx.ip,
message: 'Limite de requisições por usuário excedido',
});
// 🏢 Rate limiting específico para organização
export const organizationRateLimitMiddleware = rateLimitMiddleware({
maxRequests: 1000,
windowMs: 60 * 1000, // 1 minuto
keyGenerator: (ctx) => ctx.organization?.id || ctx.session?.user?.id || ctx.ip,
message: 'Limite de requisições da organização excedido',
});
// 📊 Rate limiting dinâmico baseado no plano
export const dynamicRateLimitMiddleware = middleware(async ({ ctx, next }) => {
if (!ctx.session) {
// 🔍 Rate limit para usuários não autenticados
return rateLimitMiddleware({
maxRequests: 10,
windowMs: 60 * 1000, // 1 minuto
keyGenerator: (ctx) => ctx.ip,
message: 'Limite de requisições para usuários não autenticados excedido',
})({ ctx, next });
}
// 🎯 Rate limit baseado no plano
const plan = ctx.organization?.plan || 'FREE';
const limits = {
FREE: { maxRequests: 100, windowMs: 60 * 1000 },
PRO: { maxRequests: 1000, windowMs: 60 * 1000 },
ENTERPRISE: { maxRequests: 10000, windowMs: 60 * 1000 },
};
const config = limits[plan];
return rateLimitMiddleware({
...config,
keyGenerator: (ctx) => `${ctx.session!.user.id}:${plan}`,
message: `Limite de requisições do plano ${plan} excedido`,
})({ ctx, next });
});
// 📁 src/server/trpc/middlewares/logging.ts
import { TRPCError } from '@trpc/server';
import { middleware } from '../trpc';
import type { Context } from '../context';
// 📊 Interface para dados de log
interface LogData {
traceId: string;
userId?: string;
organizationId?: string;
path: string;
type: 'query' | 'mutation' | 'subscription';
input: any;
duration: number;
success: boolean;
error?: string;
ip: string;
userAgent: string;
timestamp: number;
}
// 📝 Middleware de logging
export const loggingMiddleware = middleware(async ({ ctx, type, path, input, next }) => {
const startTime = Date.now();
// 🎯 Criar entrada de log inicial
const logData: LogData = {
traceId: ctx.traceId,
userId: ctx.session?.user?.id,
organizationId: ctx.organization?.id,
path,
type,
input: sanitizeInput(input),
duration: 0,
success: false,
ip: ctx.ip,
userAgent: ctx.userAgent,
timestamp: startTime,
};
ctx.logger.info('tRPC request started', {
traceId: ctx.traceId,
path,
type,
userId: ctx.session?.user?.id,
});
try {
// 🔄 Executar próximo middleware
const result = await next();
// ✅ Requisição bem-sucedida
logData.duration = Date.now() - startTime;
logData.success = true;
ctx.logger.info('tRPC request completed', {
...logData,
resultSize: JSON.stringify(result).length,
});
// 📊 Salvar no banco para análise
await saveLogToDB(ctx, logData);
return result;
} catch (error) {
// ❌ Erro na requisição
logData.duration = Date.now() - startTime;
logData.success = false;
logData.error = error instanceof Error ? error.message : 'Unknown error';
ctx.logger.error('tRPC request failed', {
...logData,
error: error instanceof Error ? {
message: error.message,
stack: error.stack,
} : error,
});
// 📊 Salvar erro no banco
await saveLogToDB(ctx, logData);
throw error;
}
});
// 🔧 Funções auxiliares
function sanitizeInput(input: any): any {
if (!input) return input;
// 🔒 Remover dados sensíveis
const sensitiveFields = ['password', 'token', 'secret', 'key'];
const sanitized = JSON.parse(JSON.stringify(input));
function sanitizeObject(obj: any): any {
if (typeof obj !== 'object' || obj === null) return obj;
for (const key in obj) {
if (sensitiveFields.some(field => key.toLowerCase().includes(field))) {
obj[key] = '[REDACTED]';
} else if (typeof obj[key] === 'object') {
obj[key] = sanitizeObject(obj[key]);
}
}
return obj;
}
return sanitizeObject(sanitized);
}
async function saveLogToDB(ctx: Context, logData: LogData): Promise<void> {
try {
await ctx.prisma.requestLog.create({
data: {
traceId: logData.traceId,
userId: logData.userId,
organizationId: logData.organizationId,
path: logData.path,
type: logData.type,
input: logData.input,
duration: logData.duration,
success: logData.success,
error: logData.error,
ip: logData.ip,
userAgent: logData.userAgent,
timestamp: new Date(logData.timestamp),
},
});
} catch (error) {
ctx.logger.error('Error saving log to DB', error);
}
}
// 📁 src/server/trpc/procedures.ts
import { initTRPC } from '@trpc/server';
import { ZodError } from 'zod';
import type { Context } from './context';
import {
authMiddleware,
adminMiddleware,
organizationMiddleware,
permissionMiddleware,
featureMiddleware,
} from './middlewares/auth';
import {
userRateLimitMiddleware,
organizationRateLimitMiddleware,
dynamicRateLimitMiddleware,
} from './middlewares/rateLimit';
import { loggingMiddleware } from './middlewares/logging';
// 🔧 Inicializar tRPC
const t = initTRPC.context<Context>().create({
errorFormatter: ({ shape, error }) => {
return {
...shape,
data: {
...shape.data,
zodError: error.cause instanceof ZodError ? error.cause.flatten() : null,
},
};
},
});
// 📊 Middleware base para todas as procedures
const baseMiddleware = t.middleware(async ({ ctx, next }) => {
return loggingMiddleware({ ctx, next });
});
// 🎯 Procedures básicas
export const router = t.router;
export const middleware = t.middleware;
// 📊 Procedure pública (sem autenticação)
export const publicProcedure = t.procedure
.use(baseMiddleware);
// 🔒 Procedure protegida (requer autenticação)
export const protectedProcedure = t.procedure
.use(baseMiddleware)
.use(authMiddleware)
.use(userRateLimitMiddleware);
// 👑 Procedure de admin
export const adminProcedure = t.procedure
.use(baseMiddleware)
.use(adminMiddleware)
.use(organizationRateLimitMiddleware);
// 🏢 Procedure de organização
export const organizationProcedure = t.procedure
.use(baseMiddleware)
.use(authMiddleware)
.use(organizationMiddleware)
.use(dynamicRateLimitMiddleware);
// 🎯 Procedure com permissão específica
export function permissionProcedure(permission: string) {
return t.procedure
.use(baseMiddleware)
.use(authMiddleware)
.use(permissionMiddleware(permission))
.use(userRateLimitMiddleware);
}
// 📊 Procedure com feature flag
export function featureProcedure(feature: string) {
return t.procedure
.use(baseMiddleware)
.use(authMiddleware)
.use(organizationMiddleware)
.use(featureMiddleware(feature))
.use(dynamicRateLimitMiddleware);
}
Ordem dos Middlewares:Logging → Auth → Rate Limit → Business Logic para máxima eficiência.
Contexto Rico:Forneça informações suficientes para decisões de autorização granular.
Rate Limiting Inteligente:Ajuste limites baseado no plano e tipo de operação.
Observabilidade:Monitore métricas, logs e traces para identificar problemas rapidamente.