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):
Regeneração de chave (self-service)¶
POST /api/account/regenerate-key:
- Auth com chave atual
UPDATE api_keys SET api_key = gen_random_uuid() WHERE key_id = $1- Chave antiga deixa de funcionar imediatamente (mesma linha, novo valor)
- Retorna nova UUID na resposta
- Email em background com a nova chave
Não há fluxo de "confirmação por link" — quem tem a chave atual já é 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).