🚀 Oferta especial: 60% OFF no CrazyStack - Últimas vagas!Garantir vaga →
Módulo 3 - Aula 4

Middleware e Contexto

Domine middleware e contexto no tRPC: autenticação robusta, autorização granular, logs detalhados, rate limiting e contexto avançado para SaaS empresarial.

85 min
Avançado
Segurança

🎯 Por que middleware e contexto são fundamentais em SaaS?

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.

🔧 Contexto Avançado

src/server/trpc/context.ts
// 📁 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)}`;
}

🔐 Middleware de Autenticação

src/server/trpc/middlewares/auth.ts
// 📁 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();
  });
}

🚫 Rate Limiting

src/server/trpc/middlewares/rateLimit.ts
// 📁 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 });
});

📊 Logging e Monitoramento

src/server/trpc/middlewares/logging.ts
// 📁 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);
  }
}

⚙️ Configuração de Procedures

src/server/trpc/procedures.ts
// 📁 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);
}

💡 Melhores Práticas para Middleware

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.