/** * Middleware de autenticação para a External API. * * Aceita a integration_key via: * - Header: x-nw-key: nw_... * * Resolve a chave para tenantId e injeta em req.extTenantId. * Rate-limiting básico: max 120 req/min por chave (janela deslizante simples em memória). */ import type { Request, Response, NextFunction } from 'express' import type { PrismaClient } from '@prisma/client' // ─── Cache de API key (evita Prisma query em cada requisição) ──────────────── // Chaves são imutáveis durante a vida da sessão — TTL de 5 min é suficiente. const KEY_CACHE_TTL = 5 * 60_000 interface KeyCacheEntry { tenantId: string; expiresAt: Date | null; cachedAt: number } const keyCache = new Map() setInterval(() => { const now = Date.now() for (const [k, v] of keyCache.entries()) { if (now - v.cachedAt > KEY_CACHE_TTL) keyCache.delete(k) } }, KEY_CACHE_TTL) // ─── Rate limiter simples em memória (por chave) ───────────────────────────── const RATE_WINDOW_MS = 60_000 const RATE_MAX = 120 interface RateEntry { count: number; resetAt: number } const rateMap = new Map() function checkRateLimit(key: string): boolean { const now = Date.now() let entry = rateMap.get(key) if (!entry || now > entry.resetAt) { entry = { count: 1, resetAt: now + RATE_WINDOW_MS } rateMap.set(key, entry) return true } entry.count++ return entry.count <= RATE_MAX } // Limpeza periódica para não vazar memória setInterval(() => { const now = Date.now() for (const [k, v] of rateMap.entries()) { if (now > v.resetAt) rateMap.delete(k) } }, RATE_WINDOW_MS * 2) // ─── Extend Express Request ─────────────────────────────────────────────────── declare global { // eslint-disable-next-line @typescript-eslint/no-namespace namespace Express { interface Request { extTenantId?: string } } } // ─── Factory ───────────────────────────────────────────────────────────────── export function buildApiKeyAuth(prisma: PrismaClient) { return async function apiKeyAuth(req: Request, res: Response, next: NextFunction) { const key = (req.headers['x-nw-key'] as string | undefined)?.trim() if (!key) { res.status(401).json({ error: 'Header x-nw-key ausente' }) return } if (!checkRateLimit(key)) { res.status(429).json({ error: 'Rate limit excedido (120 req/min por chave)' }) return } // Verifica cache antes de ir ao banco const cached = keyCache.get(key) if (cached && Date.now() - cached.cachedAt < KEY_CACHE_TTL) { if (cached.expiresAt && cached.expiresAt < new Date()) { res.status(401).json({ error: 'Chave expirada' }) return } req.extTenantId = cached.tenantId next() return } const apiKey = await prisma.apiKey.findUnique({ where: { key }, select: { tenantId: true, isActive: true, expiresAt: true }, }) if (!apiKey || !apiKey.isActive) { res.status(401).json({ error: 'Chave inválida ou inativa' }) return } if (apiKey.expiresAt && apiKey.expiresAt < new Date()) { res.status(401).json({ error: 'Chave expirada' }) return } keyCache.set(key, { tenantId: apiKey.tenantId, expiresAt: apiKey.expiresAt, cachedAt: Date.now() }) req.extTenantId = apiKey.tenantId next() } } /** * Versão standalone que valida a chave sem passar pelo Express. * Usada no handshake do WS (upgrade request). */ export async function resolveApiKey( prisma: PrismaClient, key: string, ): Promise { if (!key) return null const cached = keyCache.get(key) if (cached && Date.now() - cached.cachedAt < KEY_CACHE_TTL) { if (cached.expiresAt && cached.expiresAt < new Date()) return null return cached.tenantId } const apiKey = await prisma.apiKey.findUnique({ where: { key }, select: { tenantId: true, isActive: true, expiresAt: true }, }) if (!apiKey || !apiKey.isActive) return null if (apiKey.expiresAt && apiKey.expiresAt < new Date()) return null keyCache.set(key, { tenantId: apiKey.tenantId, expiresAt: apiKey.expiresAt, cachedAt: Date.now() }) return apiKey.tenantId }