diff --git a/.env.example b/.env.example
new file mode 100644
index 0000000..239fb10
--- /dev/null
+++ b/.env.example
@@ -0,0 +1,75 @@
+# ═══════════════════════════════════════════════════════════
+# 🐍 OPHION - Configuração de Exemplo
+# Copie para .env e preencha os valores
+# ═══════════════════════════════════════════════════════════
+
+# ─────────────────────────────────────────────────────────────
+# Organização
+# ─────────────────────────────────────────────────────────────
+ORG_NAME="Minha Empresa"
+ADMIN_EMAIL=admin@empresa.com
+ADMIN_PASSWORD=mude-esta-senha
+
+# ─────────────────────────────────────────────────────────────
+# Rede
+# ─────────────────────────────────────────────────────────────
+DOMAIN=localhost
+SERVER_PORT=8080
+DASHBOARD_PORT=3000
+API_URL=http://ophion-server:8080
+
+# ─────────────────────────────────────────────────────────────
+# Segurança (GERE VALORES ÚNICOS!)
+# Use: openssl rand -hex 32
+# ─────────────────────────────────────────────────────────────
+JWT_SECRET=MUDE-ISTO-openssl-rand-hex-32
+API_KEY=ophion_MUDE-ISTO-openssl-rand-hex-32
+
+# ─────────────────────────────────────────────────────────────
+# PostgreSQL
+# ─────────────────────────────────────────────────────────────
+POSTGRES_USER=ophion
+POSTGRES_PASSWORD=MUDE-ISTO-senha-segura
+POSTGRES_DB=ophion
+DATABASE_URL=postgres://ophion:MUDE-ISTO-senha-segura@postgres:5432/ophion
+
+# ─────────────────────────────────────────────────────────────
+# ClickHouse
+# ─────────────────────────────────────────────────────────────
+CLICKHOUSE_URL=clickhouse://clickhouse:9000/ophion
+
+# ─────────────────────────────────────────────────────────────
+# Redis
+# ─────────────────────────────────────────────────────────────
+REDIS_URL=redis://redis:6379
+
+# ─────────────────────────────────────────────────────────────
+# Alertas - Telegram (opcional)
+# ─────────────────────────────────────────────────────────────
+TELEGRAM_ENABLED=false
+TELEGRAM_BOT_TOKEN=
+TELEGRAM_CHAT_ID=
+
+# ─────────────────────────────────────────────────────────────
+# Alertas - Slack (opcional)
+# ─────────────────────────────────────────────────────────────
+SLACK_ENABLED=false
+SLACK_WEBHOOK_URL=
+
+# ─────────────────────────────────────────────────────────────
+# IA - OpenAI (opcional, para recursos de IA)
+# ─────────────────────────────────────────────────────────────
+OPENAI_API_KEY=
+
+# ─────────────────────────────────────────────────────────────
+# Configurações Gerais
+# ─────────────────────────────────────────────────────────────
+TZ=America/Sao_Paulo
+LOG_LEVEL=info
+
+# ─────────────────────────────────────────────────────────────
+# Retenção de Dados
+# ─────────────────────────────────────────────────────────────
+METRICS_RETENTION_DAYS=90
+LOGS_RETENTION_DAYS=30
+TRACES_RETENTION_DAYS=14
diff --git a/deploy/docker/Dockerfile b/deploy/docker/Dockerfile
new file mode 100644
index 0000000..e19049d
--- /dev/null
+++ b/deploy/docker/Dockerfile
@@ -0,0 +1,119 @@
+# ═══════════════════════════════════════════════════════════
+# 🐍 OPHION - Dockerfile All-in-One
+# Imagem única com Server + Agent + Dashboard
+# ═══════════════════════════════════════════════════════════
+
+# ─────────────────────────────────────────────────────────────
+# Stage 1: Build Go binaries
+# ─────────────────────────────────────────────────────────────
+FROM golang:1.22-alpine AS go-builder
+
+WORKDIR /build
+
+# Dependências
+RUN apk add --no-cache git ca-certificates
+
+# Go modules
+COPY go.mod go.sum ./
+RUN go mod download
+
+# Código fonte
+COPY cmd/ ./cmd/
+COPY internal/ ./internal/
+
+# Build server
+RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 \
+ go build -ldflags="-s -w" -o ophion-server ./cmd/server
+
+# Build agent
+RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 \
+ go build -ldflags="-s -w" -o ophion-agent ./cmd/agent
+
+# ─────────────────────────────────────────────────────────────
+# Stage 2: Build Dashboard (Next.js)
+# ─────────────────────────────────────────────────────────────
+FROM node:20-alpine AS web-builder
+
+WORKDIR /build
+
+# Dependências
+COPY dashboard/package*.json ./
+RUN npm ci --only=production
+
+# Código fonte
+COPY dashboard/ ./
+
+# Build
+ENV NEXT_TELEMETRY_DISABLED=1
+RUN npm run build
+
+# ─────────────────────────────────────────────────────────────
+# Stage 3: Runtime Image
+# ─────────────────────────────────────────────────────────────
+FROM alpine:3.19
+
+LABEL org.opencontainers.image.title="OPHION"
+LABEL org.opencontainers.image.description="Open Source Observability Platform"
+LABEL org.opencontainers.image.source="https://github.com/bigtux/ophion"
+LABEL org.opencontainers.image.vendor="OPHION"
+LABEL org.opencontainers.image.licenses="AGPL-3.0"
+
+# Dependências runtime
+RUN apk add --no-cache \
+ ca-certificates \
+ tzdata \
+ nodejs \
+ npm \
+ supervisor \
+ curl \
+ bash
+
+# Criar usuário não-root
+RUN addgroup -g 1000 ophion && \
+ adduser -u 1000 -G ophion -s /bin/sh -D ophion
+
+WORKDIR /app
+
+# Copiar binários Go
+COPY --from=go-builder /build/ophion-server /app/bin/
+COPY --from=go-builder /build/ophion-agent /app/bin/
+
+# Copiar Dashboard
+COPY --from=web-builder /build/.next /app/web/.next
+COPY --from=web-builder /build/public /app/web/public
+COPY --from=web-builder /build/package*.json /app/web/
+COPY --from=web-builder /build/node_modules /app/web/node_modules
+
+# Configs
+COPY configs/ /app/configs/
+COPY web/ /app/static/
+
+# Supervisor config
+RUN mkdir -p /etc/supervisor.d
+COPY deploy/docker/supervisord.conf /etc/supervisor.d/ophion.ini
+
+# Diretórios
+RUN mkdir -p /app/data /app/logs && \
+ chown -R ophion:ophion /app
+
+# Script de entrada
+COPY deploy/docker/entrypoint.sh /app/
+RUN chmod +x /app/entrypoint.sh
+
+# Portas
+EXPOSE 8080 3000
+
+# Health check
+HEALTHCHECK --interval=30s --timeout=10s --start-period=10s --retries=3 \
+ CMD curl -f http://localhost:8080/health || exit 1
+
+# Variáveis de ambiente padrão
+ENV TZ=America/Sao_Paulo \
+ LOG_LEVEL=info \
+ SERVER_PORT=8080 \
+ DASHBOARD_PORT=3000
+
+USER ophion
+
+ENTRYPOINT ["/app/entrypoint.sh"]
+CMD ["all"]
diff --git a/deploy/docker/entrypoint.sh b/deploy/docker/entrypoint.sh
new file mode 100644
index 0000000..8451668
--- /dev/null
+++ b/deploy/docker/entrypoint.sh
@@ -0,0 +1,35 @@
+#!/bin/bash
+# ═══════════════════════════════════════════════════════════
+# 🐍 OPHION - Entrypoint
+# ═══════════════════════════════════════════════════════════
+
+set -e
+
+MODE=${1:-all}
+
+echo "🐍 OPHION starting in mode: $MODE"
+
+case "$MODE" in
+ server)
+ echo "Starting API server on port ${SERVER_PORT:-8080}..."
+ exec /app/bin/ophion-server
+ ;;
+ agent)
+ echo "Starting agent..."
+ exec /app/bin/ophion-agent -config /app/configs/agent.yaml
+ ;;
+ web)
+ echo "Starting dashboard on port ${DASHBOARD_PORT:-3000}..."
+ cd /app/web
+ exec npm start
+ ;;
+ all)
+ echo "Starting all services with supervisor..."
+ exec supervisord -c /etc/supervisord.conf
+ ;;
+ *)
+ echo "Unknown mode: $MODE"
+ echo "Usage: entrypoint.sh [server|agent|web|all]"
+ exit 1
+ ;;
+esac
diff --git a/deploy/docker/supervisord.conf b/deploy/docker/supervisord.conf
new file mode 100644
index 0000000..2012961
--- /dev/null
+++ b/deploy/docker/supervisord.conf
@@ -0,0 +1,26 @@
+[supervisord]
+nodaemon=true
+logfile=/app/logs/supervisord.log
+pidfile=/tmp/supervisord.pid
+user=ophion
+
+[program:ophion-server]
+command=/app/bin/ophion-server
+directory=/app
+autostart=true
+autorestart=true
+stdout_logfile=/app/logs/server.log
+stderr_logfile=/app/logs/server.error.log
+environment=PORT="%(ENV_SERVER_PORT)s"
+
+[program:ophion-web]
+command=npm start
+directory=/app/web
+autostart=true
+autorestart=true
+stdout_logfile=/app/logs/web.log
+stderr_logfile=/app/logs/web.error.log
+environment=PORT="%(ENV_DASHBOARD_PORT)s"
+
+[group:ophion]
+programs=ophion-server,ophion-web
diff --git a/docs/INSTALL.md b/docs/INSTALL.md
index e1e0aee..492115c 100644
--- a/docs/INSTALL.md
+++ b/docs/INSTALL.md
@@ -1,436 +1,231 @@
-# 🐍 OPHION - Manual de Instalação e Configuração
+# 🐍 OPHION - Guia de Instalação
-## Índice
-1. [Instalação do Servidor](#instalação-do-servidor)
-2. [Instalação do Agent](#instalação-do-agent)
-3. [Monitoramento de Docker](#monitoramento-de-docker)
-4. [Monitoramento de Aplicações](#monitoramento-de-aplicações)
-5. [Configuração de Alertas](#configuração-de-alertas)
+## Instalação Rápida (1 comando)
+
+```bash
+curl -fsSL https://get.ophion.io | bash
+```
+
+O instalador vai:
+1. ✅ Verificar requisitos (Docker, Docker Compose)
+2. 📋 Coletar informações da sua empresa
+3. 🔐 Gerar credenciais seguras
+4. 📦 Configurar todos os serviços
+5. 🚀 Iniciar a plataforma
---
-## 1. Instalação do Servidor
+## Instalação Manual (Passo a Passo)
-### Requisitos Mínimos
-- CPU: 2 cores
-- RAM: 4GB (8GB recomendado)
-- Disco: 50GB SSD
-- OS: Ubuntu 22.04+ / Debian 12+
-- Docker 24+
-
-### Instalação Rápida (1 comando)
+### 1. Requisitos
```bash
-curl -fsSL https://ophion.com.br/install.sh | bash
+# Ubuntu/Debian
+sudo apt update
+sudo apt install -y curl git
+
+# Instalar Docker
+curl -fsSL https://get.docker.com | sh
+sudo usermod -aG docker $USER
+
+# Logout e login para aplicar grupo docker
```
-### Instalação Manual
+### 2. Baixar Ophion
```bash
-# Clonar repositório
git clone https://github.com/bigtux/ophion.git
-cd ophion/deploy/docker
-
-# Configurar variáveis de ambiente
-cp .env.example .env
-nano .env # Editar conforme necessário
-
-# Iniciar serviços
-docker compose up -d
-
-# Verificar status
-docker compose ps
+cd ophion
```
-### Variáveis de Ambiente (.env)
+### 3. Configurar
+
+Copie e edite o arquivo de configuração:
+
+```bash
+cp .env.example .env
+nano .env
+```
+
+Configurações importantes:
```env
-# Segurança (OBRIGATÓRIO - gere valores únicos!)
-JWT_SECRET=sua-chave-secreta-aqui-min-32-chars
+# Sua empresa
+ORG_NAME="Minha Empresa"
+ADMIN_EMAIL=admin@empresa.com
+ADMIN_PASSWORD=senha-segura-aqui
-# Banco de Dados
-POSTGRES_USER=ophion
-POSTGRES_PASSWORD=senha-forte-aqui
-POSTGRES_DB=ophion
+# Domínio (ou localhost para testes)
+DOMAIN=ophion.empresa.com
-# ClickHouse (métricas/logs)
-CLICKHOUSE_USER=default
-CLICKHOUSE_PASSWORD=senha-clickhouse
+# Portas
+SERVER_PORT=8080
+DASHBOARD_PORT=3000
-# Redis
-REDIS_PASSWORD=senha-redis
-
-# Configurações do Servidor
-OPHION_PORT=8080
-OPHION_HOST=0.0.0.0
-
-# Retenção de dados (dias)
-METRICS_RETENTION_DAYS=30
-LOGS_RETENTION_DAYS=14
+# Segurança (gere valores únicos!)
+JWT_SECRET=seu-jwt-secret-aqui
```
-### Verificar Instalação
+### 4. Iniciar
```bash
-# Health check
-curl http://localhost:8080/health
-
-# Resposta esperada:
-# {"status":"healthy","service":"ophion","version":"0.1.0"}
+docker compose up -d
```
-### Acessar Dashboard
+### 5. Acessar
-Abra no navegador: `http://seu-servidor:3000`
-
-1. Crie sua conta de administrador
-2. Configure sua organização
-3. Gere API Keys para os agents
+- **Dashboard:** http://localhost:3000
+- **API:** http://localhost:8080
---
-## 2. Instalação do Agent
+## Instalação do Agent (Servidores Monitorados)
-O Agent coleta métricas do servidor e envia para o OPHION.
-
-### Instalação Rápida
+Em cada servidor que você quer monitorar:
```bash
-# Substitua YOUR_API_KEY pela chave gerada no dashboard
-curl -fsSL https://ophion.com.br/agent.sh | OPHION_API_KEY=YOUR_API_KEY bash
+curl -fsSL http://SEU-SERVIDOR-OPHION:8080/install-agent.sh | sudo bash
```
-### Instalação Manual
+Ou manualmente:
```bash
-# Baixar binário
-wget https://github.com/bigtux/ophion/releases/latest/download/ophion-agent-linux-amd64
-chmod +x ophion-agent-linux-amd64
-sudo mv ophion-agent-linux-amd64 /usr/local/bin/ophion-agent
+# Baixar
+curl -o /usr/local/bin/ophion-agent \
+ http://SEU-SERVIDOR-OPHION:8080/downloads/agent/linux/amd64/ophion-agent
+chmod +x /usr/local/bin/ophion-agent
-# Criar arquivo de configuração
-sudo mkdir -p /etc/ophion
-sudo tee /etc/ophion/agent.yaml << EOF
-server: https://api.ophion.com.br
-api_key: YOUR_API_KEY
-hostname: $(hostname)
-interval: 30s
+# Configurar
+mkdir -p /etc/ophion
+cat > /etc/ophion/agent.yaml << EOF
+server:
+ url: http://SEU-SERVIDOR-OPHION:8080
+ api_key: SUA-API-KEY
-collectors:
- cpu: true
- memory: true
- disk: true
- network: true
- processes: true
+collection:
+ interval: 30s
+
+metrics:
+ enabled: true
+
+logs:
+ enabled: true
+ paths:
+ - /var/log/syslog
EOF
-# Criar serviço systemd
-sudo tee /etc/systemd/system/ophion-agent.service << EOF
+# Criar serviço
+cat > /etc/systemd/system/ophion-agent.service << EOF
[Unit]
-Description=OPHION Monitoring Agent
+Description=OPHION Agent
After=network.target
[Service]
-Type=simple
-ExecStart=/usr/local/bin/ophion-agent --config /etc/ophion/agent.yaml
+ExecStart=/usr/local/bin/ophion-agent -config /etc/ophion/agent.yaml
Restart=always
-RestartSec=10
[Install]
WantedBy=multi-user.target
EOF
-# Iniciar serviço
-sudo systemctl daemon-reload
-sudo systemctl enable ophion-agent
-sudo systemctl start ophion-agent
-
-# Verificar status
-sudo systemctl status ophion-agent
-```
-
-### Variáveis de Ambiente do Agent
-
-```bash
-export OPHION_SERVER="https://api.ophion.com.br"
-export OPHION_API_KEY="ophion_xxxxxxxxxxxxxxxx"
-export OPHION_HOSTNAME="meu-servidor"
-export OPHION_INTERVAL="30s"
+# Iniciar
+systemctl daemon-reload
+systemctl enable --now ophion-agent
```
---
-## 3. Monitoramento de Docker
-
-### Opção A: Agent com Acesso ao Docker Socket
+## Comandos Úteis
```bash
-# Adicionar ao agent.yaml
-collectors:
- docker:
- enabled: true
- socket: /var/run/docker.sock
- collect_container_stats: true
- collect_container_logs: true
-```
+# Status
+ophion status
-### Opção B: Container Dedicado
+# Logs
+ophion logs # Logs do server
+ophion logs ophion-web # Logs do dashboard
-```yaml
-# docker-compose.yml do seu projeto
-services:
- ophion-agent:
- image: ophion/agent:latest
- container_name: ophion-agent
- restart: unless-stopped
- environment:
- - OPHION_SERVER=https://api.ophion.com.br
- - OPHION_API_KEY=YOUR_API_KEY
- volumes:
- - /var/run/docker.sock:/var/run/docker.sock:ro
- - /proc:/host/proc:ro
- - /sys:/host/sys:ro
- network_mode: host
- pid: host
-```
+# Gerenciamento
+ophion start
+ophion stop
+ophion restart
+ophion update
-### Métricas Coletadas do Docker
+# Backup
+ophion backup
-| Métrica | Descrição |
-|---------|-----------|
-| `container.cpu.usage` | Uso de CPU por container |
-| `container.memory.usage` | Uso de memória |
-| `container.memory.limit` | Limite de memória |
-| `container.network.rx_bytes` | Bytes recebidos |
-| `container.network.tx_bytes` | Bytes enviados |
-| `container.disk.read_bytes` | Leitura de disco |
-| `container.disk.write_bytes` | Escrita de disco |
-| `container.status` | Status (running/stopped) |
-| `container.restarts` | Contagem de restarts |
-
-### Labels para Identificação
-
-Adicione labels aos seus containers para melhor organização:
-
-```yaml
-services:
- minha-app:
- labels:
- ophion.monitor: "true"
- ophion.service: "api"
- ophion.environment: "production"
- ophion.team: "backend"
+# Gerar nova API Key
+ophion api-key
```
---
-## 4. Monitoramento de Aplicações (APM)
+## Estrutura de Diretórios
-### Node.js
-
-```bash
-npm install @ophion/apm
```
-
-```javascript
-// No início do seu app (antes de outros imports)
-const ophion = require('@ophion/apm');
-
-ophion.init({
- serverUrl: 'https://api.ophion.com.br',
- apiKey: 'YOUR_API_KEY',
- serviceName: 'minha-api',
- environment: 'production'
-});
-
-// Seu código normal...
-const express = require('express');
-const app = express();
-```
-
-### Python
-
-```bash
-pip install ophion-apm
-```
-
-```python
-# No início do seu app
-import ophion_apm
-
-ophion_apm.init(
- server_url='https://api.ophion.com.br',
- api_key='YOUR_API_KEY',
- service_name='minha-api',
- environment='production'
-)
-
-# Seu código normal...
-from flask import Flask
-app = Flask(__name__)
-```
-
-### Go
-
-```go
-import "github.com/bigtux/ophion/sdk/go/apm"
-
-func main() {
- // Inicializar APM
- apm.Init(apm.Config{
- ServerURL: "https://api.ophion.com.br",
- APIKey: "YOUR_API_KEY",
- ServiceName: "minha-api",
- Environment: "production",
- })
- defer apm.Close()
-
- // Seu código normal...
-}
-```
-
-### Java (Spring Boot)
-
-```xml
-
-
- com.ophion
- ophion-apm
- 1.0.0
-
-```
-
-```yaml
-# application.yml
-ophion:
- apm:
- server-url: https://api.ophion.com.br
- api-key: YOUR_API_KEY
- service-name: minha-api
- environment: production
-```
-
-### OpenTelemetry (Universal)
-
-OPHION é compatível com OpenTelemetry. Use qualquer SDK OTel:
-
-```bash
-# Variáveis de ambiente
-export OTEL_EXPORTER_OTLP_ENDPOINT="https://api.ophion.com.br/v1/traces"
-export OTEL_EXPORTER_OTLP_HEADERS="Authorization=Bearer YOUR_API_KEY"
-export OTEL_SERVICE_NAME="minha-api"
+/opt/ophion/
+├── docker-compose.yml # Configuração dos containers
+├── .env # Variáveis de ambiente (SECRETO!)
+├── data/
+│ ├── postgres/ # Dados do PostgreSQL
+│ ├── clickhouse/ # Métricas e logs
+│ └── redis/ # Cache
+├── configs/ # Configurações customizadas
+├── logs/ # Logs da aplicação
+├── scripts/
+│ └── install-agent.sh # Instalador do agent
+└── backups/ # Backups automáticos
```
---
-## 5. Configuração de Alertas
+## Portas
-### Via Dashboard
-
-1. Acesse **Alertas** → **Novo Alerta**
-2. Defina a condição:
- - Métrica: `cpu.usage`
- - Operador: `>`
- - Valor: `80`
- - Duração: `5 minutos`
-3. Configure notificações:
- - Telegram
- - Slack
- - Email
- - Webhook
-
-### Via API
-
-```bash
-curl -X POST "https://api.ophion.com.br/api/v1/alerts" \
- -H "Authorization: Bearer YOUR_API_KEY" \
- -H "Content-Type: application/json" \
- -d '{
- "name": "CPU Alta",
- "description": "Alerta quando CPU > 80%",
- "condition": {
- "metric": "cpu.usage",
- "operator": ">",
- "threshold": 80,
- "duration": "5m"
- },
- "notifications": [
- {
- "type": "telegram",
- "chat_id": "123456789"
- },
- {
- "type": "email",
- "to": "admin@empresa.com"
- }
- ],
- "severity": "warning"
- }'
-```
-
-### Integrações Disponíveis
-
-| Canal | Configuração |
-|-------|-------------|
-| **Telegram** | Bot token + Chat ID |
-| **Slack** | Webhook URL |
-| **Discord** | Webhook URL |
-| **Email** | SMTP ou API (SendGrid, Resend) |
-| **PagerDuty** | Integration Key |
-| **Webhook** | URL customizada |
-
-### Configurar Telegram
-
-1. Crie um bot com [@BotFather](https://t.me/BotFather)
-2. Obtenha o token do bot
-3. Inicie conversa com o bot
-4. No dashboard OPHION: **Configurações** → **Integrações** → **Telegram**
-5. Cole o token e configure
+| Serviço | Porta | Descrição |
+|---------|-------|-----------|
+| Dashboard | 3000 | Interface web |
+| API | 8080 | REST API |
+| PostgreSQL | 5432 | Banco de dados (interno) |
+| ClickHouse | 9000 | Métricas/Logs (interno) |
+| Redis | 6379 | Cache (interno) |
---
## Troubleshooting
+### Containers não iniciam
+
+```bash
+# Ver logs
+docker compose logs
+
+# Verificar recursos
+docker system df
+df -h
+```
+
### Agent não conecta
```bash
-# Verificar conectividade
-curl -v https://api.ophion.com.br/health
+# Testar conectividade
+curl http://SEU-SERVIDOR:8080/health
-# Verificar logs do agent
+# Ver logs do agent
journalctl -u ophion-agent -f
-
-# Testar API key
-curl -H "Authorization: Bearer YOUR_API_KEY" \
- https://api.ophion.com.br/api/v1/status
```
-### Métricas não aparecem
-
-1. Verifique se o agent está rodando: `systemctl status ophion-agent`
-2. Verifique a API key no dashboard
-3. Confira o hostname no dashboard
-4. Aguarde até 60 segundos para primeira coleta
-
-### Docker metrics não coletam
+### Resetar senha admin
```bash
-# Verificar permissões do socket
-ls -la /var/run/docker.sock
-
-# Agent precisa estar no grupo docker
-sudo usermod -aG docker ophion-agent
+docker compose exec postgres psql -U ophion -c \
+ "UPDATE users SET password_hash = crypt('nova-senha', gen_salt('bf')) WHERE email = 'admin@email.com';"
```
---
## Suporte
-- 📧 Email: suporte@ophion.com.br
-- 💬 Telegram: [@ophion_suporte](https://t.me/ophion_suporte)
-- 📖 Docs: https://docs.ophion.com.br
-- 🐙 GitHub: https://github.com/bigtux/ophion
-
----
-
-*Made with 🖤 in Brazil*
+- 📖 Docs: https://docs.ophion.io
+- 💬 Discord: https://discord.gg/ophion
+- 🐛 Issues: https://github.com/bigtux/ophion/issues
+- 📧 Email: support@ophion.io
diff --git a/docs/SECURITY.md b/docs/SECURITY.md
new file mode 100644
index 0000000..b3d5417
--- /dev/null
+++ b/docs/SECURITY.md
@@ -0,0 +1,270 @@
+# 🔐 OPHION Security Guide
+
+## Visão Geral
+
+OPHION foi projetado com segurança em camadas (defense in depth):
+
+1. **Autenticação** - JWT + API Keys
+2. **Autorização** - RBAC + Scopes
+3. **Rate Limiting** - Proteção contra DDoS/Brute Force
+4. **Criptografia** - TLS + Bcrypt + SHA256
+5. **Auditoria** - Logs de todas as ações
+6. **Isolamento** - Multi-tenant por organização
+
+---
+
+## 🔑 Autenticação
+
+### JWT Tokens
+
+- **Access Token**: Curta duração (15-60 min)
+- **Refresh Token**: Longa duração (7-30 dias)
+- **Algoritmo**: HS256 (HMAC-SHA256)
+- **Revogação**: Via Redis blacklist
+
+```bash
+# Header de autenticação
+Authorization: Bearer eyJhbGciOiJIUzI1NiIs...
+```
+
+### API Keys
+
+- **Formato**: `ophion_` + 64 caracteres hex
+- **Storage**: Hash SHA256 (nunca plaintext)
+- **Scopes**: Permissões granulares
+- **Rotação**: Recomendado a cada 90 dias
+
+```bash
+# Exemplo de API Key
+ophion_a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6...
+
+# Header de autenticação
+Authorization: Bearer ophion_xxx...
+```
+
+### Scopes de API Key
+
+| Scope | Descrição |
+|-------|-----------|
+| `metrics:read` | Ler métricas |
+| `metrics:write` | Enviar métricas |
+| `logs:read` | Ler logs |
+| `logs:write` | Enviar logs |
+| `traces:read` | Ler traces |
+| `traces:write` | Enviar traces |
+| `alerts:read` | Ler alertas |
+| `alerts:write` | Criar/editar alertas |
+| `admin` | Acesso administrativo |
+| `*` | Acesso total |
+
+---
+
+## 🛡️ Proteções
+
+### Rate Limiting
+
+| Endpoint | Limite | Janela |
+|----------|--------|--------|
+| `/api/v1/auth/*` | 5 req | 1 min |
+| `/api/v1/*` (API) | 100 req | 1 min |
+| `/api/v1/ingest/*` | 1000 req | 1 min |
+| `/api/v1/export/*` | 10 req | 1 hora |
+
+### Brute Force Protection
+
+- **Máximo de tentativas**: 5 falhas
+- **Bloqueio**: 15 minutos
+- **Tracking**: Por IP + Email
+
+### Security Headers
+
+```http
+X-Frame-Options: DENY
+X-Content-Type-Options: nosniff
+X-XSS-Protection: 1; mode=block
+Strict-Transport-Security: max-age=31536000; includeSubDomains
+Content-Security-Policy: default-src 'self'
+Referrer-Policy: strict-origin-when-cross-origin
+Permissions-Policy: geolocation=(), microphone=(), camera=()
+```
+
+---
+
+## 🔒 Senhas
+
+### Política de Senha
+
+- Mínimo 12 caracteres
+- Letra maiúscula obrigatória
+- Letra minúscula obrigatória
+- Número obrigatório
+- Caractere especial obrigatório
+- Bloqueio de senhas comuns
+
+### Armazenamento
+
+- **Algoritmo**: Bcrypt
+- **Cost Factor**: 12
+- **Salt**: Único por senha (auto-gerado)
+
+---
+
+## 🔐 Criptografia
+
+### Em Trânsito
+
+- TLS 1.2+ obrigatório em produção
+- Certificados Let's Encrypt (automático)
+- HSTS habilitado
+
+### Em Repouso
+
+- Senhas: Bcrypt hash
+- API Keys: SHA256 hash
+- Tokens: Opcionalmente criptografados
+- Dados sensíveis: AES-256-GCM (configurável)
+
+---
+
+## 📝 Auditoria
+
+### Eventos Registrados
+
+- `auth.login` - Login bem-sucedido
+- `auth.login_failed` - Falha de login
+- `auth.logout` - Logout
+- `apikey.created` - API key criada
+- `apikey.revoked` - API key revogada
+- `user.created` - Usuário criado
+- `user.deleted` - Usuário deletado
+- `config.changed` - Configuração alterada
+- `alert.created` - Alerta criado
+- `data.export` - Exportação de dados
+
+### Campos de Auditoria
+
+```json
+{
+ "timestamp": "2024-01-15T10:30:00Z",
+ "event_type": "auth.login",
+ "user_id": "uuid",
+ "org_id": "uuid",
+ "ip": "192.168.1.1",
+ "user_agent": "Mozilla/5.0...",
+ "resource": "/api/v1/auth/login",
+ "action": "POST",
+ "status": "success",
+ "details": {}
+}
+```
+
+---
+
+## 🏢 Multi-Tenancy
+
+### Isolamento de Dados
+
+- Cada organização tem `org_id` único
+- Queries sempre filtradas por `org_id`
+- API Keys vinculadas à organização
+- Usuários pertencem a uma organização
+
+### RBAC (Role-Based Access Control)
+
+| Role | Permissões |
+|------|------------|
+| `viewer` | Somente leitura |
+| `operator` | Leitura + Ack de alertas |
+| `editor` | Leitura + Escrita |
+| `admin` | Acesso total à organização |
+| `super_admin` | Acesso total ao sistema |
+
+---
+
+## ⚙️ Configurações de Segurança
+
+### Variáveis de Ambiente
+
+```bash
+# Obrigatórias
+JWT_SECRET= # openssl rand -hex 32
+ADMIN_PASSWORD=
+
+# Recomendadas
+HTTPS_ONLY=true # Forçar HTTPS
+SESSION_TIMEOUT=3600 # 1 hora
+REFRESH_TOKEN_DAYS=7 # 7 dias
+PASSWORD_MIN_LENGTH=12 # Mínimo 12 chars
+RATE_LIMIT_ENABLED=true
+AUDIT_LOG_ENABLED=true
+
+# IP Whitelist (opcional)
+ADMIN_IP_WHITELIST=10.0.0.0/8,192.168.0.0/16
+API_IP_WHITELIST= # Vazio = todos permitidos
+```
+
+### Checklist de Produção
+
+- [ ] JWT_SECRET único e forte (64+ chars)
+- [ ] HTTPS habilitado
+- [ ] Senhas fortes para todos os usuários
+- [ ] Rate limiting habilitado
+- [ ] Logs de auditoria habilitados
+- [ ] Backup configurado
+- [ ] Firewall configurado
+- [ ] Atualizações automáticas
+- [ ] Monitoramento de segurança
+
+---
+
+## 🚨 Resposta a Incidentes
+
+### Se suspeitar de comprometimento:
+
+1. **Revogar todas as API Keys**
+ ```bash
+ ophion security revoke-all-keys
+ ```
+
+2. **Invalidar todas as sessões**
+ ```bash
+ ophion security invalidate-sessions
+ ```
+
+3. **Rotacionar JWT Secret**
+ ```bash
+ # Atualizar .env com novo JWT_SECRET
+ ophion restart
+ ```
+
+4. **Revisar logs de auditoria**
+ ```bash
+ ophion logs --type audit --since 24h
+ ```
+
+5. **Resetar senhas comprometidas**
+
+---
+
+## 📞 Reportar Vulnerabilidades
+
+Se você encontrar uma vulnerabilidade de segurança:
+
+1. **NÃO** abra uma issue pública
+2. Envie email para: security@ophion.io
+3. Inclua detalhes e passos para reproduzir
+4. Aguarde confirmação antes de divulgar
+
+**Programa de Bug Bounty**: Em breve!
+
+---
+
+## 🔄 Atualizações de Segurança
+
+Assine nossa lista de segurança:
+- Email: security-announce@ophion.io
+- GitHub Security Advisories
+
+Versões com suporte de segurança:
+- Última versão: Suporte completo
+- Versão anterior: Patches críticos por 6 meses
diff --git a/go.mod b/go.mod
index e80d2e4..615968c 100644
--- a/go.mod
+++ b/go.mod
@@ -4,6 +4,8 @@ go 1.22
require (
github.com/gofiber/fiber/v2 v2.52.0
- github.com/shirou/gopsutil/v3 v3.24.1
github.com/golang-jwt/jwt/v5 v5.2.0
+ github.com/redis/go-redis/v9 v9.4.0
+ github.com/shirou/gopsutil/v3 v3.24.1
+ golang.org/x/crypto v0.18.0
)
diff --git a/install.sh b/install.sh
index 8c4ae9c..4c3b336 100755
--- a/install.sh
+++ b/install.sh
@@ -1,49 +1,844 @@
#!/bin/bash
+#
+# 🐍 OPHION - Instalador Interativo
+# Plataforma de Observabilidade Open Source
+#
+# Uso: curl -fsSL https://get.ophion.io | bash
+#
+
set -e
-echo "🐍 OPHION - Observability Platform Installer"
-echo "============================================="
+# Cores
+RED='\033[0;31m'
+GREEN='\033[0;32m'
+YELLOW='\033[1;33m'
+BLUE='\033[0;34m'
+PURPLE='\033[0;35m'
+CYAN='\033[0;36m'
+NC='\033[0m' # No Color
-# Check Docker
-if ! command -v docker &> /dev/null; then
- echo "❌ Docker not found. Installing..."
- curl -fsSL https://get.docker.com | sh
-fi
+# ASCII Art
+show_banner() {
+ echo -e "${PURPLE}"
+ cat << "EOF"
+ ____ _____ _ _ _____ ____ _ _
+ / __ \| __ \| | | |_ _/ __ \| \ | |
+ | | | | |__) | |__| | | || | | | \| |
+ | | | | ___/| __ | | || | | | . ` |
+ | |__| | | | | | |_| || |__| | |\ |
+ \____/|_| |_| |_|_____\____/|_| \_|
+
+ Open Source Observability Platform
+ Made with 🖤 in Brazil
+EOF
+ echo -e "${NC}"
+}
-# Check Docker Compose
-if ! command -v docker-compose &> /dev/null && ! docker compose version &> /dev/null; then
- echo "❌ Docker Compose not found. Please install it."
+# Logging
+log_info() { echo -e "${BLUE}[INFO]${NC} $1"; }
+log_success() { echo -e "${GREEN}[✓]${NC} $1"; }
+log_warn() { echo -e "${YELLOW}[!]${NC} $1"; }
+log_error() { echo -e "${RED}[✗]${NC} $1"; }
+
+# Verificar requisitos
+check_requirements() {
+ log_info "Verificando requisitos..."
+
+ # Docker
+ if ! command -v docker &> /dev/null; then
+ log_error "Docker não encontrado!"
+ echo ""
+ echo "Instale o Docker primeiro:"
+ echo " curl -fsSL https://get.docker.com | sh"
+ exit 1
+ fi
+ log_success "Docker instalado"
+
+ # Docker Compose
+ if ! docker compose version &> /dev/null && ! command -v docker-compose &> /dev/null; then
+ log_error "Docker Compose não encontrado!"
+ exit 1
+ fi
+ log_success "Docker Compose instalado"
+
+ # Verificar se Docker está rodando
+ if ! docker info &> /dev/null; then
+ log_error "Docker não está rodando!"
+ echo " sudo systemctl start docker"
+ exit 1
+ fi
+ log_success "Docker está rodando"
+}
+
+# Gerar string aleatória
+generate_secret() {
+ openssl rand -hex 32 2>/dev/null || head -c 64 /dev/urandom | base64 | tr -dc 'a-zA-Z0-9' | head -c 64
+}
+
+# Gerar API Key
+generate_api_key() {
+ echo "ophion_$(openssl rand -hex 32 2>/dev/null || head -c 64 /dev/urandom | base64 | tr -dc 'a-zA-Z0-9' | head -c 64)"
+}
+
+# Coletar informações do cliente
+collect_info() {
+ echo ""
+ echo -e "${CYAN}═══════════════════════════════════════════════════════════${NC}"
+ echo -e "${CYAN} CONFIGURAÇÃO INICIAL ${NC}"
+ echo -e "${CYAN}═══════════════════════════════════════════════════════════${NC}"
+ echo ""
+
+ # Nome da organização
+ read -p "📋 Nome da sua empresa/organização: " ORG_NAME
+ ORG_NAME=${ORG_NAME:-"Minha Empresa"}
+
+ # Email do admin
+ read -p "📧 Email do administrador: " ADMIN_EMAIL
+ while [[ ! "$ADMIN_EMAIL" =~ ^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$ ]]; do
+ log_warn "Email inválido!"
+ read -p "📧 Email do administrador: " ADMIN_EMAIL
+ done
+
+ # Senha do admin
+ echo -n "🔐 Senha do administrador (mín. 8 caracteres): "
+ read -s ADMIN_PASSWORD
+ echo ""
+ while [[ ${#ADMIN_PASSWORD} -lt 8 ]]; do
+ log_warn "Senha muito curta!"
+ echo -n "🔐 Senha do administrador (mín. 8 caracteres): "
+ read -s ADMIN_PASSWORD
+ echo ""
+ done
+
+ # Domínio (opcional)
+ read -p "🌐 Domínio (deixe vazio para localhost): " DOMAIN
+ DOMAIN=${DOMAIN:-"localhost"}
+
+ # Porta
+ read -p "🔌 Porta do servidor [8080]: " SERVER_PORT
+ SERVER_PORT=${SERVER_PORT:-8080}
+
+ # Porta do dashboard
+ read -p "🖥️ Porta do dashboard [3000]: " DASHBOARD_PORT
+ DASHBOARD_PORT=${DASHBOARD_PORT:-3000}
+
+ # Habilitar HTTPS?
+ if [[ "$DOMAIN" != "localhost" ]]; then
+ read -p "🔒 Habilitar HTTPS com Let's Encrypt? (s/n) [s]: " ENABLE_HTTPS
+ ENABLE_HTTPS=${ENABLE_HTTPS:-s}
+ else
+ ENABLE_HTTPS="n"
+ fi
+
+ # Telegram para alertas (opcional)
+ echo ""
+ read -p "📱 Configurar alertas no Telegram? (s/n) [n]: " ENABLE_TELEGRAM
+ if [[ "$ENABLE_TELEGRAM" =~ ^[sS]$ ]]; then
+ read -p " Bot Token: " TELEGRAM_BOT_TOKEN
+ read -p " Chat ID: " TELEGRAM_CHAT_ID
+ fi
+
+ echo ""
+ log_success "Informações coletadas!"
+}
+
+# Criar diretório de instalação
+setup_directory() {
+ INSTALL_DIR="/opt/ophion"
+
+ log_info "Criando diretório de instalação em $INSTALL_DIR..."
+
+ sudo mkdir -p "$INSTALL_DIR"
+ sudo mkdir -p "$INSTALL_DIR/data/postgres"
+ sudo mkdir -p "$INSTALL_DIR/data/clickhouse"
+ sudo mkdir -p "$INSTALL_DIR/data/redis"
+ sudo mkdir -p "$INSTALL_DIR/configs"
+ sudo mkdir -p "$INSTALL_DIR/logs"
+
+ sudo chown -R $USER:$USER "$INSTALL_DIR"
+
+ cd "$INSTALL_DIR"
+ log_success "Diretório criado"
+}
+
+# Gerar arquivo .env
+generate_env() {
+ log_info "Gerando configuração..."
+
+ JWT_SECRET=$(generate_secret)
+ POSTGRES_PASSWORD=$(generate_secret | head -c 32)
+ API_KEY=$(generate_api_key)
+
+ cat > "$INSTALL_DIR/.env" << EOF
+# ═══════════════════════════════════════════════════════════
+# 🐍 OPHION - Configuração
+# Gerado em: $(date)
+# Organização: $ORG_NAME
+# ═══════════════════════════════════════════════════════════
+
+# Organização
+ORG_NAME="$ORG_NAME"
+ADMIN_EMAIL="$ADMIN_EMAIL"
+ADMIN_PASSWORD="$ADMIN_PASSWORD"
+
+# Rede
+DOMAIN=$DOMAIN
+SERVER_PORT=$SERVER_PORT
+DASHBOARD_PORT=$DASHBOARD_PORT
+API_URL=http://ophion-server:8080
+
+# Segurança (NÃO COMPARTILHE!)
+JWT_SECRET=$JWT_SECRET
+API_KEY=$API_KEY
+
+# PostgreSQL
+POSTGRES_USER=ophion
+POSTGRES_PASSWORD=$POSTGRES_PASSWORD
+POSTGRES_DB=ophion
+DATABASE_URL=postgres://ophion:$POSTGRES_PASSWORD@postgres:5432/ophion
+
+# ClickHouse
+CLICKHOUSE_URL=clickhouse://clickhouse:9000/ophion
+
+# Redis
+REDIS_URL=redis://redis:6379
+
+# Telegram Alertas
+TELEGRAM_ENABLED=${ENABLE_TELEGRAM:-n}
+TELEGRAM_BOT_TOKEN=${TELEGRAM_BOT_TOKEN:-}
+TELEGRAM_CHAT_ID=${TELEGRAM_CHAT_ID:-}
+
+# OpenAI (para recursos de IA)
+OPENAI_API_KEY=
+
+# Timezone
+TZ=America/Sao_Paulo
+EOF
+
+ chmod 600 "$INSTALL_DIR/.env"
+ log_success "Arquivo .env gerado"
+}
+
+# Gerar docker-compose.yml
+generate_compose() {
+ log_info "Gerando docker-compose.yml..."
+
+ cat > "$INSTALL_DIR/docker-compose.yml" << 'EOF'
+version: '3.8'
+
+services:
+ # ═══════════════════════════════════════════════════════════
+ # 🐍 OPHION Server (API)
+ # ═══════════════════════════════════════════════════════════
+ ophion-server:
+ image: ghcr.io/bigtux/ophion-server:latest
+ container_name: ophion-server
+ ports:
+ - "${SERVER_PORT}:8080"
+ environment:
+ - DATABASE_URL=${DATABASE_URL}
+ - CLICKHOUSE_URL=${CLICKHOUSE_URL}
+ - REDIS_URL=${REDIS_URL}
+ - JWT_SECRET=${JWT_SECRET}
+ - ADMIN_EMAIL=${ADMIN_EMAIL}
+ - ADMIN_PASSWORD=${ADMIN_PASSWORD}
+ - ORG_NAME=${ORG_NAME}
+ - TELEGRAM_ENABLED=${TELEGRAM_ENABLED}
+ - TELEGRAM_BOT_TOKEN=${TELEGRAM_BOT_TOKEN}
+ - TELEGRAM_CHAT_ID=${TELEGRAM_CHAT_ID}
+ - OPENAI_API_KEY=${OPENAI_API_KEY}
+ - TZ=${TZ}
+ volumes:
+ - ./configs:/app/configs:ro
+ - ./logs:/app/logs
+ depends_on:
+ postgres:
+ condition: service_healthy
+ clickhouse:
+ condition: service_started
+ redis:
+ condition: service_started
+ healthcheck:
+ test: ["CMD", "wget", "-q", "--spider", "http://localhost:8080/health"]
+ interval: 30s
+ timeout: 10s
+ retries: 3
+ restart: unless-stopped
+ networks:
+ - ophion-net
+
+ # ═══════════════════════════════════════════════════════════
+ # 🖥️ OPHION Dashboard (Web UI)
+ # ═══════════════════════════════════════════════════════════
+ ophion-web:
+ image: ghcr.io/bigtux/ophion-web:latest
+ container_name: ophion-web
+ ports:
+ - "${DASHBOARD_PORT}:3000"
+ environment:
+ - API_URL=http://ophion-server:8080
+ - NEXT_PUBLIC_API_URL=http://${DOMAIN}:${SERVER_PORT}
+ - ORG_NAME=${ORG_NAME}
+ depends_on:
+ - ophion-server
+ restart: unless-stopped
+ networks:
+ - ophion-net
+
+ # ═══════════════════════════════════════════════════════════
+ # 🐘 PostgreSQL (Metadados, Usuários, Config)
+ # ═══════════════════════════════════════════════════════════
+ postgres:
+ image: postgres:16-alpine
+ container_name: ophion-postgres
+ environment:
+ - POSTGRES_USER=${POSTGRES_USER}
+ - POSTGRES_PASSWORD=${POSTGRES_PASSWORD}
+ - POSTGRES_DB=${POSTGRES_DB}
+ volumes:
+ - ./data/postgres:/var/lib/postgresql/data
+ - ./init/postgres:/docker-entrypoint-initdb.d:ro
+ healthcheck:
+ test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER} -d ${POSTGRES_DB}"]
+ interval: 10s
+ timeout: 5s
+ retries: 5
+ restart: unless-stopped
+ networks:
+ - ophion-net
+
+ # ═══════════════════════════════════════════════════════════
+ # 🏠 ClickHouse (Métricas, Logs, Traces)
+ # ═══════════════════════════════════════════════════════════
+ clickhouse:
+ image: clickhouse/clickhouse-server:24.1
+ container_name: ophion-clickhouse
+ volumes:
+ - ./data/clickhouse:/var/lib/clickhouse
+ - ./init/clickhouse:/docker-entrypoint-initdb.d:ro
+ ulimits:
+ nofile:
+ soft: 262144
+ hard: 262144
+ restart: unless-stopped
+ networks:
+ - ophion-net
+
+ # ═══════════════════════════════════════════════════════════
+ # 🔴 Redis (Cache, Sessions, Filas)
+ # ═══════════════════════════════════════════════════════════
+ redis:
+ image: redis:7-alpine
+ container_name: ophion-redis
+ command: redis-server --appendonly yes
+ volumes:
+ - ./data/redis:/data
+ healthcheck:
+ test: ["CMD", "redis-cli", "ping"]
+ interval: 10s
+ timeout: 5s
+ retries: 3
+ restart: unless-stopped
+ networks:
+ - ophion-net
+
+networks:
+ ophion-net:
+ driver: bridge
+
+EOF
+
+ log_success "docker-compose.yml gerado"
+}
+
+# Gerar scripts SQL de inicialização
+generate_init_scripts() {
+ log_info "Gerando scripts de inicialização..."
+
+ mkdir -p "$INSTALL_DIR/init/postgres"
+ mkdir -p "$INSTALL_DIR/init/clickhouse"
+
+ # PostgreSQL init
+ cat > "$INSTALL_DIR/init/postgres/01-schema.sql" << 'EOF'
+-- ═══════════════════════════════════════════════════════════
+-- 🐍 OPHION - Schema PostgreSQL
+-- ═══════════════════════════════════════════════════════════
+
+-- Extensões
+CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
+CREATE EXTENSION IF NOT EXISTS "pgcrypto";
+
+-- Organizações
+CREATE TABLE organizations (
+ id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
+ name VARCHAR(255) NOT NULL,
+ slug VARCHAR(100) UNIQUE NOT NULL,
+ settings JSONB DEFAULT '{}',
+ created_at TIMESTAMPTZ DEFAULT NOW(),
+ updated_at TIMESTAMPTZ DEFAULT NOW()
+);
+
+-- Usuários
+CREATE TABLE users (
+ id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
+ org_id UUID REFERENCES organizations(id) ON DELETE CASCADE,
+ email VARCHAR(255) UNIQUE NOT NULL,
+ password_hash VARCHAR(255) NOT NULL,
+ name VARCHAR(255),
+ role VARCHAR(50) DEFAULT 'viewer',
+ avatar_url TEXT,
+ settings JSONB DEFAULT '{}',
+ last_login_at TIMESTAMPTZ,
+ created_at TIMESTAMPTZ DEFAULT NOW(),
+ updated_at TIMESTAMPTZ DEFAULT NOW()
+);
+
+-- API Keys
+CREATE TABLE api_keys (
+ id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
+ org_id UUID REFERENCES organizations(id) ON DELETE CASCADE,
+ key_hash VARCHAR(64) NOT NULL,
+ key_prefix VARCHAR(20) NOT NULL,
+ name VARCHAR(255),
+ description TEXT,
+ scopes TEXT[] DEFAULT ARRAY['metrics:write', 'logs:write'],
+ created_by UUID REFERENCES users(id),
+ last_used_at TIMESTAMPTZ,
+ expires_at TIMESTAMPTZ,
+ revoked BOOLEAN DEFAULT FALSE,
+ created_at TIMESTAMPTZ DEFAULT NOW()
+);
+
+-- Hosts/Agents
+CREATE TABLE hosts (
+ id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
+ org_id UUID REFERENCES organizations(id) ON DELETE CASCADE,
+ hostname VARCHAR(255) NOT NULL,
+ ip_address INET,
+ os VARCHAR(100),
+ arch VARCHAR(50),
+ agent_version VARCHAR(50),
+ tags JSONB DEFAULT '{}',
+ status VARCHAR(50) DEFAULT 'unknown',
+ last_seen_at TIMESTAMPTZ,
+ created_at TIMESTAMPTZ DEFAULT NOW(),
+ UNIQUE(org_id, hostname)
+);
+
+-- Alert Rules
+CREATE TABLE alert_rules (
+ id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
+ org_id UUID REFERENCES organizations(id) ON DELETE CASCADE,
+ name VARCHAR(255) NOT NULL,
+ description TEXT,
+ query TEXT NOT NULL,
+ condition VARCHAR(50) NOT NULL,
+ threshold DECIMAL,
+ severity VARCHAR(50) DEFAULT 'warning',
+ enabled BOOLEAN DEFAULT TRUE,
+ notify_channels JSONB DEFAULT '[]',
+ created_by UUID REFERENCES users(id),
+ created_at TIMESTAMPTZ DEFAULT NOW(),
+ updated_at TIMESTAMPTZ DEFAULT NOW()
+);
+
+-- Alert History
+CREATE TABLE alert_history (
+ id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
+ rule_id UUID REFERENCES alert_rules(id) ON DELETE CASCADE,
+ org_id UUID REFERENCES organizations(id) ON DELETE CASCADE,
+ host_id UUID REFERENCES hosts(id),
+ severity VARCHAR(50),
+ status VARCHAR(50) DEFAULT 'firing',
+ message TEXT,
+ value DECIMAL,
+ fired_at TIMESTAMPTZ DEFAULT NOW(),
+ resolved_at TIMESTAMPTZ,
+ acknowledged_by UUID REFERENCES users(id),
+ acknowledged_at TIMESTAMPTZ
+);
+
+-- Dashboards
+CREATE TABLE dashboards (
+ id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
+ org_id UUID REFERENCES organizations(id) ON DELETE CASCADE,
+ name VARCHAR(255) NOT NULL,
+ description TEXT,
+ layout JSONB DEFAULT '[]',
+ is_default BOOLEAN DEFAULT FALSE,
+ created_by UUID REFERENCES users(id),
+ created_at TIMESTAMPTZ DEFAULT NOW(),
+ updated_at TIMESTAMPTZ DEFAULT NOW()
+);
+
+-- Índices
+CREATE INDEX idx_users_org ON users(org_id);
+CREATE INDEX idx_users_email ON users(email);
+CREATE INDEX idx_api_keys_org ON api_keys(org_id);
+CREATE INDEX idx_api_keys_prefix ON api_keys(key_prefix);
+CREATE INDEX idx_hosts_org ON hosts(org_id);
+CREATE INDEX idx_hosts_status ON hosts(status);
+CREATE INDEX idx_alert_history_org ON alert_history(org_id);
+CREATE INDEX idx_alert_history_status ON alert_history(status);
+CREATE INDEX idx_alert_history_fired ON alert_history(fired_at DESC);
+
+EOF
+
+ # ClickHouse init
+ cat > "$INSTALL_DIR/init/clickhouse/01-schema.sql" << 'EOF'
+-- ═══════════════════════════════════════════════════════════
+-- 🐍 OPHION - Schema ClickHouse
+-- ═══════════════════════════════════════════════════════════
+
+CREATE DATABASE IF NOT EXISTS ophion;
+
+-- Métricas de Sistema
+CREATE TABLE IF NOT EXISTS ophion.metrics (
+ org_id UUID,
+ host_id UUID,
+ hostname LowCardinality(String),
+ metric_name LowCardinality(String),
+ metric_type LowCardinality(String),
+ value Float64,
+ tags Map(String, String),
+ timestamp DateTime64(3),
+ INDEX idx_metric_name metric_name TYPE bloom_filter GRANULARITY 4,
+ INDEX idx_hostname hostname TYPE bloom_filter GRANULARITY 4
+) ENGINE = MergeTree()
+PARTITION BY toYYYYMM(timestamp)
+ORDER BY (org_id, host_id, metric_name, timestamp)
+TTL timestamp + INTERVAL 90 DAY;
+
+-- Logs
+CREATE TABLE IF NOT EXISTS ophion.logs (
+ org_id UUID,
+ host_id UUID,
+ hostname LowCardinality(String),
+ service LowCardinality(String),
+ level LowCardinality(String),
+ message String,
+ attributes Map(String, String),
+ trace_id String,
+ span_id String,
+ timestamp DateTime64(3),
+ INDEX idx_level level TYPE set(0) GRANULARITY 4,
+ INDEX idx_service service TYPE bloom_filter GRANULARITY 4,
+ INDEX idx_message message TYPE tokenbf_v1(10240, 3, 0) GRANULARITY 4
+) ENGINE = MergeTree()
+PARTITION BY toYYYYMM(timestamp)
+ORDER BY (org_id, timestamp, host_id)
+TTL timestamp + INTERVAL 30 DAY;
+
+-- Traces (Spans)
+CREATE TABLE IF NOT EXISTS ophion.traces (
+ org_id UUID,
+ trace_id String,
+ span_id String,
+ parent_span_id String,
+ operation_name LowCardinality(String),
+ service_name LowCardinality(String),
+ kind LowCardinality(String),
+ status_code UInt8,
+ status_message String,
+ attributes Map(String, String),
+ events Nested(
+ name String,
+ timestamp DateTime64(3),
+ attributes Map(String, String)
+ ),
+ duration_ms Float64,
+ start_time DateTime64(3),
+ end_time DateTime64(3),
+ INDEX idx_trace_id trace_id TYPE bloom_filter GRANULARITY 4,
+ INDEX idx_service service_name TYPE bloom_filter GRANULARITY 4
+) ENGINE = MergeTree()
+PARTITION BY toYYYYMM(start_time)
+ORDER BY (org_id, service_name, start_time, trace_id)
+TTL start_time + INTERVAL 14 DAY;
+
+-- Aggregated metrics (rollups)
+CREATE TABLE IF NOT EXISTS ophion.metrics_hourly (
+ org_id UUID,
+ host_id UUID,
+ metric_name LowCardinality(String),
+ hour DateTime,
+ min_value Float64,
+ max_value Float64,
+ avg_value Float64,
+ count UInt64
+) ENGINE = SummingMergeTree()
+PARTITION BY toYYYYMM(hour)
+ORDER BY (org_id, host_id, metric_name, hour)
+TTL hour + INTERVAL 1 YEAR;
+
+-- Materialized view para rollup
+CREATE MATERIALIZED VIEW IF NOT EXISTS ophion.metrics_hourly_mv
+TO ophion.metrics_hourly AS
+SELECT
+ org_id,
+ host_id,
+ metric_name,
+ toStartOfHour(timestamp) AS hour,
+ min(value) AS min_value,
+ max(value) AS max_value,
+ avg(value) AS avg_value,
+ count() AS count
+FROM ophion.metrics
+GROUP BY org_id, host_id, metric_name, hour;
+
+EOF
+
+ log_success "Scripts de inicialização gerados"
+}
+
+# Gerar script do Agent
+generate_agent_installer() {
+ log_info "Gerando instalador do agent..."
+
+ mkdir -p "$INSTALL_DIR/scripts"
+
+ cat > "$INSTALL_DIR/scripts/install-agent.sh" << EOF
+#!/bin/bash
+#
+# 🐍 OPHION Agent Installer
+# Servidor: http://${DOMAIN}:${SERVER_PORT}
+#
+
+set -e
+
+API_KEY="${API_KEY}"
+SERVER_URL="http://${DOMAIN}:${SERVER_PORT}"
+
+echo "🐍 Instalando OPHION Agent..."
+
+# Detectar OS
+if [[ -f /etc/debian_version ]]; then
+ OS="debian"
+elif [[ -f /etc/redhat-release ]]; then
+ OS="redhat"
+else
+ echo "OS não suportado!"
exit 1
fi
-# Create directory
-INSTALL_DIR="${OPHION_DIR:-/opt/ophion}"
-mkdir -p "$INSTALL_DIR"
+# Baixar agent
+curl -fsSL -o /tmp/ophion-agent "\${SERVER_URL}/downloads/agent/linux/amd64/ophion-agent"
+chmod +x /tmp/ophion-agent
+sudo mv /tmp/ophion-agent /usr/local/bin/
+
+# Criar config
+sudo mkdir -p /etc/ophion
+sudo tee /etc/ophion/agent.yaml > /dev/null << AGENTEOF
+server:
+ url: \${SERVER_URL}
+ api_key: \${API_KEY}
+
+collection:
+ interval: 30s
+
+metrics:
+ enabled: true
+ include:
+ - cpu
+ - memory
+ - disk
+ - network
+ - processes
+
+logs:
+ enabled: true
+ paths:
+ - /var/log/syslog
+ - /var/log/auth.log
+AGENTEOF
+
+# Criar systemd service
+sudo tee /etc/systemd/system/ophion-agent.service > /dev/null << SERVICEEOF
+[Unit]
+Description=OPHION Monitoring Agent
+After=network.target
+
+[Service]
+Type=simple
+ExecStart=/usr/local/bin/ophion-agent -config /etc/ophion/agent.yaml
+Restart=always
+RestartSec=10
+
+[Install]
+WantedBy=multi-user.target
+SERVICEEOF
+
+# Iniciar
+sudo systemctl daemon-reload
+sudo systemctl enable ophion-agent
+sudo systemctl start ophion-agent
+
+echo ""
+echo "✅ OPHION Agent instalado!"
+echo " Status: sudo systemctl status ophion-agent"
+echo " Logs: sudo journalctl -u ophion-agent -f"
+EOF
+
+ chmod +x "$INSTALL_DIR/scripts/install-agent.sh"
+ log_success "Instalador do agent gerado"
+}
+
+# Gerar comandos de gerenciamento
+generate_cli() {
+ log_info "Gerando CLI de gerenciamento..."
+
+ cat > "$INSTALL_DIR/ophion" << 'EOF'
+#!/bin/bash
+#
+# 🐍 OPHION CLI
+#
+
+INSTALL_DIR="/opt/ophion"
cd "$INSTALL_DIR"
-# Download docker-compose
-echo "📥 Downloading OPHION..."
-curl -fsSL https://raw.githubusercontent.com/bigtux/ophion/main/deploy/docker/docker-compose.yml -o docker-compose.yml
+case "$1" in
+ start)
+ echo "🚀 Iniciando OPHION..."
+ docker compose up -d
+ echo "✅ OPHION iniciado!"
+ echo " Dashboard: http://localhost:${DASHBOARD_PORT:-3000}"
+ echo " API: http://localhost:${SERVER_PORT:-8080}"
+ ;;
+ stop)
+ echo "🛑 Parando OPHION..."
+ docker compose down
+ echo "✅ OPHION parado"
+ ;;
+ restart)
+ echo "🔄 Reiniciando OPHION..."
+ docker compose restart
+ echo "✅ OPHION reiniciado"
+ ;;
+ status)
+ docker compose ps
+ ;;
+ logs)
+ docker compose logs -f ${2:-ophion-server}
+ ;;
+ update)
+ echo "📦 Atualizando OPHION..."
+ docker compose pull
+ docker compose up -d
+ echo "✅ OPHION atualizado!"
+ ;;
+ backup)
+ BACKUP_DIR="$INSTALL_DIR/backups/$(date +%Y%m%d_%H%M%S)"
+ mkdir -p "$BACKUP_DIR"
+ echo "💾 Criando backup em $BACKUP_DIR..."
+ docker compose exec -T postgres pg_dump -U ophion ophion > "$BACKUP_DIR/postgres.sql"
+ cp "$INSTALL_DIR/.env" "$BACKUP_DIR/"
+ echo "✅ Backup criado!"
+ ;;
+ api-key)
+ NEW_KEY="ophion_$(openssl rand -hex 32)"
+ echo "🔑 Nova API Key gerada:"
+ echo ""
+ echo " $NEW_KEY"
+ echo ""
+ echo "⚠️ Salve esta key! Ela não será mostrada novamente."
+ ;;
+ agent-install)
+ echo ""
+ echo "Para instalar o agent em outro servidor, execute:"
+ echo ""
+ echo " curl -fsSL http://$(hostname -I | awk '{print $1}'):${SERVER_PORT:-8080}/install-agent.sh | sudo bash"
+ echo ""
+ ;;
+ *)
+ echo "🐍 OPHION CLI"
+ echo ""
+ echo "Uso: ophion "
+ echo ""
+ echo "Comandos:"
+ echo " start Iniciar todos os serviços"
+ echo " stop Parar todos os serviços"
+ echo " restart Reiniciar todos os serviços"
+ echo " status Ver status dos serviços"
+ echo " logs [svc] Ver logs (padrão: ophion-server)"
+ echo " update Atualizar para última versão"
+ echo " backup Criar backup dos dados"
+ echo " api-key Gerar nova API key"
+ echo " agent-install Mostrar comando de instalação do agent"
+ ;;
+esac
+EOF
-# Generate secrets
-JWT_SECRET=$(openssl rand -hex 32)
-echo "JWT_SECRET=$JWT_SECRET" > .env
+ chmod +x "$INSTALL_DIR/ophion"
+ sudo ln -sf "$INSTALL_DIR/ophion" /usr/local/bin/ophion
+
+ log_success "CLI instalado em /usr/local/bin/ophion"
+}
-# Start services
-echo "🚀 Starting OPHION..."
-docker compose up -d
+# Resumo final
+show_summary() {
+ echo ""
+ echo -e "${GREEN}═══════════════════════════════════════════════════════════${NC}"
+ echo -e "${GREEN} 🎉 OPHION INSTALADO COM SUCESSO! ${NC}"
+ echo -e "${GREEN}═══════════════════════════════════════════════════════════${NC}"
+ echo ""
+ echo -e " ${CYAN}Organização:${NC} $ORG_NAME"
+ echo -e " ${CYAN}Admin:${NC} $ADMIN_EMAIL"
+ echo ""
+ echo -e " ${CYAN}Dashboard:${NC} http://${DOMAIN}:${DASHBOARD_PORT}"
+ echo -e " ${CYAN}API:${NC} http://${DOMAIN}:${SERVER_PORT}"
+ echo ""
+ echo -e " ${YELLOW}🔑 API Key (SALVE AGORA!):${NC}"
+ echo -e " ${PURPLE}$API_KEY${NC}"
+ echo ""
+ echo -e " ${CYAN}Diretório:${NC} $INSTALL_DIR"
+ echo ""
+ echo -e "${GREEN}───────────────────────────────────────────────────────────${NC}"
+ echo ""
+ echo " Comandos úteis:"
+ echo ""
+ echo " ophion start # Iniciar"
+ echo " ophion stop # Parar"
+ echo " ophion status # Ver status"
+ echo " ophion logs # Ver logs"
+ echo " ophion agent-install # Instalar agent em outros servidores"
+ echo ""
+ echo -e "${GREEN}───────────────────────────────────────────────────────────${NC}"
+ echo ""
+
+ read -p "🚀 Iniciar OPHION agora? (s/n) [s]: " START_NOW
+ START_NOW=${START_NOW:-s}
+
+ if [[ "$START_NOW" =~ ^[sS]$ ]]; then
+ echo ""
+ log_info "Iniciando serviços..."
+ cd "$INSTALL_DIR"
+ docker compose up -d
+
+ echo ""
+ log_success "OPHION está rodando!"
+ echo ""
+ echo -e " Acesse: ${CYAN}http://${DOMAIN}:${DASHBOARD_PORT}${NC}"
+ echo -e " Login: ${CYAN}${ADMIN_EMAIL}${NC}"
+ echo ""
+ fi
+}
-echo ""
-echo "✅ OPHION installed successfully!"
-echo ""
-echo "📊 Dashboard: http://localhost:3000"
-echo "🔌 API: http://localhost:8080"
-echo ""
-echo "Next steps:"
-echo "1. Open http://localhost:3000 in your browser"
-echo "2. Create your admin account"
-echo "3. Add your first server with the agent"
-echo ""
-echo "To install the agent on a server:"
-echo " curl -fsSL https://get.ophion.io/agent | bash"
-echo ""
+# ═══════════════════════════════════════════════════════════
+# MAIN
+# ═══════════════════════════════════════════════════════════
+
+main() {
+ clear
+ show_banner
+ check_requirements
+ collect_info
+ setup_directory
+ generate_env
+ generate_compose
+ generate_init_scripts
+ generate_agent_installer
+ generate_cli
+ show_summary
+}
+
+main "$@"
diff --git a/internal/api/ratelimit.go b/internal/api/ratelimit.go
index 7a71087..df03512 100644
--- a/internal/api/ratelimit.go
+++ b/internal/api/ratelimit.go
@@ -1,27 +1,265 @@
package api
import (
+ "fmt"
+ "strconv"
+ "sync"
"time"
"github.com/gofiber/fiber/v2"
- "github.com/gofiber/fiber/v2/middleware/limiter"
)
-func RateLimitMiddleware() fiber.Handler {
- return limiter.New(limiter.Config{
- Max: 100, // 100 requests
- Expiration: 1 * time.Minute, // per minute
+// ═══════════════════════════════════════════════════════════
+// 🚦 RATE LIMITING MIDDLEWARE
+// ═══════════════════════════════════════════════════════════
+
+// RateLimitConfig configuração do rate limiter
+type RateLimitConfig struct {
+ // Requests máximos por janela
+ Max int
+
+ // Janela de tempo
+ Window time.Duration
+
+ // Função para extrair identificador (default: IP)
+ KeyGenerator func(*fiber.Ctx) string
+
+ // Função para resposta quando limitado
+ LimitReached func(*fiber.Ctx) error
+
+ // Pular rate limit para certos paths
+ SkipPaths []string
+
+ // Headers customizados
+ Headers RateLimitHeaders
+}
+
+// RateLimitHeaders headers do rate limit
+type RateLimitHeaders struct {
+ Limit string // X-RateLimit-Limit
+ Remaining string // X-RateLimit-Remaining
+ Reset string // X-RateLimit-Reset
+ RetryAfter string // Retry-After
+}
+
+// DefaultRateLimitConfig configuração padrão
+func DefaultRateLimitConfig() RateLimitConfig {
+ return RateLimitConfig{
+ Max: 100,
+ Window: time.Minute,
KeyGenerator: func(c *fiber.Ctx) string {
- // Use API key or IP for rate limiting
- if key := c.Locals("api_key"); key != nil {
- return key.(string)
- }
return c.IP()
},
LimitReached: func(c *fiber.Ctx) error {
return c.Status(429).JSON(fiber.Map{
- "error": "Rate limit exceeded",
+ "error": "Too Many Requests",
+ "message": "Rate limit exceeded. Please slow down.",
+ "retry_after": 60,
+ })
+ },
+ Headers: RateLimitHeaders{
+ Limit: "X-RateLimit-Limit",
+ Remaining: "X-RateLimit-Remaining",
+ Reset: "X-RateLimit-Reset",
+ RetryAfter: "Retry-After",
+ },
+ }
+}
+
+// rateLimitStore armazena contagem de requests
+type rateLimitStore struct {
+ sync.RWMutex
+ entries map[string]*rateLimitEntry
+}
+
+type rateLimitEntry struct {
+ count int
+ expiresAt time.Time
+}
+
+var store = &rateLimitStore{
+ entries: make(map[string]*rateLimitEntry),
+}
+
+// Limpar entries expirados periodicamente
+func init() {
+ go func() {
+ ticker := time.NewTicker(time.Minute)
+ for range ticker.C {
+ store.cleanup()
+ }
+ }()
+}
+
+func (s *rateLimitStore) cleanup() {
+ s.Lock()
+ defer s.Unlock()
+
+ now := time.Now()
+ for key, entry := range s.entries {
+ if now.After(entry.expiresAt) {
+ delete(s.entries, key)
+ }
+ }
+}
+
+// RateLimit middleware de rate limiting
+func RateLimit(config ...RateLimitConfig) fiber.Handler {
+ cfg := DefaultRateLimitConfig()
+ if len(config) > 0 {
+ cfg = config[0]
+ }
+
+ return func(c *fiber.Ctx) error {
+ // Verificar skip paths
+ path := c.Path()
+ for _, skip := range cfg.SkipPaths {
+ if path == skip {
+ return c.Next()
+ }
+ }
+
+ // Gerar key
+ key := cfg.KeyGenerator(c)
+
+ // Verificar/atualizar contagem
+ store.Lock()
+
+ now := time.Now()
+ entry, exists := store.entries[key]
+
+ if !exists || now.After(entry.expiresAt) {
+ // Nova janela
+ store.entries[key] = &rateLimitEntry{
+ count: 1,
+ expiresAt: now.Add(cfg.Window),
+ }
+ store.Unlock()
+
+ // Headers
+ c.Set(cfg.Headers.Limit, strconv.Itoa(cfg.Max))
+ c.Set(cfg.Headers.Remaining, strconv.Itoa(cfg.Max-1))
+ c.Set(cfg.Headers.Reset, strconv.FormatInt(now.Add(cfg.Window).Unix(), 10))
+
+ return c.Next()
+ }
+
+ entry.count++
+ remaining := cfg.Max - entry.count
+ resetTime := entry.expiresAt.Unix()
+
+ store.Unlock()
+
+ // Headers
+ c.Set(cfg.Headers.Limit, strconv.Itoa(cfg.Max))
+ c.Set(cfg.Headers.Remaining, strconv.Itoa(max(0, remaining)))
+ c.Set(cfg.Headers.Reset, strconv.FormatInt(resetTime, 10))
+
+ // Verificar se excedeu
+ if remaining < 0 {
+ retryAfter := int(time.Until(entry.expiresAt).Seconds())
+ c.Set(cfg.Headers.RetryAfter, strconv.Itoa(max(1, retryAfter)))
+ return cfg.LimitReached(c)
+ }
+
+ return c.Next()
+ }
+}
+
+// ═══════════════════════════════════════════════════════════
+// 🎯 RATE LIMIT PRESETS
+// ═══════════════════════════════════════════════════════════
+
+// RateLimitAuth rate limit para endpoints de autenticação (mais restritivo)
+func RateLimitAuth() fiber.Handler {
+ return RateLimit(RateLimitConfig{
+ Max: 5,
+ Window: time.Minute,
+ KeyGenerator: func(c *fiber.Ctx) string {
+ // Combinar IP + email para prevenir ataques distribuídos
+ email := c.FormValue("email")
+ if email == "" {
+ email = c.Query("email")
+ }
+ return fmt.Sprintf("auth:%s:%s", c.IP(), email)
+ },
+ LimitReached: func(c *fiber.Ctx) error {
+ return c.Status(429).JSON(fiber.Map{
+ "error": "Too Many Login Attempts",
+ "message": "You have exceeded the maximum number of login attempts. Please wait before trying again.",
+ "retry_after": 60,
})
},
})
}
+
+// RateLimitAPI rate limit para API geral
+func RateLimitAPI() fiber.Handler {
+ return RateLimit(RateLimitConfig{
+ Max: 100,
+ Window: time.Minute,
+ KeyGenerator: func(c *fiber.Ctx) string {
+ // Usar API key ou IP
+ if apiKey := c.Locals("api_key_id"); apiKey != nil {
+ return fmt.Sprintf("api:%v", apiKey)
+ }
+ if userID := c.Locals("user_id"); userID != nil {
+ return fmt.Sprintf("user:%v", userID)
+ }
+ return fmt.Sprintf("ip:%s", c.IP())
+ },
+ })
+}
+
+// RateLimitIngest rate limit para ingestão de dados (mais permissivo)
+func RateLimitIngest() fiber.Handler {
+ return RateLimit(RateLimitConfig{
+ Max: 1000,
+ Window: time.Minute,
+ KeyGenerator: func(c *fiber.Ctx) string {
+ if orgID := c.Locals("org_id"); orgID != nil {
+ return fmt.Sprintf("ingest:%v", orgID)
+ }
+ return fmt.Sprintf("ingest:ip:%s", c.IP())
+ },
+ LimitReached: func(c *fiber.Ctx) error {
+ return c.Status(429).JSON(fiber.Map{
+ "error": "Ingest Rate Limit Exceeded",
+ "message": "Your organization has exceeded the data ingestion rate limit.",
+ "retry_after": 10,
+ })
+ },
+ })
+}
+
+// RateLimitExport rate limit para exportação de dados
+func RateLimitExport() fiber.Handler {
+ return RateLimit(RateLimitConfig{
+ Max: 10,
+ Window: time.Hour,
+ KeyGenerator: func(c *fiber.Ctx) string {
+ if userID := c.Locals("user_id"); userID != nil {
+ return fmt.Sprintf("export:%v", userID)
+ }
+ return fmt.Sprintf("export:ip:%s", c.IP())
+ },
+ LimitReached: func(c *fiber.Ctx) error {
+ return c.Status(429).JSON(fiber.Map{
+ "error": "Export Rate Limit Exceeded",
+ "message": "You have exceeded the maximum number of exports per hour.",
+ "retry_after": 3600,
+ })
+ },
+ })
+}
+
+// ═══════════════════════════════════════════════════════════
+// 🔧 HELPERS
+// ═══════════════════════════════════════════════════════════
+
+func max(a, b int) int {
+ if a > b {
+ return a
+ }
+ return b
+}
diff --git a/internal/auth/middleware.go b/internal/auth/middleware.go
index 261914a..59defae 100644
--- a/internal/auth/middleware.go
+++ b/internal/auth/middleware.go
@@ -1,93 +1,373 @@
package auth
import (
+ "context"
"crypto/rand"
"encoding/hex"
+ "fmt"
"strings"
"time"
+ "github.com/bigtux/ophion/internal/security"
"github.com/gofiber/fiber/v2"
"github.com/golang-jwt/jwt/v5"
+ "github.com/redis/go-redis/v9"
)
-var jwtSecret []byte
+// ═══════════════════════════════════════════════════════════
+// 🔐 AUTH CONFIG
+// ═══════════════════════════════════════════════════════════
-func Init(secret string) {
- jwtSecret = []byte(secret)
+type AuthConfig struct {
+ JWTSecret []byte
+ JWTExpiration time.Duration
+ RefreshExpiration time.Duration
+ Issuer string
}
-// GenerateAPIKey creates a new API key for agents
-func GenerateAPIKey() string {
- bytes := make([]byte, 32)
- rand.Read(bytes)
- return "ophion_" + hex.EncodeToString(bytes)
+type AuthService struct {
+ config AuthConfig
+ redis *redis.Client
+ rateLimiter *security.RateLimiter
+ loginTracker *security.LoginAttemptTracker
+ apiKeyStore APIKeyStore
}
-// GenerateJWT creates a JWT token for users
-func GenerateJWT(userID string, email string) (string, error) {
- claims := jwt.MapClaims{
- "sub": userID,
- "email": email,
- "iat": time.Now().Unix(),
- "exp": time.Now().Add(24 * time.Hour).Unix(),
+// APIKeyStore interface para storage de API keys
+type APIKeyStore interface {
+ ValidateKey(ctx context.Context, keyHash string) (*APIKeyInfo, error)
+ UpdateLastUsed(ctx context.Context, keyID string) error
+}
+
+type APIKeyInfo struct {
+ ID string
+ OrgID string
+ Scopes []string
+ Name string
+}
+
+// NewAuthService cria serviço de autenticação
+func NewAuthService(config AuthConfig, redis *redis.Client, apiKeyStore APIKeyStore) *AuthService {
+ return &AuthService{
+ config: config,
+ redis: redis,
+ rateLimiter: security.NewRateLimiter(security.AuthRateLimit, time.Minute),
+ loginTracker: security.NewLoginAttemptTracker(),
+ apiKeyStore: apiKeyStore,
}
-
- token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
- return token.SignedString(jwtSecret)
}
-// ValidateJWT validates a JWT token
-func ValidateJWT(tokenString string) (*jwt.MapClaims, error) {
- token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) {
- return jwtSecret, nil
+// ═══════════════════════════════════════════════════════════
+// 🎫 JWT TOKENS
+// ═══════════════════════════════════════════════════════════
+
+type TokenClaims struct {
+ jwt.RegisteredClaims
+ UserID string `json:"uid"`
+ OrgID string `json:"oid"`
+ Email string `json:"email"`
+ Role string `json:"role"`
+ Scopes []string `json:"scopes,omitempty"`
+ TokenID string `json:"jti"`
+}
+
+// GenerateTokenPair gera access + refresh tokens
+func (s *AuthService) GenerateTokenPair(userID, orgID, email, role string) (accessToken, refreshToken string, err error) {
+ now := time.Now()
+ tokenID := generateTokenID()
+
+ // Access Token (curta duração)
+ accessClaims := TokenClaims{
+ RegisteredClaims: jwt.RegisteredClaims{
+ Issuer: s.config.Issuer,
+ Subject: userID,
+ IssuedAt: jwt.NewNumericDate(now),
+ ExpiresAt: jwt.NewNumericDate(now.Add(s.config.JWTExpiration)),
+ ID: tokenID,
+ },
+ UserID: userID,
+ OrgID: orgID,
+ Email: email,
+ Role: role,
+ TokenID: tokenID,
+ }
+
+ accessToken, err = jwt.NewWithClaims(jwt.SigningMethodHS256, accessClaims).SignedString(s.config.JWTSecret)
+ if err != nil {
+ return "", "", fmt.Errorf("failed to sign access token: %w", err)
+ }
+
+ // Refresh Token (longa duração)
+ refreshID := generateTokenID()
+ refreshClaims := jwt.RegisteredClaims{
+ Issuer: s.config.Issuer,
+ Subject: userID,
+ IssuedAt: jwt.NewNumericDate(now),
+ ExpiresAt: jwt.NewNumericDate(now.Add(s.config.RefreshExpiration)),
+ ID: refreshID,
+ }
+
+ refreshToken, err = jwt.NewWithClaims(jwt.SigningMethodHS256, refreshClaims).SignedString(s.config.JWTSecret)
+ if err != nil {
+ return "", "", fmt.Errorf("failed to sign refresh token: %w", err)
+ }
+
+ // Armazenar refresh token no Redis (permite revogação)
+ if s.redis != nil {
+ ctx := context.Background()
+ key := fmt.Sprintf("refresh_token:%s", refreshID)
+ s.redis.Set(ctx, key, userID, s.config.RefreshExpiration)
+ }
+
+ return accessToken, refreshToken, nil
+}
+
+// ValidateAccessToken valida access token
+func (s *AuthService) ValidateAccessToken(tokenString string) (*TokenClaims, error) {
+ token, err := jwt.ParseWithClaims(tokenString, &TokenClaims{}, func(token *jwt.Token) (interface{}, error) {
+ if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
+ return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"])
+ }
+ return s.config.JWTSecret, nil
})
-
- if err != nil || !token.Valid {
- return nil, err
+
+ if err != nil {
+ return nil, fmt.Errorf("invalid token: %w", err)
}
-
- claims, ok := token.Claims.(jwt.MapClaims)
- if !ok {
- return nil, jwt.ErrInvalidKey
+
+ claims, ok := token.Claims.(*TokenClaims)
+ if !ok || !token.Valid {
+ return nil, fmt.Errorf("invalid token claims")
}
-
- return &claims, nil
+
+ // Verificar se token foi revogado
+ if s.redis != nil {
+ ctx := context.Background()
+ revoked, _ := s.redis.Get(ctx, fmt.Sprintf("revoked_token:%s", claims.TokenID)).Result()
+ if revoked != "" {
+ return nil, fmt.Errorf("token revoked")
+ }
+ }
+
+ return claims, nil
}
-// AuthMiddleware protects routes
-func AuthMiddleware() fiber.Handler {
+// RevokeToken revoga um token
+func (s *AuthService) RevokeToken(tokenID string, expiration time.Duration) error {
+ if s.redis == nil {
+ return nil
+ }
+ ctx := context.Background()
+ return s.redis.Set(ctx, fmt.Sprintf("revoked_token:%s", tokenID), "1", expiration).Err()
+}
+
+// ═══════════════════════════════════════════════════════════
+// 🛡️ MIDDLEWARE
+// ═══════════════════════════════════════════════════════════
+
+// AuthMiddleware middleware de autenticação
+func (s *AuthService) AuthMiddleware() fiber.Handler {
return func(c *fiber.Ctx) error {
- authHeader := c.Get("Authorization")
+ // Obter IP real (considerando proxies)
+ ip := c.IP()
+ if forwarded := c.Get("X-Forwarded-For"); forwarded != "" {
+ ip = strings.Split(forwarded, ",")[0]
+ }
+ // Rate limiting por IP
+ if !s.rateLimiter.Allow(ip) {
+ return c.Status(429).JSON(fiber.Map{
+ "error": "Too many requests",
+ "message": "Rate limit exceeded. Try again later.",
+ })
+ }
+
+ authHeader := c.Get("Authorization")
if authHeader == "" {
return c.Status(401).JSON(fiber.Map{
- "error": "Missing authorization header",
+ "error": "Unauthorized",
+ "message": "Missing authorization header",
})
}
-
- // Support both "Bearer " and API keys
+
token := strings.TrimPrefix(authHeader, "Bearer ")
- // Check if it's an API key
- if strings.HasPrefix(token, "ophion_") {
- // TODO: Validate API key against database
- c.Locals("auth_type", "api_key")
- c.Locals("api_key", token)
- return c.Next()
+ // API Key authentication
+ if strings.HasPrefix(token, security.APIKeyPrefix) {
+ return s.authenticateAPIKey(c, token)
}
+
+ // JWT authentication
+ return s.authenticateJWT(c, token)
+ }
+}
- // Validate JWT
- claims, err := ValidateJWT(token)
- if err != nil {
- return c.Status(401).JSON(fiber.Map{
- "error": "Invalid token",
+// authenticateAPIKey valida API key
+func (s *AuthService) authenticateAPIKey(c *fiber.Ctx, apiKey string) error {
+ // Validar formato
+ if !security.ValidateAPIKeyFormat(apiKey) {
+ return c.Status(401).JSON(fiber.Map{
+ "error": "Unauthorized",
+ "message": "Invalid API key format",
+ })
+ }
+
+ // Hash da key para busca
+ keyHash := security.HashAPIKey(apiKey)
+
+ // Buscar no storage
+ keyInfo, err := s.apiKeyStore.ValidateKey(c.Context(), keyHash)
+ if err != nil || keyInfo == nil {
+ return c.Status(401).JSON(fiber.Map{
+ "error": "Unauthorized",
+ "message": "Invalid API key",
+ })
+ }
+
+ // Atualizar last_used (async)
+ go s.apiKeyStore.UpdateLastUsed(context.Background(), keyInfo.ID)
+
+ // Setar contexto
+ c.Locals("auth_type", "api_key")
+ c.Locals("org_id", keyInfo.OrgID)
+ c.Locals("api_key_id", keyInfo.ID)
+ c.Locals("scopes", keyInfo.Scopes)
+
+ return c.Next()
+}
+
+// authenticateJWT valida JWT token
+func (s *AuthService) authenticateJWT(c *fiber.Ctx, token string) error {
+ claims, err := s.ValidateAccessToken(token)
+ if err != nil {
+ return c.Status(401).JSON(fiber.Map{
+ "error": "Unauthorized",
+ "message": err.Error(),
+ })
+ }
+
+ // Setar contexto
+ c.Locals("auth_type", "jwt")
+ c.Locals("user_id", claims.UserID)
+ c.Locals("org_id", claims.OrgID)
+ c.Locals("email", claims.Email)
+ c.Locals("role", claims.Role)
+ c.Locals("token_id", claims.TokenID)
+
+ return c.Next()
+}
+
+// RequireScopes middleware que exige scopes específicos
+func RequireScopes(required ...string) fiber.Handler {
+ return func(c *fiber.Ctx) error {
+ scopes, ok := c.Locals("scopes").([]string)
+ if !ok {
+ // JWT tokens têm acesso total por padrão
+ if c.Locals("auth_type") == "jwt" {
+ return c.Next()
+ }
+ return c.Status(403).JSON(fiber.Map{
+ "error": "Forbidden",
+ "message": "Missing required scopes",
})
}
-
- c.Locals("auth_type", "jwt")
- c.Locals("user_id", (*claims)["sub"])
- c.Locals("email", (*claims)["email"])
+
+ scopeMap := make(map[string]bool)
+ for _, s := range scopes {
+ scopeMap[s] = true
+ }
+
+ for _, req := range required {
+ if !scopeMap[req] && !scopeMap["*"] {
+ return c.Status(403).JSON(fiber.Map{
+ "error": "Forbidden",
+ "message": fmt.Sprintf("Missing required scope: %s", req),
+ })
+ }
+ }
return c.Next()
}
}
+
+// RequireRole middleware que exige role específico
+func RequireRole(roles ...string) fiber.Handler {
+ return func(c *fiber.Ctx) error {
+ userRole, ok := c.Locals("role").(string)
+ if !ok {
+ return c.Status(403).JSON(fiber.Map{
+ "error": "Forbidden",
+ "message": "Role not found",
+ })
+ }
+
+ for _, r := range roles {
+ if userRole == r {
+ return c.Next()
+ }
+ }
+
+ // Admin tem acesso a tudo
+ if userRole == "admin" {
+ return c.Next()
+ }
+
+ return c.Status(403).JSON(fiber.Map{
+ "error": "Forbidden",
+ "message": "Insufficient permissions",
+ })
+ }
+}
+
+// ═══════════════════════════════════════════════════════════
+// 🔐 LOGIN PROTECTION
+// ═══════════════════════════════════════════════════════════
+
+// CheckLoginAllowed verifica se login é permitido (brute force protection)
+func (s *AuthService) CheckLoginAllowed(identifier string) (bool, time.Duration, error) {
+ locked, remaining := s.loginTracker.IsLocked(identifier)
+ return !locked, remaining, nil
+}
+
+// RecordLoginAttempt registra tentativa de login
+func (s *AuthService) RecordLoginAttempt(identifier string, success bool) (blocked bool, remaining int) {
+ if success {
+ s.loginTracker.RecordSuccess(identifier)
+ return false, 0
+ }
+ return s.loginTracker.RecordFailure(identifier)
+}
+
+// ═══════════════════════════════════════════════════════════
+// 🔧 HELPERS
+// ═══════════════════════════════════════════════════════════
+
+func generateTokenID() string {
+ bytes := make([]byte, 16)
+ rand.Read(bytes)
+ return hex.EncodeToString(bytes)
+}
+
+// GetUserID obtém user ID do contexto
+func GetUserID(c *fiber.Ctx) string {
+ if id, ok := c.Locals("user_id").(string); ok {
+ return id
+ }
+ return ""
+}
+
+// GetOrgID obtém org ID do contexto
+func GetOrgID(c *fiber.Ctx) string {
+ if id, ok := c.Locals("org_id").(string); ok {
+ return id
+ }
+ return ""
+}
+
+// GetRole obtém role do contexto
+func GetRole(c *fiber.Ctx) string {
+ if role, ok := c.Locals("role").(string); ok {
+ return role
+ }
+ return ""
+}
diff --git a/internal/security/security.go b/internal/security/security.go
new file mode 100644
index 0000000..193f42b
--- /dev/null
+++ b/internal/security/security.go
@@ -0,0 +1,558 @@
+package security
+
+import (
+ "crypto/rand"
+ "crypto/sha256"
+ "crypto/subtle"
+ "encoding/base64"
+ "encoding/hex"
+ "fmt"
+ "net"
+ "regexp"
+ "strings"
+ "sync"
+ "time"
+
+ "github.com/gofiber/fiber/v2"
+ "golang.org/x/crypto/bcrypt"
+)
+
+// ═══════════════════════════════════════════════════════════
+// 🔐 CONSTANTES DE SEGURANÇA
+// ═══════════════════════════════════════════════════════════
+
+const (
+ // Bcrypt cost (12-14 recomendado para produção)
+ BcryptCost = 12
+
+ // Tamanho mínimo de senha
+ MinPasswordLength = 12
+
+ // Tamanho da API Key
+ APIKeyLength = 32
+
+ // Prefixo da API Key
+ APIKeyPrefix = "ophion_"
+
+ // Rate limit padrão (requests por minuto)
+ DefaultRateLimit = 100
+
+ // Rate limit para auth (tentativas por minuto)
+ AuthRateLimit = 5
+
+ // Tempo de bloqueio após falhas (minutos)
+ LockoutDuration = 15
+
+ // Máximo de tentativas antes do bloqueio
+ MaxFailedAttempts = 5
+)
+
+// ═══════════════════════════════════════════════════════════
+// 🔑 API KEY MANAGEMENT
+// ═══════════════════════════════════════════════════════════
+
+// GenerateAPIKey cria uma nova API key segura
+func GenerateAPIKey() (key string, hash string, prefix string) {
+ bytes := make([]byte, APIKeyLength)
+ if _, err := rand.Read(bytes); err != nil {
+ panic("failed to generate random bytes")
+ }
+
+ key = APIKeyPrefix + hex.EncodeToString(bytes)
+ hash = HashAPIKey(key)
+ prefix = key[:len(APIKeyPrefix)+8] // ophion_xxxxxxxx
+
+ return key, hash, prefix
+}
+
+// HashAPIKey cria hash SHA256 da API key
+func HashAPIKey(key string) string {
+ hash := sha256.Sum256([]byte(key))
+ return hex.EncodeToString(hash[:])
+}
+
+// ValidateAPIKeyFormat verifica formato da API key
+func ValidateAPIKeyFormat(key string) bool {
+ if !strings.HasPrefix(key, APIKeyPrefix) {
+ return false
+ }
+ // ophion_ + 64 hex chars
+ return len(key) == len(APIKeyPrefix)+64
+}
+
+// SecureCompare compara strings em tempo constante (previne timing attacks)
+func SecureCompare(a, b string) bool {
+ return subtle.ConstantTimeCompare([]byte(a), []byte(b)) == 1
+}
+
+// ═══════════════════════════════════════════════════════════
+// 🔒 PASSWORD MANAGEMENT
+// ═══════════════════════════════════════════════════════════
+
+// PasswordPolicy define regras de senha
+type PasswordPolicy struct {
+ MinLength int
+ RequireUppercase bool
+ RequireLowercase bool
+ RequireNumbers bool
+ RequireSpecial bool
+}
+
+// DefaultPasswordPolicy retorna política padrão
+func DefaultPasswordPolicy() PasswordPolicy {
+ return PasswordPolicy{
+ MinLength: 12,
+ RequireUppercase: true,
+ RequireLowercase: true,
+ RequireNumbers: true,
+ RequireSpecial: true,
+ }
+}
+
+// ValidatePassword verifica se senha atende à política
+func ValidatePassword(password string, policy PasswordPolicy) (bool, []string) {
+ var errors []string
+
+ if len(password) < policy.MinLength {
+ errors = append(errors, fmt.Sprintf("Senha deve ter no mínimo %d caracteres", policy.MinLength))
+ }
+
+ if policy.RequireUppercase && !regexp.MustCompile(`[A-Z]`).MatchString(password) {
+ errors = append(errors, "Senha deve conter letra maiúscula")
+ }
+
+ if policy.RequireLowercase && !regexp.MustCompile(`[a-z]`).MatchString(password) {
+ errors = append(errors, "Senha deve conter letra minúscula")
+ }
+
+ if policy.RequireNumbers && !regexp.MustCompile(`[0-9]`).MatchString(password) {
+ errors = append(errors, "Senha deve conter número")
+ }
+
+ if policy.RequireSpecial && !regexp.MustCompile(`[!@#$%^&*(),.?":{}|<>]`).MatchString(password) {
+ errors = append(errors, "Senha deve conter caractere especial")
+ }
+
+ // Verificar senhas comuns
+ if isCommonPassword(password) {
+ errors = append(errors, "Senha muito comum, escolha outra")
+ }
+
+ return len(errors) == 0, errors
+}
+
+// HashPassword cria hash bcrypt da senha
+func HashPassword(password string) (string, error) {
+ bytes, err := bcrypt.GenerateFromPassword([]byte(password), BcryptCost)
+ return string(bytes), err
+}
+
+// VerifyPassword verifica senha contra hash
+func VerifyPassword(password, hash string) bool {
+ err := bcrypt.CompareHashAndPassword([]byte(hash), []byte(password))
+ return err == nil
+}
+
+// isCommonPassword verifica senhas comuns
+func isCommonPassword(password string) bool {
+ common := []string{
+ "password", "123456", "12345678", "qwerty", "abc123",
+ "monkey", "1234567", "letmein", "trustno1", "dragon",
+ "baseball", "iloveyou", "master", "sunshine", "ashley",
+ "passw0rd", "shadow", "123123", "654321", "superman",
+ "senha", "mudar123", "admin123", "root123",
+ }
+ lower := strings.ToLower(password)
+ for _, p := range common {
+ if strings.Contains(lower, p) {
+ return true
+ }
+ }
+ return false
+}
+
+// ═══════════════════════════════════════════════════════════
+// 🚦 RATE LIMITING
+// ═══════════════════════════════════════════════════════════
+
+// RateLimiter controla taxa de requests
+type RateLimiter struct {
+ sync.RWMutex
+ requests map[string]*rateLimitEntry
+ limit int
+ window time.Duration
+}
+
+type rateLimitEntry struct {
+ count int
+ firstSeen time.Time
+ blocked bool
+ blockedAt time.Time
+}
+
+// NewRateLimiter cria novo rate limiter
+func NewRateLimiter(limit int, window time.Duration) *RateLimiter {
+ rl := &RateLimiter{
+ requests: make(map[string]*rateLimitEntry),
+ limit: limit,
+ window: window,
+ }
+ // Cleanup goroutine
+ go rl.cleanup()
+ return rl
+}
+
+// Allow verifica se request é permitido
+func (rl *RateLimiter) Allow(identifier string) bool {
+ rl.Lock()
+ defer rl.Unlock()
+
+ now := time.Now()
+ entry, exists := rl.requests[identifier]
+
+ if !exists {
+ rl.requests[identifier] = &rateLimitEntry{
+ count: 1,
+ firstSeen: now,
+ }
+ return true
+ }
+
+ // Verificar se está bloqueado
+ if entry.blocked {
+ if now.Sub(entry.blockedAt) < time.Duration(LockoutDuration)*time.Minute {
+ return false
+ }
+ // Desbloquear
+ entry.blocked = false
+ entry.count = 0
+ entry.firstSeen = now
+ }
+
+ // Verificar janela de tempo
+ if now.Sub(entry.firstSeen) > rl.window {
+ entry.count = 1
+ entry.firstSeen = now
+ return true
+ }
+
+ entry.count++
+
+ if entry.count > rl.limit {
+ return false
+ }
+
+ return true
+}
+
+// Block bloqueia um identificador
+func (rl *RateLimiter) Block(identifier string) {
+ rl.Lock()
+ defer rl.Unlock()
+
+ entry, exists := rl.requests[identifier]
+ if !exists {
+ entry = &rateLimitEntry{}
+ rl.requests[identifier] = entry
+ }
+ entry.blocked = true
+ entry.blockedAt = time.Now()
+}
+
+// cleanup remove entradas antigas
+func (rl *RateLimiter) cleanup() {
+ ticker := time.NewTicker(5 * time.Minute)
+ for range ticker.C {
+ rl.Lock()
+ now := time.Now()
+ for id, entry := range rl.requests {
+ if now.Sub(entry.firstSeen) > rl.window*2 && !entry.blocked {
+ delete(rl.requests, id)
+ }
+ }
+ rl.Unlock()
+ }
+}
+
+// ═══════════════════════════════════════════════════════════
+// 🛡️ BRUTE FORCE PROTECTION
+// ═══════════════════════════════════════════════════════════
+
+// LoginAttemptTracker rastreia tentativas de login
+type LoginAttemptTracker struct {
+ sync.RWMutex
+ attempts map[string]*loginAttempt
+}
+
+type loginAttempt struct {
+ failures int
+ lastAttempt time.Time
+ lockedUntil time.Time
+}
+
+// NewLoginAttemptTracker cria tracker
+func NewLoginAttemptTracker() *LoginAttemptTracker {
+ return &LoginAttemptTracker{
+ attempts: make(map[string]*loginAttempt),
+ }
+}
+
+// RecordFailure registra falha de login
+func (t *LoginAttemptTracker) RecordFailure(identifier string) (blocked bool, remainingAttempts int) {
+ t.Lock()
+ defer t.Unlock()
+
+ now := time.Now()
+ attempt, exists := t.attempts[identifier]
+
+ if !exists {
+ t.attempts[identifier] = &loginAttempt{
+ failures: 1,
+ lastAttempt: now,
+ }
+ return false, MaxFailedAttempts - 1
+ }
+
+ // Reset se última tentativa foi há muito tempo
+ if now.Sub(attempt.lastAttempt) > time.Duration(LockoutDuration)*time.Minute {
+ attempt.failures = 1
+ attempt.lastAttempt = now
+ attempt.lockedUntil = time.Time{}
+ return false, MaxFailedAttempts - 1
+ }
+
+ attempt.failures++
+ attempt.lastAttempt = now
+
+ if attempt.failures >= MaxFailedAttempts {
+ attempt.lockedUntil = now.Add(time.Duration(LockoutDuration) * time.Minute)
+ return true, 0
+ }
+
+ return false, MaxFailedAttempts - attempt.failures
+}
+
+// RecordSuccess registra sucesso de login
+func (t *LoginAttemptTracker) RecordSuccess(identifier string) {
+ t.Lock()
+ defer t.Unlock()
+ delete(t.attempts, identifier)
+}
+
+// IsLocked verifica se está bloqueado
+func (t *LoginAttemptTracker) IsLocked(identifier string) (bool, time.Duration) {
+ t.RLock()
+ defer t.RUnlock()
+
+ attempt, exists := t.attempts[identifier]
+ if !exists {
+ return false, 0
+ }
+
+ if attempt.lockedUntil.IsZero() {
+ return false, 0
+ }
+
+ remaining := time.Until(attempt.lockedUntil)
+ if remaining <= 0 {
+ return false, 0
+ }
+
+ return true, remaining
+}
+
+// ═══════════════════════════════════════════════════════════
+// 🔍 INPUT VALIDATION & SANITIZATION
+// ═══════════════════════════════════════════════════════════
+
+// SanitizeEmail limpa e valida email
+func SanitizeEmail(email string) (string, error) {
+ email = strings.TrimSpace(strings.ToLower(email))
+
+ emailRegex := regexp.MustCompile(`^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$`)
+ if !emailRegex.MatchString(email) {
+ return "", fmt.Errorf("email inválido")
+ }
+
+ return email, nil
+}
+
+// SanitizeHostname limpa hostname
+func SanitizeHostname(hostname string) (string, error) {
+ hostname = strings.TrimSpace(hostname)
+
+ // Permitir apenas alfanuméricos, hífens e pontos
+ hostnameRegex := regexp.MustCompile(`^[a-zA-Z0-9][a-zA-Z0-9.-]{0,253}[a-zA-Z0-9]$`)
+ if !hostnameRegex.MatchString(hostname) && len(hostname) > 1 {
+ return "", fmt.Errorf("hostname inválido")
+ }
+
+ return hostname, nil
+}
+
+// SanitizeSQL previne SQL injection (para queries dinâmicas)
+func SanitizeSQL(input string) string {
+ // Remover caracteres perigosos
+ dangerous := []string{"'", "\"", ";", "--", "/*", "*/", "xp_", "sp_"}
+ result := input
+ for _, d := range dangerous {
+ result = strings.ReplaceAll(result, d, "")
+ }
+ return result
+}
+
+// ValidateIPAddress valida endereço IP
+func ValidateIPAddress(ip string) bool {
+ return net.ParseIP(ip) != nil
+}
+
+// ═══════════════════════════════════════════════════════════
+// 🛡️ SECURITY HEADERS MIDDLEWARE
+// ═══════════════════════════════════════════════════════════
+
+// SecurityHeaders adiciona headers de segurança
+func SecurityHeaders() fiber.Handler {
+ return func(c *fiber.Ctx) error {
+ // Prevenir clickjacking
+ c.Set("X-Frame-Options", "DENY")
+
+ // Prevenir MIME type sniffing
+ c.Set("X-Content-Type-Options", "nosniff")
+
+ // XSS Protection
+ c.Set("X-XSS-Protection", "1; mode=block")
+
+ // Content Security Policy
+ c.Set("Content-Security-Policy", "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self' data:; connect-src 'self' wss: https:")
+
+ // HSTS (apenas se HTTPS)
+ if c.Protocol() == "https" {
+ c.Set("Strict-Transport-Security", "max-age=31536000; includeSubDomains; preload")
+ }
+
+ // Referrer Policy
+ c.Set("Referrer-Policy", "strict-origin-when-cross-origin")
+
+ // Permissions Policy
+ c.Set("Permissions-Policy", "geolocation=(), microphone=(), camera=()")
+
+ // Remove Server header
+ c.Set("Server", "")
+
+ return c.Next()
+ }
+}
+
+// ═══════════════════════════════════════════════════════════
+// 📝 AUDIT LOGGING
+// ═══════════════════════════════════════════════════════════
+
+// AuditEvent representa um evento de auditoria
+type AuditEvent struct {
+ Timestamp time.Time `json:"timestamp"`
+ EventType string `json:"event_type"`
+ UserID string `json:"user_id,omitempty"`
+ OrgID string `json:"org_id,omitempty"`
+ IP string `json:"ip"`
+ UserAgent string `json:"user_agent"`
+ Resource string `json:"resource"`
+ Action string `json:"action"`
+ Status string `json:"status"`
+ Details map[string]string `json:"details,omitempty"`
+}
+
+// AuditEventType tipos de eventos
+const (
+ AuditLogin = "auth.login"
+ AuditLogout = "auth.logout"
+ AuditLoginFailed = "auth.login_failed"
+ AuditAPIKeyCreated = "apikey.created"
+ AuditAPIKeyRevoked = "apikey.revoked"
+ AuditUserCreated = "user.created"
+ AuditUserDeleted = "user.deleted"
+ AuditConfigChanged = "config.changed"
+ AuditAlertCreated = "alert.created"
+ AuditDataExport = "data.export"
+)
+
+// AuditLogger interface para logging de auditoria
+type AuditLogger interface {
+ Log(event AuditEvent) error
+}
+
+// ═══════════════════════════════════════════════════════════
+// 🔐 SECRETS MANAGEMENT
+// ═══════════════════════════════════════════════════════════
+
+// GenerateSecureToken gera token seguro
+func GenerateSecureToken(length int) string {
+ bytes := make([]byte, length)
+ if _, err := rand.Read(bytes); err != nil {
+ panic("failed to generate secure token")
+ }
+ return base64.URLEncoding.EncodeToString(bytes)[:length]
+}
+
+// MaskSecret mascara segredos para logging
+func MaskSecret(secret string) string {
+ if len(secret) <= 8 {
+ return "****"
+ }
+ return secret[:4] + "****" + secret[len(secret)-4:]
+}
+
+// ═══════════════════════════════════════════════════════════
+// 🌐 IP FILTERING
+// ═══════════════════════════════════════════════════════════
+
+// IPWhitelist gerencia whitelist de IPs
+type IPWhitelist struct {
+ sync.RWMutex
+ allowed map[string]bool
+ cidrs []*net.IPNet
+}
+
+// NewIPWhitelist cria whitelist
+func NewIPWhitelist(ips []string, cidrs []string) *IPWhitelist {
+ wl := &IPWhitelist{
+ allowed: make(map[string]bool),
+ }
+
+ for _, ip := range ips {
+ wl.allowed[ip] = true
+ }
+
+ for _, cidr := range cidrs {
+ _, network, err := net.ParseCIDR(cidr)
+ if err == nil {
+ wl.cidrs = append(wl.cidrs, network)
+ }
+ }
+
+ return wl
+}
+
+// IsAllowed verifica se IP está permitido
+func (wl *IPWhitelist) IsAllowed(ip string) bool {
+ wl.RLock()
+ defer wl.RUnlock()
+
+ // Verificar lista direta
+ if wl.allowed[ip] {
+ return true
+ }
+
+ // Verificar CIDRs
+ parsedIP := net.ParseIP(ip)
+ if parsedIP == nil {
+ return false
+ }
+
+ for _, network := range wl.cidrs {
+ if network.Contains(parsedIP) {
+ return true
+ }
+ }
+
+ return false
+}