Files
clube67_newwhats.local/clube67/newwhats.local/plugins/ext-api/backend/apikey-auth.ts
T

134 lines
4.5 KiB
TypeScript

/**
* 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<string, KeyCacheEntry>()
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<string, RateEntry>()
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<string | null> {
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
}