Pular para conteúdo

Multi-Tenancy

Como o sistema atende múltiplos assinantes simultaneamente com segurança.

Modelo de dados

CREATE TABLE api_keys (
    key_id          BIGSERIAL PRIMARY KEY,
    user_name       TEXT NOT NULL,
    email           TEXT NOT NULL,
    api_key         UUID NOT NULL DEFAULT gen_random_uuid(),
    plan_tier       TEXT NOT NULL DEFAULT 'basic',
    -- restrições (NULL = sem restrição = acesso total)
    symbols_allowed JSONB,
    tfs_allowed     JSONB,
    active          BOOLEAN NOT NULL DEFAULT TRUE,
    expires_at      TIMESTAMPTZ,
    created_at      TIMESTAMPTZ NOT NULL DEFAULT NOW(),
    last_seen_at    TIMESTAMPTZ,   -- heartbeat do EA
    notes           TEXT,
    CONSTRAINT api_keys_key_unique UNIQUE (api_key)
);

Planos

Plano Símbolos TFs Rate Limit Preço
Telegram (sinais por canal) R$ 97/mês
Basic EURUSD, GBPUSD, XAUUSD H1, H4 60 req/min R$ 197/mês
Pro Todos (14 pares) M5, M15, H1, H4 300 req/min R$ 297/mês
Owner Todos Todos 600 req/min — (interno)

Defaults definidos em app/api/routers/admin.py:_PLAN_DEFAULTS.

Fluxo de provisionamento (Eduzz)

sequenceDiagram
    participant C as Cliente
    participant E as Eduzz
    participant W as /webhooks/eduzz
    participant DB as PostgreSQL
    participant M as SMTP (Gmail)

    C->>E: Compra PIX/Cartão
    E->>E: Confirma pagamento
    E->>W: POST + HMAC-SHA256
    W->>W: Valida signature
    W->>W: Identifica plano (productId)
    W->>DB: SELECT existing key by email
    alt já tem key ativa
        W-->>E: 200 already_provisioned
    else nova compra
        W->>DB: INSERT api_keys (gen_random_uuid)
        W->>M: Email HTML com chave + tutorial
        W-->>E: 200 api_key_created
    end
    M->>C: Email com UUID + instruções MT5

Cancelamento

sequenceDiagram
    participant E as Eduzz
    participant W as /webhooks/eduzz
    participant DB as PostgreSQL
    participant M as SMTP

    E->>W: cancel | refund | chargeback
    W->>W: Valida signature
    W->>DB: UPDATE api_keys SET active=FALSE<br/>WHERE email=$1
    W->>M: Email "acesso revogado"
    M->>C: Notificação ao cliente
    W-->>E: 200 access_revoked

Característica importante: revoga todas as chaves do email (caso o cliente tenha múltiplas).

Filtragem por plano

No /api/scan

@router.get("/scan")
async def scan(request: Request):
    ki = request.state.key_info
    rows = await get_latest_predictions(pool)

    # Filtra por symbols/tfs permitidos
    if ki.symbols_allowed:
        rows = [r for r in rows if r["symbol"] in ki.symbols_allowed]
    if ki.tfs_allowed:
        rows = [r for r in rows if r["timeframe"] in ki.tfs_allowed]

    return {"items": rows, "plan_tier": ki.plan_tier}

No /api/account/signals

Filtragem no SQL (não em Python) para evitar trazer dados que serão descartados:

WHERE p.created_at >= NOW() - make_interval(days => $1)
  AND s.symbol  = ANY($2::text[])    -- só se symbols_allowed != NULL
  AND t.tf_code = ANY($3::text[])    -- só se tfs_allowed != NULL

Em /api/account/executions

Filtragem por api_key_id (FK populada quando EA reporta execução):

WHERE api_key_id = $1
  AND opened_at >= NOW() - make_interval(days => $2)

Regeneração de chave (self-service)

POST /api/account/regenerate-key:

  1. Auth com chave atual
  2. UPDATE api_keys SET api_key = gen_random_uuid() WHERE key_id = $1
  3. Chave antiga deixa de funcionar imediatamente (mesma linha, novo valor)
  4. Retorna nova UUID na resposta
  5. Email em background com a nova chave

Não há fluxo de "confirmação por link" — quem tem a chave atual é o dono (auth). Adicionar confirmação por email seria fricção sem ganho de segurança.

Heartbeat & EA status

Toda chamada autenticada atualiza last_seen_at (throttle 5min):

if last_seen_at is None or (now - last_seen_at) > timedelta(minutes=5):
    await pool.execute(
        "UPDATE api_keys SET last_seen_at = NOW() WHERE api_key = $1::uuid",
        api_key,
    )

Frontend /minha-conta mostra:

  • 🟢 Online: last_seen < 5 min
  • 🟡 Recente: 5min ≤ last_seen < 2h
  • 🔴 Offline: > 2h
  • Nunca conectou: NULL

/admin/monitoring-data agrega para o dashboard /ops.

Segurança

Vetor Mitigação
Brute-force de UUID Rate limit por IP (20 inválidas/min), UUID v4 = 122 bits
Compartilhamento Anti-key-sharing alert (3+ IPs/1h)
Vazamento Auto-rotate via /account/regenerate-key
HMAC bypass EDUZZ_SECRET obrigatório (fail-closed)
SQL injection asyncpg parametrizado ($1, $2, ...)
CORS Restrito a quantfx.com.br

Auditoria

Evento Onde
Login (key auth) Log info + heartbeat last_seen_at
Key compartilhada Alerta Telegram + log warning
Key inválida Log warning + rate limit IP
Webhook recebido Log com hash do email (sem PII)
Webhook rejeitado Log warning com motivo
Key revogada Log warning + email cliente
Key regenerada Log info + email cliente

Falta: audit_log estruturado

Atualmente eventos importantes vão para arquivo de log apenas. Roadmap inclui audit_log table dedicada (ADR pendente).