fix(security): Resolve 21 SQL injection vulnerabilities and add transactions
Security fixes (v1.2.2): - Fix SQL injection in analytics.ts (16 occurrences) - Fix SQL injection in advanced-search.ts (1 occurrence) - Fix SQL injection in search-queries.ts (1 occurrence) - Add validateDaysInterval(), isValidISODate(), validatePeriod() to security.ts - Use make_interval(days => N) for safe PostgreSQL intervals - Validate UUIDs BEFORE string construction Transaction support: - bulk-operations.ts: 6 atomic operations with withTransaction() - desk-sync.ts: 2 operations with transactions - export-import.ts: 1 operation with transaction Rate limiting: - Add automatic cleanup of expired entries (every 5 minutes) Audit: - Archive previous audit docs to docs/audits/2026-01-31-v1.2.1/ - Create new AUDIT-REQUEST.md for v1.2.2 verification Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
209
AUDIT-REQUEST.md
209
AUDIT-REQUEST.md
@@ -1,145 +1,118 @@
|
|||||||
# Pedido de Auditoria Externa - MCP Outline PostgreSQL
|
# Pedido de Auditoria de Segurança - MCP Outline PostgreSQL v1.2.2
|
||||||
|
|
||||||
## Resumo do Projecto
|
## Contexto
|
||||||
|
|
||||||
**Nome:** MCP Outline PostgreSQL
|
Este é um servidor MCP (Model Context Protocol) que fornece acesso directo via PostgreSQL à base de dados do Outline Wiki. A versão anterior (v1.2.1) passou por uma auditoria que identificou vulnerabilidades de SQL injection e falta de transacções em operações bulk.
|
||||||
**Versão:** 1.2.1
|
|
||||||
**Repositório:** https://git.descomplicar.pt/ealmeida/mcp-outline-postgresql
|
|
||||||
**Tecnologia:** TypeScript, Node.js, PostgreSQL
|
|
||||||
**Protocolo:** Model Context Protocol (MCP) v1.0.0
|
|
||||||
|
|
||||||
### Descrição
|
**Versão actual:** 1.2.2 (após correcções de segurança)
|
||||||
|
**Total de tools:** 164 ferramentas em 33 módulos
|
||||||
|
|
||||||
Servidor MCP que fornece acesso directo à base de dados PostgreSQL do Outline Wiki. Implementa 164 tools em 33 módulos para operações CRUD, pesquisa, analytics, gestão de permissões e integração com Desk CRM.
|
## Correcções Aplicadas (v1.2.2)
|
||||||
|
|
||||||
**Arquitectura:** Claude Code → MCP Server (stdio) → PostgreSQL (Outline DB)
|
### SQL Injection Prevention
|
||||||
|
- 21 vulnerabilidades corrigidas em `analytics.ts`, `advanced-search.ts`, `search-queries.ts`
|
||||||
|
- Validação de UUIDs ANTES de construir strings SQL
|
||||||
|
- Novas funções: `validateDaysInterval()`, `isValidISODate()`, `validatePeriod()`
|
||||||
|
- Uso de `make_interval(days => N)` para intervalos seguros
|
||||||
|
|
||||||
---
|
### Transacções Atómicas
|
||||||
|
- `bulk-operations.ts`: 6 operações
|
||||||
|
- `desk-sync.ts`: 2 operações
|
||||||
|
- `export-import.ts`: 1 operação
|
||||||
|
|
||||||
## Âmbito da Auditoria
|
### Rate Limiting
|
||||||
|
- Cleanup automático de entradas expiradas (cada 5 minutos)
|
||||||
|
|
||||||
### 1. Segurança (Prioridade Alta)
|
## Pedido de Auditoria
|
||||||
|
|
||||||
- [ ] **SQL Injection** - Validação de queries parametrizadas em todos os 160 handlers
|
Por favor, realiza uma auditoria de segurança completa ao código actual, focando em:
|
||||||
- [ ] **Input Validation** - Verificação de sanitização de inputs (UUIDs, strings, arrays)
|
|
||||||
- [ ] **Rate Limiting** - Eficácia da implementação actual
|
|
||||||
- [ ] **Autenticação** - Validação de acesso à base de dados
|
|
||||||
- [ ] **Exposição de Dados** - Verificar se há fuga de informação sensível nas respostas
|
|
||||||
- [ ] **Permissões** - Validar que operações respeitam modelo de permissões do Outline
|
|
||||||
|
|
||||||
### 2. Qualidade de Código (Prioridade Média)
|
### 1. SQL Injection (Verificação)
|
||||||
|
- Confirmar que todas as interpolações de strings foram eliminadas
|
||||||
|
- Verificar se existem novos vectores de ataque
|
||||||
|
- Validar que as funções de validação são suficientes
|
||||||
|
|
||||||
- [ ] **TypeScript** - Type safety, uso correcto de tipos
|
### 2. Transacções
|
||||||
- [ ] **Error Handling** - Tratamento de erros consistente
|
- Verificar se as transacções estão correctamente implementadas
|
||||||
- [ ] **Padrões** - Consistência entre módulos
|
- Identificar operações que ainda possam beneficiar de transacções
|
||||||
- [ ] **Code Smells** - Duplicação, complexidade ciclomática
|
- Verificar tratamento de erros e rollback
|
||||||
- [ ] **Manutenibilidade** - Facilidade de extensão e manutenção
|
|
||||||
|
|
||||||
### 3. Performance (Prioridade Média)
|
### 3. Autenticação/Autorização
|
||||||
|
- Verificar se existe controlo de acesso adequado
|
||||||
|
- Analisar uso de "admin user" hardcoded em algumas operações
|
||||||
|
|
||||||
- [ ] **Queries SQL** - Optimização, uso de índices
|
### 4. Validação de Input
|
||||||
- [ ] **Connection Pooling** - Configuração adequada
|
- Verificar sanitização de inputs em todas as tools
|
||||||
- [ ] **Memory Leaks** - Potenciais fugas de memória
|
- Identificar campos que podem ser explorados
|
||||||
- [ ] **Pagination** - Implementação eficiente em listagens
|
|
||||||
|
|
||||||
### 4. Compatibilidade (Prioridade Baixa)
|
### 5. Rate Limiting
|
||||||
|
- Verificar eficácia do rate limiter actual
|
||||||
|
- Sugerir melhorias se necessário
|
||||||
|
|
||||||
- [ ] **Schema Outline** - Compatibilidade com Outline v0.78+
|
### 6. Logging e Auditoria
|
||||||
- [ ] **MCP Protocol** - Conformidade com especificação MCP
|
- Verificar se operações sensíveis são registadas
|
||||||
- [ ] **Node.js** - Compatibilidade com versões LTS
|
- Identificar lacunas em logging
|
||||||
|
|
||||||
---
|
### 7. Dependências
|
||||||
|
- Verificar se existem dependências com vulnerabilidades conhecidas
|
||||||
|
|
||||||
## Ficheiros Críticos para Revisão
|
## Estrutura do Projecto
|
||||||
|
|
||||||
| Ficheiro | Descrição | Prioridade |
|
```
|
||||||
|----------|-----------|------------|
|
src/
|
||||||
| `src/utils/security.ts` | Funções de segurança e validação | **Alta** |
|
├── index.ts # MCP entry point
|
||||||
| `src/pg-client.ts` | Cliente PostgreSQL e pooling | **Alta** |
|
├── pg-client.ts # PostgreSQL client wrapper
|
||||||
| `src/tools/documents.ts` | 19 tools - maior módulo | **Alta** |
|
├── config/database.ts # DB configuration
|
||||||
| `src/tools/users.ts` | Gestão de utilizadores | **Alta** |
|
├── utils/
|
||||||
| `src/tools/bulk-operations.ts` | Operações em lote | **Alta** |
|
│ ├── logger.ts
|
||||||
| `src/tools/advanced-search.ts` | Pesquisa full-text | Média |
|
│ └── security.ts # Validações, rate limiting
|
||||||
| `src/tools/analytics.ts` | Queries analíticas | Média |
|
└── tools/ # 33 módulos de tools
|
||||||
| `src/tools/export-import.ts` | Export/Import Markdown | Média |
|
├── analytics.ts # CORRIGIDO v1.2.2
|
||||||
| `src/tools/desk-sync.ts` | Integração Desk CRM | Média |
|
├── advanced-search.ts # CORRIGIDO v1.2.2
|
||||||
| `src/index.ts` | Entry point MCP | Média |
|
├── search-queries.ts # CORRIGIDO v1.2.2
|
||||||
|
├── bulk-operations.ts # TRANSACÇÕES v1.2.2
|
||||||
|
├── desk-sync.ts # TRANSACÇÕES v1.2.2
|
||||||
|
├── export-import.ts # TRANSACÇÕES v1.2.2
|
||||||
|
└── [outros 27 módulos]
|
||||||
|
```
|
||||||
|
|
||||||
---
|
## Ficheiros Prioritários para Análise
|
||||||
|
|
||||||
## Métricas do Projecto
|
1. `src/utils/security.ts` - Funções de validação e rate limiting
|
||||||
|
2. `src/tools/analytics.ts` - Maior quantidade de correcções
|
||||||
|
3. `src/tools/bulk-operations.ts` - Operações críticas com transacções
|
||||||
|
4. `src/tools/documents.ts` - CRUD principal
|
||||||
|
5. `src/tools/users.ts` - Gestão de utilizadores
|
||||||
|
|
||||||
| Métrica | Valor |
|
## Output Esperado
|
||||||
|---------|-------|
|
|
||||||
| Total de Tools | 164 |
|
|
||||||
| Módulos | 33 |
|
|
||||||
| Linhas de Código (estimado) | ~6500 |
|
|
||||||
| Ficheiros TypeScript | 37 |
|
|
||||||
| Dependências Runtime | 4 |
|
|
||||||
|
|
||||||
### Dependências
|
1. **Score de segurança** (0-10)
|
||||||
|
2. **Lista de vulnerabilidades** encontradas (se houver)
|
||||||
|
3. **Confirmação das correcções** - validar que v1.2.2 resolve os problemas anteriores
|
||||||
|
4. **Recomendações** para próximas melhorias
|
||||||
|
5. **Priorização** (P0 crítico, P1 alto, P2 médio, P3 baixo)
|
||||||
|
|
||||||
```json
|
## Comandos Úteis
|
||||||
{
|
|
||||||
"@modelcontextprotocol/sdk": "^1.0.0",
|
```bash
|
||||||
"pg": "^8.11.0",
|
# Ver estrutura
|
||||||
"dotenv": "^16.0.0",
|
tree src/ -I node_modules
|
||||||
"uuid": "^9.0.0"
|
|
||||||
}
|
# Build
|
||||||
|
npm run build
|
||||||
|
|
||||||
|
# Verificar interpolações (deve retornar vazio)
|
||||||
|
grep -rn "INTERVAL '\${" src/tools/*.ts
|
||||||
|
grep -rn "= '\${" src/tools/*.ts
|
||||||
|
|
||||||
|
# Ver security.ts
|
||||||
|
cat src/utils/security.ts
|
||||||
|
|
||||||
|
# Ver ficheiros corrigidos
|
||||||
|
cat src/tools/analytics.ts
|
||||||
|
cat src/tools/bulk-operations.ts
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Contexto de Uso
|
*MCP Outline PostgreSQL v1.2.2 | Descomplicar® | 2026-01-31*
|
||||||
|
|
||||||
O MCP será utilizado por:
|
|
||||||
- Claude Code (Anthropic) para gestão de documentação interna
|
|
||||||
- Automações via N8N para sincronização de conteúdo
|
|
||||||
- Integrações com outros sistemas internos
|
|
||||||
|
|
||||||
**Dados expostos:** Documentos, utilizadores, colecções, comentários, permissões do Outline Wiki.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Entregáveis Esperados
|
|
||||||
|
|
||||||
1. **Relatório de Segurança**
|
|
||||||
- Vulnerabilidades encontradas (críticas, altas, médias, baixas)
|
|
||||||
- Recomendações de mitigação
|
|
||||||
- Código de exemplo para correcções
|
|
||||||
|
|
||||||
2. **Relatório de Qualidade**
|
|
||||||
- Análise estática de código
|
|
||||||
- Sugestões de melhoria
|
|
||||||
- Áreas de refactoring prioritário
|
|
||||||
|
|
||||||
3. **Relatório de Performance**
|
|
||||||
- Queries problemáticas identificadas
|
|
||||||
- Sugestões de optimização
|
|
||||||
- Benchmarks se aplicável
|
|
||||||
|
|
||||||
4. **Sumário Executivo**
|
|
||||||
- Avaliação geral do projecto
|
|
||||||
- Riscos principais
|
|
||||||
- Roadmap de correcções sugerido
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Informações de Contacto
|
|
||||||
|
|
||||||
**Solicitante:** Descomplicar®
|
|
||||||
**Email:** emanuel@descomplicar.pt
|
|
||||||
**Website:** https://descomplicar.pt
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Anexos
|
|
||||||
|
|
||||||
- `SPEC-MCP-OUTLINE.md` - Especificação técnica completa
|
|
||||||
- `CLAUDE.md` - Documentação do projecto
|
|
||||||
- `CHANGELOG.md` - Histórico de versões
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
*Documento gerado em 2026-01-31*
|
|
||||||
*MCP Outline PostgreSQL v1.2.0*
|
|
||||||
|
|||||||
24
CHANGELOG.md
24
CHANGELOG.md
@@ -2,6 +2,30 @@
|
|||||||
|
|
||||||
All notable changes to this project will be documented in this file.
|
All notable changes to this project will be documented in this file.
|
||||||
|
|
||||||
|
## [1.2.2] - 2026-01-31
|
||||||
|
|
||||||
|
### Security
|
||||||
|
|
||||||
|
- **SQL Injection Prevention:** Fixed 21 SQL injection vulnerabilities across analytics, advanced-search, and search-queries modules
|
||||||
|
- Replaced string interpolation with parameterized queries for all user inputs
|
||||||
|
- Added `validateDaysInterval()` function for safe interval validation
|
||||||
|
- Added `isValidISODate()` function for date format validation
|
||||||
|
- Added `validatePeriod()` function for period parameter validation
|
||||||
|
- All UUID validations now occur BEFORE string construction
|
||||||
|
- Using `make_interval(days => N)` for safe interval expressions
|
||||||
|
|
||||||
|
- **Transaction Support:** Added atomic operations for bulk operations
|
||||||
|
- `bulk-operations.ts`: All 6 bulk operations now use transactions
|
||||||
|
- `desk-sync.ts`: Create project doc and link task use transactions
|
||||||
|
- `export-import.ts`: Import markdown folder uses transactions
|
||||||
|
|
||||||
|
- **Rate Limiting:** Added automatic cleanup of expired entries (every 5 minutes)
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Refactored security utilities with new validation functions
|
||||||
|
- Improved error messages for invalid input parameters
|
||||||
|
|
||||||
## [1.2.1] - 2026-01-31
|
## [1.2.1] - 2026-01-31
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|||||||
168
docs/audits/2026-01-31-v1.2.1/AUDIT-REQUEST.md
Normal file
168
docs/audits/2026-01-31-v1.2.1/AUDIT-REQUEST.md
Normal file
@@ -0,0 +1,168 @@
|
|||||||
|
# Pedido de Auditoria Externa - MCP Outline PostgreSQL
|
||||||
|
|
||||||
|
## Resumo do Projecto
|
||||||
|
|
||||||
|
**Nome:** MCP Outline PostgreSQL
|
||||||
|
**Versão:** 1.2.1
|
||||||
|
**Repositório:** https://git.descomplicar.pt/ealmeida/mcp-outline-postgresql
|
||||||
|
**Tecnologia:** TypeScript, Node.js, PostgreSQL
|
||||||
|
**Protocolo:** Model Context Protocol (MCP) v1.0.0
|
||||||
|
|
||||||
|
### Descrição
|
||||||
|
|
||||||
|
Servidor MCP que fornece acesso directo à base de dados PostgreSQL do Outline Wiki. Implementa 164 tools em 33 módulos para operações CRUD, pesquisa, analytics, gestão de permissões e integração com Desk CRM.
|
||||||
|
|
||||||
|
**Arquitectura:** Claude Code → MCP Server (stdio) → PostgreSQL (Outline DB)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Âmbito da Auditoria
|
||||||
|
|
||||||
|
### 1. Segurança (Prioridade Alta)
|
||||||
|
|
||||||
|
- [ ] **SQL Injection** - Validação de queries parametrizadas em todos os 160 handlers
|
||||||
|
- [ ] **Input Validation** - Verificação de sanitização de inputs (UUIDs, strings, arrays)
|
||||||
|
- [ ] **Rate Limiting** - Eficácia da implementação actual
|
||||||
|
- [ ] **Autenticação** - Validação de acesso à base de dados
|
||||||
|
- [ ] **Exposição de Dados** - Verificar se há fuga de informação sensível nas respostas
|
||||||
|
- [ ] **Permissões** - Validar que operações respeitam modelo de permissões do Outline
|
||||||
|
|
||||||
|
### 2. Qualidade de Código (Prioridade Média)
|
||||||
|
|
||||||
|
- [ ] **TypeScript** - Type safety, uso correcto de tipos
|
||||||
|
- [ ] **Error Handling** - Tratamento de erros consistente
|
||||||
|
- [ ] **Padrões** - Consistência entre módulos
|
||||||
|
- [ ] **Code Smells** - Duplicação, complexidade ciclomática
|
||||||
|
- [ ] **Manutenibilidade** - Facilidade de extensão e manutenção
|
||||||
|
|
||||||
|
### 3. Performance (Prioridade Média)
|
||||||
|
|
||||||
|
- [ ] **Queries SQL** - Optimização, uso de índices
|
||||||
|
- [ ] **Connection Pooling** - Configuração adequada
|
||||||
|
- [ ] **Memory Leaks** - Potenciais fugas de memória
|
||||||
|
- [ ] **Pagination** - Implementação eficiente em listagens
|
||||||
|
|
||||||
|
### 4. Compatibilidade (Prioridade Baixa)
|
||||||
|
|
||||||
|
- [ ] **Schema Outline** - Compatibilidade com Outline v0.78+
|
||||||
|
- [ ] **MCP Protocol** - Conformidade com especificação MCP
|
||||||
|
- [ ] **Node.js** - Compatibilidade com versões LTS
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Ficheiros Críticos para Revisão
|
||||||
|
|
||||||
|
| Ficheiro | Descrição | Prioridade |
|
||||||
|
|----------|-----------|------------|
|
||||||
|
| `src/utils/security.ts` | Funções de segurança e validação | **Alta** |
|
||||||
|
| `src/pg-client.ts` | Cliente PostgreSQL e pooling | **Alta** |
|
||||||
|
| `src/tools/documents.ts` | 19 tools - maior módulo | **Alta** |
|
||||||
|
| `src/tools/users.ts` | Gestão de utilizadores | **Alta** |
|
||||||
|
| `src/tools/bulk-operations.ts` | Operações em lote | **Alta** |
|
||||||
|
| `src/tools/advanced-search.ts` | Pesquisa full-text | Média |
|
||||||
|
| `src/tools/analytics.ts` | Queries analíticas | Média |
|
||||||
|
| `src/tools/export-import.ts` | Export/Import Markdown | Média |
|
||||||
|
| `src/tools/desk-sync.ts` | Integração Desk CRM | Média |
|
||||||
|
| `src/index.ts` | Entry point MCP | Média |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Métricas do Projecto
|
||||||
|
|
||||||
|
| Métrica | Valor |
|
||||||
|
|---------|-------|
|
||||||
|
| Total de Tools | 164 |
|
||||||
|
| Módulos | 33 |
|
||||||
|
| Linhas de Código (estimado) | ~6500 |
|
||||||
|
| Ficheiros TypeScript | 37 |
|
||||||
|
| Dependências Runtime | 4 |
|
||||||
|
|
||||||
|
### Dependências
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"@modelcontextprotocol/sdk": "^1.0.0",
|
||||||
|
"pg": "^8.11.0",
|
||||||
|
"dotenv": "^16.0.0",
|
||||||
|
"uuid": "^9.0.0"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Contexto de Uso
|
||||||
|
|
||||||
|
O MCP será utilizado por:
|
||||||
|
- Claude Code (Anthropic) para gestão de documentação interna
|
||||||
|
- Automações via N8N para sincronização de conteúdo
|
||||||
|
- Integrações com outros sistemas internos
|
||||||
|
|
||||||
|
**Dados expostos:** Documentos, utilizadores, colecções, comentários, permissões do Outline Wiki.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Entregáveis Esperados
|
||||||
|
|
||||||
|
1. **Relatório de Segurança**
|
||||||
|
- Vulnerabilidades encontradas (críticas, altas, médias, baixas)
|
||||||
|
- Recomendações de mitigação
|
||||||
|
- Código de exemplo para correcções
|
||||||
|
|
||||||
|
2. **Relatório de Qualidade**
|
||||||
|
- Análise estática de código
|
||||||
|
- Sugestões de melhoria
|
||||||
|
- Áreas de refactoring prioritário
|
||||||
|
|
||||||
|
3. **Relatório de Performance**
|
||||||
|
- Queries problemáticas identificadas
|
||||||
|
- Sugestões de optimização
|
||||||
|
- Benchmarks se aplicável
|
||||||
|
|
||||||
|
4. **Sumário Executivo**
|
||||||
|
- Avaliação geral do projecto
|
||||||
|
- Riscos principais
|
||||||
|
- Roadmap de correcções sugerido
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Informações de Contacto
|
||||||
|
|
||||||
|
**Solicitante:** Descomplicar®
|
||||||
|
**Email:** emanuel@descomplicar.pt
|
||||||
|
**Website:** https://descomplicar.pt
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Anexos
|
||||||
|
|
||||||
|
- `SPEC-MCP-OUTLINE.md` - Especificação técnica completa
|
||||||
|
- `CLAUDE.md` - Documentação do projecto
|
||||||
|
- `CHANGELOG.md` - Histórico de versões
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ✅ Auditoria Concluída
|
||||||
|
|
||||||
|
**Data de Conclusão:** 2026-01-31
|
||||||
|
**Auditor:** Antigravity AI (Descomplicar®)
|
||||||
|
**Score Geral:** 7.2/10 (BOM)
|
||||||
|
|
||||||
|
### Documentos Criados
|
||||||
|
|
||||||
|
1. **[SUMARIO-AUDITORIA.md](file:///home/ealmeida/mcp-servers/mcp-outline-postgresql/SUMARIO-AUDITORIA.md)** - Sumário executivo
|
||||||
|
2. **[AUDITORIA-COMPLETA.md](file:///home/ealmeida/mcp-servers/mcp-outline-postgresql/AUDITORIA-COMPLETA.md)** - Análise detalhada
|
||||||
|
3. **[PLANO-MELHORIAS.md](file:///home/ealmeida/mcp-servers/mcp-outline-postgresql/PLANO-MELHORIAS.md)** - Plano de implementação
|
||||||
|
4. **[ROADMAP.md](file:///home/ealmeida/mcp-servers/mcp-outline-postgresql/ROADMAP.md)** - Checklist de tarefas
|
||||||
|
|
||||||
|
### Veredicto
|
||||||
|
|
||||||
|
**APROVADO PARA PRODUÇÃO COM RESERVAS**
|
||||||
|
|
||||||
|
Vulnerabilidades críticas identificadas que requerem correcção antes de deployment em produção:
|
||||||
|
- 🔴 SQL Injection (164 tools)
|
||||||
|
- 🔴 Ausência de Transacções (16 tools)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
*Documento gerado em 2026-01-31*
|
||||||
|
*MCP Outline PostgreSQL v1.2.1*
|
||||||
643
docs/audits/2026-01-31-v1.2.1/AUDITORIA-COMPLETA.md
Normal file
643
docs/audits/2026-01-31-v1.2.1/AUDITORIA-COMPLETA.md
Normal file
@@ -0,0 +1,643 @@
|
|||||||
|
# Auditoria Completa - MCP Outline PostgreSQL v1.2.1
|
||||||
|
|
||||||
|
**Data:** 2026-01-31
|
||||||
|
**Auditor:** Antigravity AI (Descomplicar®)
|
||||||
|
**Projecto:** MCP Outline PostgreSQL
|
||||||
|
**Versão:** 1.2.1
|
||||||
|
**Repositório:** https://git.descomplicar.pt/ealmeida/mcp-outline-postgresql
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📊 Sumário Executivo
|
||||||
|
|
||||||
|
### Avaliação Geral: **7.2/10** (BOM)
|
||||||
|
|
||||||
|
| Categoria | Score | Estado |
|
||||||
|
|-----------|-------|--------|
|
||||||
|
| **Segurança** | 7/10 | ⚠️ Requer Atenção |
|
||||||
|
| **Qualidade de Código** | 8/10 | ✅ Bom |
|
||||||
|
| **Performance** | 6/10 | ⚠️ Requer Optimização |
|
||||||
|
| **Manutenibilidade** | 8/10 | ✅ Bom |
|
||||||
|
| **Compatibilidade** | 9/10 | ✅ Excelente |
|
||||||
|
|
||||||
|
### Veredicto
|
||||||
|
|
||||||
|
O projecto está **APROVADO PARA PRODUÇÃO COM RESERVAS**. O código demonstra boa qualidade geral, arquitectura sólida e padrões consistentes. No entanto, existem **vulnerabilidades de segurança críticas** e **problemas de performance** que devem ser corrigidos antes de uso em produção com dados sensíveis.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔴 Vulnerabilidades Críticas Identificadas
|
||||||
|
|
||||||
|
### 1. **SQL Injection via String Concatenation** (CRÍTICO)
|
||||||
|
|
||||||
|
**Localização:** Múltiplos ficheiros em `src/tools/`
|
||||||
|
|
||||||
|
**Problema:** Uso de template strings para construir queries SQL dinâmicas sem parametrização adequada.
|
||||||
|
|
||||||
|
**Exemplo em `documents.ts:450-513`:**
|
||||||
|
```typescript
|
||||||
|
// VULNERÁVEL
|
||||||
|
const query = `
|
||||||
|
SELECT * FROM documents
|
||||||
|
WHERE title ILIKE '%${args.query}%' // ❌ INJECÇÃO DIRECTA
|
||||||
|
`;
|
||||||
|
```
|
||||||
|
|
||||||
|
**Impacto:** Permite execução de SQL arbitrário, acesso não autorizado a dados, modificação ou eliminação de dados.
|
||||||
|
|
||||||
|
**Mitigação:**
|
||||||
|
```typescript
|
||||||
|
// CORRECTO
|
||||||
|
const query = `
|
||||||
|
SELECT * FROM documents
|
||||||
|
WHERE title ILIKE $1
|
||||||
|
`;
|
||||||
|
const result = await pool.query(query, [`%${sanitizeInput(args.query)}%`]);
|
||||||
|
```
|
||||||
|
|
||||||
|
**Ficheiros Afectados:**
|
||||||
|
- `documents.ts` (19 tools)
|
||||||
|
- `collections.ts` (14 tools)
|
||||||
|
- `users.ts` (9 tools)
|
||||||
|
- `advanced-search.ts` (6 tools)
|
||||||
|
- `analytics.ts` (6 tools)
|
||||||
|
|
||||||
|
**Prioridade:** 🔴 **CRÍTICA** - Corrigir IMEDIATAMENTE
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 2. **Ausência de Transacções em Operações Críticas** (ALTA)
|
||||||
|
|
||||||
|
**Localização:** `bulk-operations.ts`, `desk-sync.ts`, `export-import.ts`
|
||||||
|
|
||||||
|
**Problema:** Operações que envolvem múltiplas escritas não estão envoltas em transacções.
|
||||||
|
|
||||||
|
**Exemplo em `bulk-operations.ts:24-48`:**
|
||||||
|
```typescript
|
||||||
|
// VULNERÁVEL - Sem transacção
|
||||||
|
for (const id of document_ids) {
|
||||||
|
await pool.query('UPDATE documents SET archivedAt = NOW() WHERE id = $1', [id]);
|
||||||
|
// Se falhar aqui, alguns docs ficam arquivados, outros não
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Impacto:** Inconsistência de dados, registos órfãos, estados parciais em caso de erro.
|
||||||
|
|
||||||
|
**Mitigação:**
|
||||||
|
```typescript
|
||||||
|
// CORRECTO
|
||||||
|
const client = await pool.connect();
|
||||||
|
try {
|
||||||
|
await client.query('BEGIN');
|
||||||
|
for (const id of document_ids) {
|
||||||
|
await client.query('UPDATE documents SET archivedAt = NOW() WHERE id = $1', [id]);
|
||||||
|
}
|
||||||
|
await client.query('COMMIT');
|
||||||
|
} catch (error) {
|
||||||
|
await client.query('ROLLBACK');
|
||||||
|
throw error;
|
||||||
|
} finally {
|
||||||
|
client.release();
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Ficheiros Afectados:**
|
||||||
|
- `bulk-operations.ts` (6 tools)
|
||||||
|
- `desk-sync.ts` (2 tools)
|
||||||
|
- `export-import.ts` (2 tools)
|
||||||
|
- `collections.ts` (operações de memberships)
|
||||||
|
|
||||||
|
**Prioridade:** 🔴 **ALTA** - Corrigir antes de produção
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 3. **Rate Limiting Ineficaz** (MÉDIA)
|
||||||
|
|
||||||
|
**Localização:** `src/utils/security.ts:16-32`
|
||||||
|
|
||||||
|
**Problema:** Rate limiting baseado em memória local (Map) que é resetado a cada restart do servidor.
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const rateLimitStore: Map<string, { count: number; resetAt: number }> = new Map();
|
||||||
|
```
|
||||||
|
|
||||||
|
**Impacto:**
|
||||||
|
- Não funciona em ambientes multi-instância
|
||||||
|
- Perde estado em restart
|
||||||
|
- Não protege contra ataques distribuídos
|
||||||
|
|
||||||
|
**Mitigação:**
|
||||||
|
- Usar Redis para rate limiting distribuído
|
||||||
|
- Implementar rate limiting ao nível do reverse proxy (Nginx)
|
||||||
|
- Adicionar CAPTCHA para operações sensíveis
|
||||||
|
|
||||||
|
**Prioridade:** 🟡 **MÉDIA** - Melhorar para produção escalável
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 4. **Exposição de Informação Sensível em Logs** (MÉDIA)
|
||||||
|
|
||||||
|
**Localização:** `src/pg-client.ts:78-82`
|
||||||
|
|
||||||
|
**Problema:** Logs podem expor queries com dados sensíveis.
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
logger.debug('Query executed', {
|
||||||
|
sql: sql.substring(0, 100), // Pode conter passwords, tokens
|
||||||
|
duration,
|
||||||
|
rowCount: result.rowCount
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
**Impacto:** Exposição de credenciais, tokens, dados pessoais em logs.
|
||||||
|
|
||||||
|
**Mitigação:**
|
||||||
|
- Sanitizar queries antes de logar
|
||||||
|
- Usar níveis de log apropriados (debug apenas em dev)
|
||||||
|
- Implementar log masking para campos sensíveis
|
||||||
|
|
||||||
|
**Prioridade:** 🟡 **MÉDIA** - Corrigir antes de produção
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ⚠️ Problemas de Performance
|
||||||
|
|
||||||
|
### 1. **N+1 Queries em Listagens** (ALTA)
|
||||||
|
|
||||||
|
**Localização:** `collections.ts:1253-1280`, `documents.ts:530-577`
|
||||||
|
|
||||||
|
**Problema:** Queries dentro de loops causam N+1 queries.
|
||||||
|
|
||||||
|
**Exemplo:**
|
||||||
|
```typescript
|
||||||
|
const collections = await pool.query('SELECT * FROM collections');
|
||||||
|
for (const collection of collections.rows) {
|
||||||
|
// ❌ N+1 - Uma query por collection
|
||||||
|
const docs = await pool.query('SELECT * FROM documents WHERE collectionId = $1', [collection.id]);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Impacto:** Performance degradada com grandes volumes de dados.
|
||||||
|
|
||||||
|
**Mitigação:**
|
||||||
|
```typescript
|
||||||
|
// Usar JOINs ou queries batch
|
||||||
|
const result = await pool.query(`
|
||||||
|
SELECT c.*, json_agg(d.*) as documents
|
||||||
|
FROM collections c
|
||||||
|
LEFT JOIN documents d ON d.collectionId = c.id
|
||||||
|
GROUP BY c.id
|
||||||
|
`);
|
||||||
|
```
|
||||||
|
|
||||||
|
**Prioridade:** 🟡 **MÉDIA-ALTA** - Optimizar para produção
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 2. **Ausência de Índices Documentados** (MÉDIA)
|
||||||
|
|
||||||
|
**Problema:** Não há documentação sobre índices necessários na base de dados.
|
||||||
|
|
||||||
|
**Impacto:** Queries lentas em tabelas grandes.
|
||||||
|
|
||||||
|
**Mitigação:**
|
||||||
|
- Criar ficheiro `migrations/indexes.sql` com índices recomendados
|
||||||
|
- Documentar índices necessários em `SPEC-MCP-OUTLINE.md`
|
||||||
|
|
||||||
|
**Índices Críticos:**
|
||||||
|
```sql
|
||||||
|
-- Full-text search
|
||||||
|
CREATE INDEX idx_documents_search ON documents USING gin(to_tsvector('english', title || ' ' || text));
|
||||||
|
|
||||||
|
-- Queries comuns
|
||||||
|
CREATE INDEX idx_documents_collection_id ON documents(collectionId) WHERE deletedAt IS NULL;
|
||||||
|
CREATE INDEX idx_documents_published ON documents(publishedAt) WHERE deletedAt IS NULL;
|
||||||
|
CREATE INDEX idx_collection_memberships_lookup ON collection_memberships(collectionId, userId);
|
||||||
|
```
|
||||||
|
|
||||||
|
**Prioridade:** 🟡 **MÉDIA** - Implementar antes de produção
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 3. **Connection Pool Não Configurado** (MÉDIA)
|
||||||
|
|
||||||
|
**Localização:** `src/pg-client.ts:14-32`
|
||||||
|
|
||||||
|
**Problema:** Pool usa configurações default sem tuning.
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
max: config.max, // Não há valor default definido
|
||||||
|
```
|
||||||
|
|
||||||
|
**Mitigação:**
|
||||||
|
```typescript
|
||||||
|
const poolConfig: PoolConfig = {
|
||||||
|
max: config.max || 20, // Default razoável
|
||||||
|
min: 5, // Manter conexões mínimas
|
||||||
|
idleTimeoutMillis: config.idleTimeoutMillis || 30000,
|
||||||
|
connectionTimeoutMillis: config.connectionTimeoutMillis || 5000,
|
||||||
|
maxUses: 7500, // Reciclar conexões
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
**Prioridade:** 🟢 **BAIXA** - Optimização
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ✅ Pontos Fortes
|
||||||
|
|
||||||
|
### 1. **Arquitectura Sólida**
|
||||||
|
- Separação clara de responsabilidades (tools, utils, config)
|
||||||
|
- Padrões consistentes entre módulos
|
||||||
|
- TypeScript bem utilizado
|
||||||
|
|
||||||
|
### 2. **Boa Cobertura Funcional**
|
||||||
|
- 164 tools cobrindo todas as áreas do Outline
|
||||||
|
- Documentação inline clara
|
||||||
|
- Schemas de input bem definidos
|
||||||
|
|
||||||
|
### 3. **Segurança Básica Implementada**
|
||||||
|
- Validação de UUIDs
|
||||||
|
- Sanitização de inputs (parcial)
|
||||||
|
- Soft deletes implementados
|
||||||
|
|
||||||
|
### 4. **Manutenibilidade**
|
||||||
|
- Código legível e bem estruturado
|
||||||
|
- Convenções de naming consistentes
|
||||||
|
- Fácil de estender
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📋 Análise de Qualidade de Código
|
||||||
|
|
||||||
|
### Métricas
|
||||||
|
|
||||||
|
| Métrica | Valor | Avaliação |
|
||||||
|
|---------|-------|-----------|
|
||||||
|
| Total de Linhas | ~6500 | ✅ Razoável |
|
||||||
|
| Ficheiros TypeScript | 37 | ✅ Bem organizado |
|
||||||
|
| Complexidade Ciclomática (média) | ~8 | ✅ Aceitável |
|
||||||
|
| Duplicação de Código | ~15% | ⚠️ Moderada |
|
||||||
|
| Type Safety | 95% | ✅ Excelente |
|
||||||
|
|
||||||
|
### Code Smells Identificados
|
||||||
|
|
||||||
|
#### 1. **Duplicação de Padrões** (BAIXA)
|
||||||
|
|
||||||
|
**Localização:** Todos os ficheiros em `src/tools/`
|
||||||
|
|
||||||
|
**Problema:** Padrão repetido em todos os handlers:
|
||||||
|
```typescript
|
||||||
|
// Repetido 164 vezes
|
||||||
|
handler: async (args, pgClient) => {
|
||||||
|
try {
|
||||||
|
const pool = pgClient.getPool();
|
||||||
|
// ... lógica
|
||||||
|
return {
|
||||||
|
content: [{
|
||||||
|
type: 'text',
|
||||||
|
text: JSON.stringify(result, null, 2)
|
||||||
|
}]
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
// ... tratamento de erro
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Mitigação:** Criar função helper:
|
||||||
|
```typescript
|
||||||
|
function createToolHandler<T>(
|
||||||
|
handler: (args: T, pool: Pool) => Promise<any>
|
||||||
|
): ToolHandler<T> {
|
||||||
|
return async (args, pgClient) => {
|
||||||
|
try {
|
||||||
|
const pool = pgClient.getPool();
|
||||||
|
const result = await handler(args, pool);
|
||||||
|
return formatToolResponse(result);
|
||||||
|
} catch (error) {
|
||||||
|
return formatToolError(error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Prioridade:** 🟢 **BAIXA** - Refactoring futuro
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### 2. **Validação Inconsistente** (MÉDIA)
|
||||||
|
|
||||||
|
**Problema:** Alguns tools validam inputs, outros não.
|
||||||
|
|
||||||
|
**Exemplo:**
|
||||||
|
```typescript
|
||||||
|
// documents.ts - Valida UUID
|
||||||
|
if (!isValidUUID(args.id)) {
|
||||||
|
throw new Error('Invalid UUID');
|
||||||
|
}
|
||||||
|
|
||||||
|
// collections.ts - Não valida
|
||||||
|
const result = await pool.query('SELECT * FROM collections WHERE id = $1', [args.id]);
|
||||||
|
```
|
||||||
|
|
||||||
|
**Mitigação:** Criar middleware de validação automática baseado em `inputSchema`.
|
||||||
|
|
||||||
|
**Prioridade:** 🟡 **MÉDIA** - Melhorar consistência
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔒 Análise de Segurança Detalhada
|
||||||
|
|
||||||
|
### Matriz de Risco
|
||||||
|
|
||||||
|
| Vulnerabilidade | Severidade | Probabilidade | Risco | Prioridade |
|
||||||
|
|----------------|------------|---------------|-------|------------|
|
||||||
|
| SQL Injection | CRÍTICA | ALTA | 🔴 CRÍTICO | P0 |
|
||||||
|
| Ausência de Transacções | ALTA | MÉDIA | 🟠 ALTO | P1 |
|
||||||
|
| Rate Limiting Ineficaz | MÉDIA | ALTA | 🟡 MÉDIO | P2 |
|
||||||
|
| Exposição de Logs | MÉDIA | BAIXA | 🟡 MÉDIO | P2 |
|
||||||
|
| Falta de Autenticação | BAIXA | BAIXA | 🟢 BAIXO | P3 |
|
||||||
|
|
||||||
|
### Recomendações de Segurança
|
||||||
|
|
||||||
|
#### 1. **Implementar Prepared Statements Universalmente**
|
||||||
|
- Auditar todos os 164 handlers
|
||||||
|
- Garantir 100% de queries parametrizadas
|
||||||
|
- Adicionar linting rule para detectar string concatenation em queries
|
||||||
|
|
||||||
|
#### 2. **Adicionar Camada de Autorização**
|
||||||
|
```typescript
|
||||||
|
// Verificar permissões antes de executar operações
|
||||||
|
async function checkPermission(userId: string, resourceId: string, action: string): Promise<boolean> {
|
||||||
|
// Verificar se user tem permissão para action em resource
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 3. **Implementar Audit Log**
|
||||||
|
- Registar todas as operações de escrita
|
||||||
|
- Incluir userId, timestamp, operação, recurso afectado
|
||||||
|
- Usar tabela `events` do Outline
|
||||||
|
|
||||||
|
#### 4. **Adicionar Input Validation Schema**
|
||||||
|
```typescript
|
||||||
|
import Ajv from 'ajv';
|
||||||
|
|
||||||
|
const ajv = new Ajv();
|
||||||
|
const validate = ajv.compile(tool.inputSchema);
|
||||||
|
|
||||||
|
if (!validate(args)) {
|
||||||
|
throw new Error(`Invalid input: ${ajv.errorsText(validate.errors)}`);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🚀 Performance - Benchmarks e Optimizações
|
||||||
|
|
||||||
|
### Queries Problemáticas Identificadas
|
||||||
|
|
||||||
|
#### 1. **Full-text Search sem Índice**
|
||||||
|
```sql
|
||||||
|
-- LENTO (sem índice tsvector)
|
||||||
|
SELECT * FROM documents
|
||||||
|
WHERE title ILIKE '%query%' OR text ILIKE '%query%';
|
||||||
|
|
||||||
|
-- RÁPIDO (com índice GIN)
|
||||||
|
SELECT * FROM documents
|
||||||
|
WHERE to_tsvector('english', title || ' ' || text) @@ plainto_tsquery('english', 'query');
|
||||||
|
```
|
||||||
|
|
||||||
|
**Ganho Estimado:** 10-100x em tabelas com >10k documentos
|
||||||
|
|
||||||
|
#### 2. **Paginação Ineficiente**
|
||||||
|
```sql
|
||||||
|
-- LENTO (OFFSET alto)
|
||||||
|
SELECT * FROM documents ORDER BY createdAt DESC LIMIT 25 OFFSET 10000;
|
||||||
|
|
||||||
|
-- RÁPIDO (cursor-based pagination)
|
||||||
|
SELECT * FROM documents
|
||||||
|
WHERE createdAt < $1
|
||||||
|
ORDER BY createdAt DESC
|
||||||
|
LIMIT 25;
|
||||||
|
```
|
||||||
|
|
||||||
|
**Ganho Estimado:** 5-20x para páginas profundas
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📦 Compatibilidade
|
||||||
|
|
||||||
|
### ✅ Outline Schema Compatibility
|
||||||
|
|
||||||
|
**Versão Testada:** Outline v0.78+
|
||||||
|
**Compatibilidade:** 95%
|
||||||
|
|
||||||
|
**Tabelas Suportadas:**
|
||||||
|
- ✅ documents, collections, users, groups
|
||||||
|
- ✅ comments, shares, revisions, events
|
||||||
|
- ✅ attachments, file_operations, oauth_clients
|
||||||
|
- ✅ stars, pins, views, reactions
|
||||||
|
- ✅ api_keys, webhooks, integrations
|
||||||
|
- ✅ notifications, subscriptions, templates
|
||||||
|
|
||||||
|
**Limitações Conhecidas:**
|
||||||
|
- ⚠️ Backlinks é view read-only (não é tabela)
|
||||||
|
- ⚠️ Algumas colunas podem variar entre versões do Outline
|
||||||
|
|
||||||
|
### ✅ MCP Protocol Compliance
|
||||||
|
|
||||||
|
**Versão:** MCP SDK v1.0.0
|
||||||
|
**Conformidade:** 100%
|
||||||
|
|
||||||
|
- ✅ Tool registration correcto
|
||||||
|
- ✅ Input schemas válidos
|
||||||
|
- ✅ Response format correcto
|
||||||
|
- ✅ Error handling adequado
|
||||||
|
|
||||||
|
### ✅ Node.js Compatibility
|
||||||
|
|
||||||
|
**Versões Suportadas:** Node.js 18+ LTS
|
||||||
|
**Dependências:**
|
||||||
|
- `@modelcontextprotocol/sdk`: ^1.0.0 ✅
|
||||||
|
- `pg`: ^8.11.0 ✅
|
||||||
|
- `dotenv`: ^16.0.0 ✅
|
||||||
|
- `uuid`: ^9.0.0 ✅
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📝 Relatórios Detalhados
|
||||||
|
|
||||||
|
### 1. Relatório de Segurança
|
||||||
|
|
||||||
|
#### Vulnerabilidades Críticas: 1
|
||||||
|
- **SQL Injection via String Concatenation** - 164 tools afectadas
|
||||||
|
|
||||||
|
#### Vulnerabilidades Altas: 1
|
||||||
|
- **Ausência de Transacções** - 10 tools afectadas
|
||||||
|
|
||||||
|
#### Vulnerabilidades Médias: 2
|
||||||
|
- **Rate Limiting Ineficaz**
|
||||||
|
- **Exposição de Logs**
|
||||||
|
|
||||||
|
#### Vulnerabilidades Baixas: 0
|
||||||
|
|
||||||
|
**Total:** 4 vulnerabilidades identificadas
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 2. Relatório de Qualidade
|
||||||
|
|
||||||
|
#### Padrões de Código: ✅ BOM
|
||||||
|
- Naming conventions consistentes
|
||||||
|
- TypeScript bem utilizado
|
||||||
|
- Estrutura modular clara
|
||||||
|
|
||||||
|
#### Manutenibilidade: ✅ BOM
|
||||||
|
- Código legível
|
||||||
|
- Documentação inline adequada
|
||||||
|
- Fácil de estender
|
||||||
|
|
||||||
|
#### Testabilidade: ⚠️ AUSENTE
|
||||||
|
- Sem testes unitários
|
||||||
|
- Sem testes de integração
|
||||||
|
- Sem CI/CD
|
||||||
|
|
||||||
|
**Recomendação:** Implementar testes com Jest/Vitest
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 3. Relatório de Performance
|
||||||
|
|
||||||
|
#### Queries Optimizadas: 40%
|
||||||
|
- Maioria usa queries simples eficientes
|
||||||
|
- Alguns casos de N+1 queries
|
||||||
|
|
||||||
|
#### Índices Documentados: 0%
|
||||||
|
- Sem documentação de índices necessários
|
||||||
|
- Sem migrations de schema
|
||||||
|
|
||||||
|
#### Connection Pooling: ⚠️ BÁSICO
|
||||||
|
- Pool implementado mas não tunado
|
||||||
|
- Sem configuração de limites
|
||||||
|
|
||||||
|
**Recomendação:** Criar guia de performance e índices
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🎯 Roadmap de Correcções Sugerido
|
||||||
|
|
||||||
|
### Fase 1: Segurança Crítica (1-2 semanas)
|
||||||
|
**Prioridade:** 🔴 CRÍTICA
|
||||||
|
|
||||||
|
- [ ] **Semana 1:** Corrigir SQL Injection em todos os 164 handlers
|
||||||
|
- Auditar todos os ficheiros em `src/tools/`
|
||||||
|
- Converter para queries parametrizadas
|
||||||
|
- Adicionar linting rule
|
||||||
|
- Testar manualmente cada tool
|
||||||
|
|
||||||
|
- [ ] **Semana 2:** Implementar Transacções
|
||||||
|
- Identificar operações multi-write
|
||||||
|
- Envolver em transacções
|
||||||
|
- Adicionar testes de rollback
|
||||||
|
|
||||||
|
**Entregável:** MCP seguro para produção
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Fase 2: Performance (1 semana)
|
||||||
|
**Prioridade:** 🟡 MÉDIA
|
||||||
|
|
||||||
|
- [ ] Criar ficheiro `migrations/indexes.sql`
|
||||||
|
- [ ] Documentar índices em `SPEC-MCP-OUTLINE.md`
|
||||||
|
- [ ] Optimizar N+1 queries
|
||||||
|
- [ ] Tunar connection pool
|
||||||
|
|
||||||
|
**Entregável:** MCP optimizado para produção
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Fase 3: Qualidade (2 semanas)
|
||||||
|
**Prioridade:** 🟢 BAIXA
|
||||||
|
|
||||||
|
- [ ] Implementar testes unitários (Jest)
|
||||||
|
- [ ] Adicionar testes de integração
|
||||||
|
- [ ] Configurar CI/CD
|
||||||
|
- [ ] Melhorar validação de inputs
|
||||||
|
- [ ] Refactoring de código duplicado
|
||||||
|
|
||||||
|
**Entregável:** MCP com qualidade enterprise
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Fase 4: Funcionalidades (ongoing)
|
||||||
|
**Prioridade:** 🟢 BAIXA
|
||||||
|
|
||||||
|
- [ ] Implementar audit log completo
|
||||||
|
- [ ] Adicionar camada de autorização
|
||||||
|
- [ ] Melhorar rate limiting (Redis)
|
||||||
|
- [ ] Adicionar métricas e monitoring
|
||||||
|
- [ ] Documentação de API completa
|
||||||
|
|
||||||
|
**Entregável:** MCP production-ready completo
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📊 Métricas de Sucesso
|
||||||
|
|
||||||
|
### KPIs de Segurança
|
||||||
|
- [ ] 0 vulnerabilidades críticas
|
||||||
|
- [ ] 0 vulnerabilidades altas
|
||||||
|
- [ ] 100% queries parametrizadas
|
||||||
|
- [ ] 100% operações críticas com transacções
|
||||||
|
|
||||||
|
### KPIs de Performance
|
||||||
|
- [ ] Queries < 100ms (p95)
|
||||||
|
- [ ] Throughput > 1000 req/s
|
||||||
|
- [ ] Connection pool utilization < 80%
|
||||||
|
|
||||||
|
### KPIs de Qualidade
|
||||||
|
- [ ] Code coverage > 80%
|
||||||
|
- [ ] 0 code smells críticos
|
||||||
|
- [ ] TypeScript strict mode enabled
|
||||||
|
- [ ] 0 linting errors
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔗 Anexos
|
||||||
|
|
||||||
|
### Ficheiros para Revisão Prioritária
|
||||||
|
|
||||||
|
1. **Segurança (CRÍTICO):**
|
||||||
|
- [src/utils/security.ts](file:///home/ealmeida/mcp-servers/mcp-outline-postgresql/src/utils/security.ts)
|
||||||
|
- [src/tools/documents.ts](file:///home/ealmeida/mcp-servers/mcp-outline-postgresql/src/tools/documents.ts)
|
||||||
|
- [src/tools/users.ts](file:///home/ealmeida/mcp-servers/mcp-outline-postgresql/src/tools/users.ts)
|
||||||
|
|
||||||
|
2. **Performance (ALTA):**
|
||||||
|
- [src/pg-client.ts](file:///home/ealmeida/mcp-servers/mcp-outline-postgresql/src/pg-client.ts)
|
||||||
|
- [src/tools/collections.ts](file:///home/ealmeida/mcp-servers/mcp-outline-postgresql/src/tools/collections.ts)
|
||||||
|
- [src/tools/advanced-search.ts](file:///home/ealmeida/mcp-servers/mcp-outline-postgresql/src/tools/advanced-search.ts)
|
||||||
|
|
||||||
|
3. **Transacções (ALTA):**
|
||||||
|
- [src/tools/bulk-operations.ts](file:///home/ealmeida/mcp-servers/mcp-outline-postgresql/src/tools/bulk-operations.ts)
|
||||||
|
- [src/tools/desk-sync.ts](file:///home/ealmeida/mcp-servers/mcp-outline-postgresql/src/tools/desk-sync.ts)
|
||||||
|
- [src/tools/export-import.ts](file:///home/ealmeida/mcp-servers/mcp-outline-postgresql/src/tools/export-import.ts)
|
||||||
|
|
||||||
|
### Documentação de Referência
|
||||||
|
|
||||||
|
- [SPEC-MCP-OUTLINE.md](file:///home/ealmeida/mcp-servers/mcp-outline-postgresql/SPEC-MCP-OUTLINE.md) - Especificação técnica
|
||||||
|
- [CHANGELOG.md](file:///home/ealmeida/mcp-servers/mcp-outline-postgresql/CHANGELOG.md) - Histórico de versões
|
||||||
|
- [CLAUDE.md](file:///home/ealmeida/mcp-servers/mcp-outline-postgresql/CLAUDE.md) - Documentação do projecto
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📞 Contacto
|
||||||
|
|
||||||
|
**Auditor:** Antigravity AI
|
||||||
|
**Organização:** Descomplicar®
|
||||||
|
**Email:** emanuel@descomplicar.pt
|
||||||
|
**Website:** https://descomplicar.pt
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
*Auditoria realizada em 2026-01-31 | MCP Outline PostgreSQL v1.2.1*
|
||||||
1196
docs/audits/2026-01-31-v1.2.1/PLANO-MELHORIAS.md
Normal file
1196
docs/audits/2026-01-31-v1.2.1/PLANO-MELHORIAS.md
Normal file
File diff suppressed because it is too large
Load Diff
366
docs/audits/2026-01-31-v1.2.1/ROADMAP.md
Normal file
366
docs/audits/2026-01-31-v1.2.1/ROADMAP.md
Normal file
@@ -0,0 +1,366 @@
|
|||||||
|
# Task List - MCP Outline PostgreSQL v2.0.0
|
||||||
|
|
||||||
|
**Projecto:** MCP Outline PostgreSQL
|
||||||
|
**Versão Actual:** 1.2.1
|
||||||
|
**Versão Alvo:** 2.0.0 (Production-Ready)
|
||||||
|
**Data Início:** 2026-01-31
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔴 FASE 1: Segurança Crítica (P0) - 2 semanas
|
||||||
|
|
||||||
|
### 1.1 SQL Injection (Semana 1)
|
||||||
|
|
||||||
|
- [ ] **1.1.1** Auditar queries vulneráveis
|
||||||
|
- [ ] Executar grep para identificar template strings em queries
|
||||||
|
- [ ] Catalogar todas as ocorrências em `vulnerable-queries.txt`
|
||||||
|
- [ ] Priorizar por criticidade (write > read)
|
||||||
|
|
||||||
|
- [ ] **1.1.2** Criar SafeQueryBuilder
|
||||||
|
- [ ] Implementar `src/utils/query-builder.ts`
|
||||||
|
- [ ] Adicionar métodos: `addParam()`, `buildILike()`, `buildIn()`
|
||||||
|
- [ ] Escrever testes unitários
|
||||||
|
- [ ] Documentar uso
|
||||||
|
|
||||||
|
- [ ] **1.1.3** Refactoring de queries - Módulos Core
|
||||||
|
- [ ] `documents.ts` (19 tools) - 2 dias
|
||||||
|
- [ ] `collections.ts` (14 tools) - 1.5 dias
|
||||||
|
- [ ] `users.ts` (9 tools) - 1 dia
|
||||||
|
- [ ] `groups.ts` (8 tools) - 1 dia
|
||||||
|
|
||||||
|
- [ ] **1.1.4** Refactoring de queries - Módulos Search/Analytics
|
||||||
|
- [ ] `advanced-search.ts` (6 tools) - 1 dia
|
||||||
|
- [ ] `analytics.ts` (6 tools) - 1 dia
|
||||||
|
- [ ] `search-queries.ts` (2 tools) - 0.5 dias
|
||||||
|
|
||||||
|
- [ ] **1.1.5** Refactoring de queries - Restantes módulos
|
||||||
|
- [ ] 27 ficheiros restantes - 2 dias
|
||||||
|
- [ ] Testar cada módulo após refactoring
|
||||||
|
|
||||||
|
- [ ] **1.1.6** Adicionar linting rule
|
||||||
|
- [ ] Configurar ESLint rule para detectar template strings em queries
|
||||||
|
- [ ] Executar linter e corrigir warnings
|
||||||
|
- [ ] Adicionar ao CI
|
||||||
|
|
||||||
|
### 1.2 Transacções (Semana 2)
|
||||||
|
|
||||||
|
- [ ] **1.2.1** Identificar operações críticas
|
||||||
|
- [ ] Listar todas as operações multi-write
|
||||||
|
- [ ] Priorizar por risco de inconsistência
|
||||||
|
- [ ] Documentar em `TRANSACTION-AUDIT.md`
|
||||||
|
|
||||||
|
- [ ] **1.2.2** Criar Transaction Helper
|
||||||
|
- [ ] Implementar `src/utils/transaction.ts`
|
||||||
|
- [ ] Adicionar retry logic para deadlocks
|
||||||
|
- [ ] Escrever testes unitários
|
||||||
|
|
||||||
|
- [ ] **1.2.3** Refactoring com transacções
|
||||||
|
- [ ] `bulk-operations.ts` (6 tools)
|
||||||
|
- [ ] `desk-sync.ts` (2 tools)
|
||||||
|
- [ ] `export-import.ts` (2 tools)
|
||||||
|
- [ ] `collections.ts` - memberships (4 tools)
|
||||||
|
- [ ] `documents.ts` - create/update com memberships (2 tools)
|
||||||
|
|
||||||
|
- [ ] **1.2.4** Testes de rollback
|
||||||
|
- [ ] Criar `tests/transactions.test.ts`
|
||||||
|
- [ ] Testar rollback em cada operação crítica
|
||||||
|
- [ ] Verificar integridade de dados
|
||||||
|
|
||||||
|
### 1.3 Validação de Inputs (Semana 2)
|
||||||
|
|
||||||
|
- [ ] **1.3.1** Implementar validação automática
|
||||||
|
- [ ] Instalar `ajv` e `ajv-formats`
|
||||||
|
- [ ] Criar `src/utils/validation.ts`
|
||||||
|
- [ ] Implementar `validateToolInput()` e `withValidation()`
|
||||||
|
|
||||||
|
- [ ] **1.3.2** Adicionar validações específicas
|
||||||
|
- [ ] `validateUUIDs()` para arrays
|
||||||
|
- [ ] `validateEnum()` para enums
|
||||||
|
- [ ] `validateStringLength()` para strings
|
||||||
|
- [ ] Escrever testes unitários
|
||||||
|
|
||||||
|
- [ ] **1.3.3** Aplicar validação a todos os tools
|
||||||
|
- [ ] Envolver handlers com `withValidation()`
|
||||||
|
- [ ] Testar validação em cada módulo
|
||||||
|
|
||||||
|
### 1.4 Audit Logging (Semana 2)
|
||||||
|
|
||||||
|
- [ ] **1.4.1** Criar sistema de audit log
|
||||||
|
- [ ] Implementar `src/utils/audit.ts`
|
||||||
|
- [ ] Criar `logAudit()` e `withAuditLog()`
|
||||||
|
- [ ] Integrar com tabela `events`
|
||||||
|
|
||||||
|
- [ ] **1.4.2** Aplicar audit log
|
||||||
|
- [ ] Identificar operações de escrita (create, update, delete)
|
||||||
|
- [ ] Envolver com `withAuditLog()`
|
||||||
|
- [ ] Testar logging
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🟡 FASE 2: Performance (P1) - 1 semana
|
||||||
|
|
||||||
|
### 2.1 Eliminar N+1 Queries
|
||||||
|
|
||||||
|
- [ ] **2.1.1** Identificar N+1 queries
|
||||||
|
- [ ] Auditar `collections.ts:1253-1280`
|
||||||
|
- [ ] Auditar `documents.ts:530-577`
|
||||||
|
- [ ] Auditar `analytics.ts`
|
||||||
|
|
||||||
|
- [ ] **2.1.2** Refactoring com JOINs
|
||||||
|
- [ ] `export_all_collections` - usar json_agg
|
||||||
|
- [ ] `list_drafts` - optimizar query
|
||||||
|
- [ ] Analytics queries - usar CTEs
|
||||||
|
|
||||||
|
- [ ] **2.1.3** Testar performance
|
||||||
|
- [ ] Benchmark antes/depois
|
||||||
|
- [ ] Verificar planos de execução (EXPLAIN)
|
||||||
|
|
||||||
|
### 2.2 Criar Índices
|
||||||
|
|
||||||
|
- [ ] **2.2.1** Criar migrations
|
||||||
|
- [ ] Criar `migrations/001_indexes.sql`
|
||||||
|
- [ ] Adicionar índices GIN para full-text search
|
||||||
|
- [ ] Adicionar índices B-tree para queries comuns
|
||||||
|
- [ ] Adicionar índices para memberships
|
||||||
|
|
||||||
|
- [ ] **2.2.2** Documentar índices
|
||||||
|
- [ ] Adicionar secção em `SPEC-MCP-OUTLINE.md`
|
||||||
|
- [ ] Documentar impacto de cada índice
|
||||||
|
- [ ] Criar guia de deployment
|
||||||
|
|
||||||
|
### 2.3 Optimizar Connection Pool
|
||||||
|
|
||||||
|
- [ ] **2.3.1** Tuning de pool
|
||||||
|
- [ ] Adicionar defaults em `src/config/database.ts`
|
||||||
|
- [ ] Configurar max, min, timeouts
|
||||||
|
- [ ] Adicionar maxUses para recycling
|
||||||
|
|
||||||
|
- [ ] **2.3.2** Pool monitoring
|
||||||
|
- [ ] Criar `src/utils/monitoring.ts`
|
||||||
|
- [ ] Adicionar logging de pool stats
|
||||||
|
- [ ] Adicionar alertas de saturação
|
||||||
|
|
||||||
|
### 2.4 Cursor-Based Pagination
|
||||||
|
|
||||||
|
- [ ] **2.4.1** Implementar cursor pagination
|
||||||
|
- [ ] Criar `src/utils/pagination.ts`
|
||||||
|
- [ ] Implementar `paginateWithCursor()`
|
||||||
|
- [ ] Escrever testes
|
||||||
|
|
||||||
|
- [ ] **2.4.2** Migrar tools principais
|
||||||
|
- [ ] `list_documents`
|
||||||
|
- [ ] `list_collections`
|
||||||
|
- [ ] `list_users`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🟢 FASE 3: Qualidade (P2) - 2 semanas
|
||||||
|
|
||||||
|
### 3.1 Testes Unitários (Semana 1)
|
||||||
|
|
||||||
|
- [ ] **3.1.1** Setup de testing
|
||||||
|
- [ ] Instalar Vitest + Testcontainers
|
||||||
|
- [ ] Criar `vitest.config.ts`
|
||||||
|
- [ ] Configurar coverage
|
||||||
|
|
||||||
|
- [ ] **3.1.2** Testes de utils
|
||||||
|
- [ ] `tests/utils/security.test.ts`
|
||||||
|
- [ ] `tests/utils/query-builder.test.ts`
|
||||||
|
- [ ] `tests/utils/validation.test.ts`
|
||||||
|
- [ ] `tests/utils/transaction.test.ts`
|
||||||
|
- [ ] `tests/utils/audit.test.ts`
|
||||||
|
|
||||||
|
- [ ] **3.1.3** Testes de tools
|
||||||
|
- [ ] `tests/tools/documents.test.ts`
|
||||||
|
- [ ] `tests/tools/collections.test.ts`
|
||||||
|
- [ ] `tests/tools/users.test.ts`
|
||||||
|
- [ ] `tests/tools/bulk-operations.test.ts`
|
||||||
|
|
||||||
|
- [ ] **3.1.4** Testes de integração
|
||||||
|
- [ ] Setup PostgreSQL container
|
||||||
|
- [ ] Testes end-to-end de workflows
|
||||||
|
- [ ] Testes de transacções
|
||||||
|
|
||||||
|
### 3.2 CI/CD (Semana 2)
|
||||||
|
|
||||||
|
- [ ] **3.2.1** GitHub Actions
|
||||||
|
- [ ] Criar `.github/workflows/ci.yml`
|
||||||
|
- [ ] Configurar test job
|
||||||
|
- [ ] Configurar lint job
|
||||||
|
- [ ] Configurar build job
|
||||||
|
|
||||||
|
- [ ] **3.2.2** Code coverage
|
||||||
|
- [ ] Integrar Codecov
|
||||||
|
- [ ] Configurar thresholds (>80%)
|
||||||
|
- [ ] Adicionar badge ao README
|
||||||
|
|
||||||
|
- [ ] **3.2.3** Automated releases
|
||||||
|
- [ ] Configurar semantic-release
|
||||||
|
- [ ] Automatizar CHANGELOG
|
||||||
|
- [ ] Automatizar tags
|
||||||
|
|
||||||
|
### 3.3 Refactoring (Semana 2)
|
||||||
|
|
||||||
|
- [ ] **3.3.1** Tool factory
|
||||||
|
- [ ] Criar `src/utils/tool-factory.ts`
|
||||||
|
- [ ] Implementar `createTool()`
|
||||||
|
- [ ] Adicionar validação automática
|
||||||
|
- [ ] Adicionar transacção automática
|
||||||
|
- [ ] Adicionar audit log automático
|
||||||
|
|
||||||
|
- [ ] **3.3.2** Aplicar factory
|
||||||
|
- [ ] Refactoring de `documents.ts`
|
||||||
|
- [ ] Refactoring de `collections.ts`
|
||||||
|
- [ ] Refactoring de `users.ts`
|
||||||
|
- [ ] Refactoring de restantes módulos
|
||||||
|
|
||||||
|
- [ ] **3.3.3** Type safety
|
||||||
|
- [ ] Activar TypeScript strict mode
|
||||||
|
- [ ] Corrigir type errors
|
||||||
|
- [ ] Adicionar tipos genéricos
|
||||||
|
|
||||||
|
### 3.4 Documentação
|
||||||
|
|
||||||
|
- [ ] **3.4.1** Actualizar README
|
||||||
|
- [ ] Adicionar badges (CI, coverage)
|
||||||
|
- [ ] Melhorar getting started
|
||||||
|
- [ ] Adicionar troubleshooting
|
||||||
|
|
||||||
|
- [ ] **3.4.2** API documentation
|
||||||
|
- [ ] Documentar cada tool
|
||||||
|
- [ ] Adicionar exemplos de uso
|
||||||
|
- [ ] Criar guia de best practices
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🟢 FASE 4: Funcionalidades (P3) - Ongoing
|
||||||
|
|
||||||
|
### 4.1 Rate Limiting Distribuído
|
||||||
|
|
||||||
|
- [ ] **4.1.1** Integrar Redis
|
||||||
|
- [ ] Adicionar dependência `ioredis`
|
||||||
|
- [ ] Configurar Redis client
|
||||||
|
- [ ] Criar `src/utils/redis-rate-limit.ts`
|
||||||
|
|
||||||
|
- [ ] **4.1.2** Implementar rate limiting
|
||||||
|
- [ ] Substituir Map por Redis
|
||||||
|
- [ ] Adicionar sliding window
|
||||||
|
- [ ] Testar em multi-instância
|
||||||
|
|
||||||
|
- [ ] **4.1.3** CAPTCHA
|
||||||
|
- [ ] Integrar reCAPTCHA
|
||||||
|
- [ ] Adicionar a operações sensíveis
|
||||||
|
- [ ] Testar bypass em testes
|
||||||
|
|
||||||
|
### 4.2 Autorização
|
||||||
|
|
||||||
|
- [ ] **4.2.1** Implementar RBAC
|
||||||
|
- [ ] Criar `src/utils/authorization.ts`
|
||||||
|
- [ ] Implementar `checkPermission()`
|
||||||
|
- [ ] Definir roles e permissions
|
||||||
|
|
||||||
|
- [ ] **4.2.2** Aplicar autorização
|
||||||
|
- [ ] Adicionar middleware de autorização
|
||||||
|
- [ ] Verificar permissões antes de operações
|
||||||
|
- [ ] Testar cenários de acesso negado
|
||||||
|
|
||||||
|
- [ ] **4.2.3** Testes de autorização
|
||||||
|
- [ ] Testes de RBAC
|
||||||
|
- [ ] Testes de permission checks
|
||||||
|
- [ ] Testes de edge cases
|
||||||
|
|
||||||
|
### 4.3 Monitoring
|
||||||
|
|
||||||
|
- [ ] **4.3.1** Prometheus
|
||||||
|
- [ ] Adicionar `prom-client`
|
||||||
|
- [ ] Criar métricas (query duration, pool stats, etc)
|
||||||
|
- [ ] Expor endpoint `/metrics`
|
||||||
|
|
||||||
|
- [ ] **4.3.2** Grafana
|
||||||
|
- [ ] Criar dashboard
|
||||||
|
- [ ] Adicionar alertas
|
||||||
|
- [ ] Documentar setup
|
||||||
|
|
||||||
|
- [ ] **4.3.3** Logging estruturado
|
||||||
|
- [ ] Migrar para Winston ou Pino
|
||||||
|
- [ ] Adicionar correlation IDs
|
||||||
|
- [ ] Configurar log levels por ambiente
|
||||||
|
|
||||||
|
### 4.4 Documentação Avançada
|
||||||
|
|
||||||
|
- [ ] **4.4.1** OpenAPI spec
|
||||||
|
- [ ] Gerar OpenAPI 3.0 spec
|
||||||
|
- [ ] Adicionar Swagger UI
|
||||||
|
- [ ] Publicar documentação
|
||||||
|
|
||||||
|
- [ ] **4.4.2** Deployment guide
|
||||||
|
- [ ] Docker Compose setup
|
||||||
|
- [ ] Kubernetes manifests
|
||||||
|
- [ ] Production checklist
|
||||||
|
|
||||||
|
- [ ] **4.4.3** Troubleshooting guide
|
||||||
|
- [ ] Common errors
|
||||||
|
- [ ] Performance tuning
|
||||||
|
- [ ] Debug tips
|
||||||
|
|
||||||
|
### 4.5 Melhorias Incrementais
|
||||||
|
|
||||||
|
- [ ] **4.5.1** Caching
|
||||||
|
- [ ] Implementar cache de queries frequentes
|
||||||
|
- [ ] Usar Redis para cache distribuído
|
||||||
|
- [ ] Adicionar cache invalidation
|
||||||
|
|
||||||
|
- [ ] **4.5.2** Webhooks
|
||||||
|
- [ ] Implementar webhook dispatcher
|
||||||
|
- [ ] Adicionar retry logic
|
||||||
|
- [ ] Testar delivery
|
||||||
|
|
||||||
|
- [ ] **4.5.3** Bulk import/export
|
||||||
|
- [ ] Optimizar import de grandes volumes
|
||||||
|
- [ ] Adicionar progress tracking
|
||||||
|
- [ ] Implementar streaming
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📊 Progress Tracking
|
||||||
|
|
||||||
|
### Fase 1: Segurança Crítica
|
||||||
|
- **Total:** 12 tarefas
|
||||||
|
- **Concluídas:** 0
|
||||||
|
- **Em Progresso:** 0
|
||||||
|
- **Bloqueadas:** 0
|
||||||
|
- **Progress:** 0%
|
||||||
|
|
||||||
|
### Fase 2: Performance
|
||||||
|
- **Total:** 10 tarefas
|
||||||
|
- **Concluídas:** 0
|
||||||
|
- **Em Progresso:** 0
|
||||||
|
- **Bloqueadas:** 0
|
||||||
|
- **Progress:** 0%
|
||||||
|
|
||||||
|
### Fase 3: Qualidade
|
||||||
|
- **Total:** 15 tarefas
|
||||||
|
- **Concluídas:** 0
|
||||||
|
- **Em Progresso:** 0
|
||||||
|
- **Bloqueadas:** 0
|
||||||
|
- **Progress:** 0%
|
||||||
|
|
||||||
|
### Fase 4: Funcionalidades
|
||||||
|
- **Total:** 15 tarefas
|
||||||
|
- **Concluídas:** 0
|
||||||
|
- **Em Progresso:** 0
|
||||||
|
- **Bloqueadas:** 0
|
||||||
|
- **Progress:** 0%
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🎯 Próximos Passos Imediatos
|
||||||
|
|
||||||
|
1. [ ] Aprovar plano de melhorias
|
||||||
|
2. [ ] Criar branch `security-fixes`
|
||||||
|
3. [ ] Iniciar tarefa 1.1.1: Auditar queries vulneráveis
|
||||||
|
4. [ ] Daily standup: actualizar progress
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
*Task list criada em 2026-01-31 | MCP Outline PostgreSQL v1.2.1 → v2.0.0*
|
||||||
154
docs/audits/2026-01-31-v1.2.1/SUMARIO-AUDITORIA.md
Normal file
154
docs/audits/2026-01-31-v1.2.1/SUMARIO-AUDITORIA.md
Normal file
@@ -0,0 +1,154 @@
|
|||||||
|
# Sumário Executivo - Auditoria MCP Outline PostgreSQL
|
||||||
|
|
||||||
|
**Data:** 2026-01-31
|
||||||
|
**Versão:** 1.2.1
|
||||||
|
**Auditor:** Antigravity AI (Descomplicar®)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📊 Avaliação Geral: **7.2/10** (BOM)
|
||||||
|
|
||||||
|
| Categoria | Score | Estado |
|
||||||
|
|-----------|-------|--------|
|
||||||
|
| Segurança | 7/10 | ⚠️ Requer Atenção |
|
||||||
|
| Qualidade | 8/10 | ✅ Bom |
|
||||||
|
| Performance | 6/10 | ⚠️ Requer Optimização |
|
||||||
|
| Manutenibilidade | 8/10 | ✅ Bom |
|
||||||
|
| Compatibilidade | 9/10 | ✅ Excelente |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🎯 Veredicto
|
||||||
|
|
||||||
|
**APROVADO PARA PRODUÇÃO COM RESERVAS**
|
||||||
|
|
||||||
|
O projecto demonstra boa qualidade geral, arquitectura sólida e padrões consistentes. No entanto, existem **vulnerabilidades de segurança críticas** que devem ser corrigidas antes de uso em produção com dados sensíveis.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔴 Vulnerabilidades Críticas
|
||||||
|
|
||||||
|
### 1. SQL Injection (CRÍTICO)
|
||||||
|
- **Afectadas:** 164 tools
|
||||||
|
- **Problema:** String concatenation em queries SQL
|
||||||
|
- **Impacto:** Execução de SQL arbitrário, acesso não autorizado
|
||||||
|
- **Prioridade:** P0 - Corrigir IMEDIATAMENTE
|
||||||
|
|
||||||
|
### 2. Ausência de Transacções (ALTA)
|
||||||
|
- **Afectadas:** 16 tools (bulk operations, desk-sync, export-import)
|
||||||
|
- **Problema:** Operações multi-write sem atomicidade
|
||||||
|
- **Impacto:** Inconsistência de dados, registos órfãos
|
||||||
|
- **Prioridade:** P0 - Corrigir antes de produção
|
||||||
|
|
||||||
|
### 3. Rate Limiting Ineficaz (MÉDIA)
|
||||||
|
- **Problema:** Rate limiting em memória local (não distribuído)
|
||||||
|
- **Impacto:** Não funciona em multi-instância, perde estado em restart
|
||||||
|
- **Prioridade:** P1 - Melhorar para produção escalável
|
||||||
|
|
||||||
|
### 4. Exposição de Logs (MÉDIA)
|
||||||
|
- **Problema:** Queries logadas podem conter dados sensíveis
|
||||||
|
- **Impacto:** Exposição de credenciais, tokens, dados pessoais
|
||||||
|
- **Prioridade:** P1 - Corrigir antes de produção
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ⚡ Problemas de Performance
|
||||||
|
|
||||||
|
### 1. N+1 Queries (ALTA)
|
||||||
|
- **Localização:** `collections.ts`, `documents.ts`, `analytics.ts`
|
||||||
|
- **Impacto:** Performance degradada com grandes volumes
|
||||||
|
- **Solução:** Usar JOINs e json_agg
|
||||||
|
|
||||||
|
### 2. Ausência de Índices (MÉDIA)
|
||||||
|
- **Problema:** Sem documentação de índices necessários
|
||||||
|
- **Impacto:** Queries lentas em tabelas grandes
|
||||||
|
- **Solução:** Criar `migrations/001_indexes.sql`
|
||||||
|
|
||||||
|
### 3. Connection Pool Não Tunado (BAIXA)
|
||||||
|
- **Problema:** Pool usa configurações default
|
||||||
|
- **Solução:** Adicionar defaults razoáveis (max: 20, min: 5)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ✅ Pontos Fortes
|
||||||
|
|
||||||
|
1. **Arquitectura Sólida** - Separação clara, padrões consistentes
|
||||||
|
2. **Boa Cobertura** - 164 tools cobrindo todas as áreas do Outline
|
||||||
|
3. **TypeScript** - Type safety bem implementado (95%)
|
||||||
|
4. **Manutenibilidade** - Código legível, fácil de estender
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🚀 Roadmap de Correcções
|
||||||
|
|
||||||
|
### Fase 1: Segurança Crítica (2 semanas) - P0
|
||||||
|
- Corrigir SQL Injection (164 tools)
|
||||||
|
- Implementar Transacções (16 tools)
|
||||||
|
- Validação robusta de inputs
|
||||||
|
- Audit logging básico
|
||||||
|
|
||||||
|
### Fase 2: Performance (1 semana) - P1
|
||||||
|
- Eliminar N+1 queries
|
||||||
|
- Criar índices necessários
|
||||||
|
- Optimizar connection pool
|
||||||
|
- Cursor-based pagination
|
||||||
|
|
||||||
|
### Fase 3: Qualidade (2 semanas) - P2
|
||||||
|
- Testes unitários (>80% coverage)
|
||||||
|
- CI/CD (GitHub Actions)
|
||||||
|
- Refactoring de código duplicado
|
||||||
|
|
||||||
|
### Fase 4: Funcionalidades (ongoing) - P3
|
||||||
|
- Rate limiting distribuído (Redis)
|
||||||
|
- Autorização (RBAC)
|
||||||
|
- Monitoring (Prometheus/Grafana)
|
||||||
|
- Documentação completa
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📋 Documentos Criados
|
||||||
|
|
||||||
|
1. **[AUDITORIA-COMPLETA.md](file:///home/ealmeida/mcp-servers/mcp-outline-postgresql/AUDITORIA-COMPLETA.md)** - Análise detalhada de segurança, performance e qualidade
|
||||||
|
2. **[PLANO-MELHORIAS.md](file:///home/ealmeida/mcp-servers/mcp-outline-postgresql/PLANO-MELHORIAS.md)** - Plano de implementação em 4 fases com código de exemplo
|
||||||
|
3. **[ROADMAP.md](file:///home/ealmeida/mcp-servers/mcp-outline-postgresql/ROADMAP.md)** - Checklist de 52 tarefas organizadas por prioridade
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🎯 Próximos Passos Recomendados
|
||||||
|
|
||||||
|
1. ✅ **Rever documentos de auditoria** (CONCLUÍDO)
|
||||||
|
2. ⏭️ **Decidir:** Avançar com Fase 1 (Segurança Crítica)?
|
||||||
|
3. ⏭️ **Se sim:** Criar branch `security-fixes`
|
||||||
|
4. ⏭️ **Iniciar:** Tarefa 1.1.1 - Auditar queries vulneráveis
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📊 Métricas de Sucesso
|
||||||
|
|
||||||
|
### Segurança
|
||||||
|
- ✅ 0 vulnerabilidades críticas
|
||||||
|
- ✅ 100% queries parametrizadas
|
||||||
|
- ✅ 100% operações críticas com transacções
|
||||||
|
|
||||||
|
### Performance
|
||||||
|
- ✅ Queries < 100ms (p95)
|
||||||
|
- ✅ 0 N+1 queries
|
||||||
|
- ✅ Índices documentados e criados
|
||||||
|
|
||||||
|
### Qualidade
|
||||||
|
- ✅ Code coverage > 80%
|
||||||
|
- ✅ CI passing
|
||||||
|
- ✅ Duplicação < 5%
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📞 Contacto
|
||||||
|
|
||||||
|
**Auditor:** Antigravity AI
|
||||||
|
**Organização:** Descomplicar®
|
||||||
|
**Email:** emanuel@descomplicar.pt
|
||||||
|
**Website:** https://descomplicar.pt
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
*Auditoria realizada em 2026-01-31 | MCP Outline PostgreSQL v1.2.1*
|
||||||
@@ -6,7 +6,7 @@
|
|||||||
|
|
||||||
import { Pool } from 'pg';
|
import { Pool } from 'pg';
|
||||||
import { BaseTool, ToolResponse, PaginationArgs } from '../types/tools.js';
|
import { BaseTool, ToolResponse, PaginationArgs } from '../types/tools.js';
|
||||||
import { validatePagination, isValidUUID, sanitizeInput } from '../utils/security.js';
|
import { validatePagination, isValidUUID, sanitizeInput, validateDaysInterval } from '../utils/security.js';
|
||||||
|
|
||||||
interface AdvancedSearchArgs extends PaginationArgs {
|
interface AdvancedSearchArgs extends PaginationArgs {
|
||||||
query: string;
|
query: string;
|
||||||
@@ -197,11 +197,14 @@ const searchRecent: BaseTool<PaginationArgs & { collection_id?: string; days?: n
|
|||||||
},
|
},
|
||||||
handler: async (args, pgClient): Promise<ToolResponse> => {
|
handler: async (args, pgClient): Promise<ToolResponse> => {
|
||||||
const { limit, offset } = validatePagination(args.limit, args.offset);
|
const { limit, offset } = validatePagination(args.limit, args.offset);
|
||||||
const days = args.days || 7;
|
|
||||||
|
// Validate and sanitize days parameter
|
||||||
|
const safeDays = validateDaysInterval(args.days, 7, 365);
|
||||||
|
|
||||||
const conditions: string[] = [
|
const conditions: string[] = [
|
||||||
'd."deletedAt" IS NULL',
|
'd."deletedAt" IS NULL',
|
||||||
'd."archivedAt" IS NULL',
|
'd."archivedAt" IS NULL',
|
||||||
`d."updatedAt" >= NOW() - INTERVAL '${days} days'`
|
`d."updatedAt" >= NOW() - make_interval(days => ${safeDays})`
|
||||||
];
|
];
|
||||||
const params: any[] = [];
|
const params: any[] = [];
|
||||||
let idx = 1;
|
let idx = 1;
|
||||||
@@ -229,7 +232,7 @@ const searchRecent: BaseTool<PaginationArgs & { collection_id?: string; days?: n
|
|||||||
return {
|
return {
|
||||||
content: [{ type: 'text', text: JSON.stringify({
|
content: [{ type: 'text', text: JSON.stringify({
|
||||||
data: result.rows,
|
data: result.rows,
|
||||||
days,
|
days: safeDays,
|
||||||
pagination: { limit, offset, total: result.rows.length }
|
pagination: { limit, offset, total: result.rows.length }
|
||||||
}, null, 2) }],
|
}, null, 2) }],
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
|
|
||||||
import { Pool } from 'pg';
|
import { Pool } from 'pg';
|
||||||
import { BaseTool, ToolResponse } from '../types/tools.js';
|
import { BaseTool, ToolResponse } from '../types/tools.js';
|
||||||
import { isValidUUID } from '../utils/security.js';
|
import { isValidUUID, validateDaysInterval, isValidISODate, validatePeriod } from '../utils/security.js';
|
||||||
|
|
||||||
interface DateRangeArgs {
|
interface DateRangeArgs {
|
||||||
date_from?: string;
|
date_from?: string;
|
||||||
@@ -27,11 +27,23 @@ const getAnalyticsOverview: BaseTool<DateRangeArgs> = {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
handler: async (args, pgClient): Promise<ToolResponse> => {
|
handler: async (args, pgClient): Promise<ToolResponse> => {
|
||||||
const dateCondition = args.date_from && args.date_to
|
// Validate date parameters if provided
|
||||||
? `AND "createdAt" BETWEEN '${args.date_from}' AND '${args.date_to}'`
|
if (args.date_from && !isValidISODate(args.date_from)) {
|
||||||
: '';
|
throw new Error('Invalid date_from format. Use ISO format (YYYY-MM-DD)');
|
||||||
|
}
|
||||||
|
if (args.date_to && !isValidISODate(args.date_to)) {
|
||||||
|
throw new Error('Invalid date_to format. Use ISO format (YYYY-MM-DD)');
|
||||||
|
}
|
||||||
|
|
||||||
// Document stats
|
// Build date condition with parameterized query
|
||||||
|
const dateParams: any[] = [];
|
||||||
|
let dateCondition = '';
|
||||||
|
if (args.date_from && args.date_to) {
|
||||||
|
dateCondition = `AND "createdAt" BETWEEN $1 AND $2`;
|
||||||
|
dateParams.push(args.date_from, args.date_to);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Document stats (no date filter needed for totals)
|
||||||
const docStats = await pgClient.query(`
|
const docStats = await pgClient.query(`
|
||||||
SELECT
|
SELECT
|
||||||
COUNT(*) as "totalDocuments",
|
COUNT(*) as "totalDocuments",
|
||||||
@@ -105,19 +117,32 @@ const getUserActivityAnalytics: BaseTool<{ user_id?: string; days?: number }> =
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
handler: async (args, pgClient): Promise<ToolResponse> => {
|
handler: async (args, pgClient): Promise<ToolResponse> => {
|
||||||
const days = args.days || 30;
|
// Validate user_id FIRST before using it
|
||||||
const userCondition = args.user_id ? `AND u.id = '${args.user_id}'` : '';
|
if (args.user_id && !isValidUUID(args.user_id)) {
|
||||||
|
throw new Error('Invalid user_id');
|
||||||
|
}
|
||||||
|
|
||||||
if (args.user_id && !isValidUUID(args.user_id)) throw new Error('Invalid user_id');
|
// Validate and sanitize days parameter
|
||||||
|
const safeDays = validateDaysInterval(args.days, 30, 365);
|
||||||
|
|
||||||
// Most active users
|
// Build query with safe interval (number is safe after validation)
|
||||||
|
const params: any[] = [];
|
||||||
|
let paramIdx = 1;
|
||||||
|
let userCondition = '';
|
||||||
|
|
||||||
|
if (args.user_id) {
|
||||||
|
userCondition = `AND u.id = $${paramIdx++}`;
|
||||||
|
params.push(args.user_id);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Most active users - using make_interval for safety
|
||||||
const activeUsers = await pgClient.query(`
|
const activeUsers = await pgClient.query(`
|
||||||
SELECT
|
SELECT
|
||||||
u.id, u.name, u.email,
|
u.id, u.name, u.email,
|
||||||
COUNT(DISTINCT d.id) FILTER (WHERE d."createdAt" >= NOW() - INTERVAL '${days} days') as "documentsCreated",
|
COUNT(DISTINCT d.id) FILTER (WHERE d."createdAt" >= NOW() - make_interval(days => ${safeDays})) as "documentsCreated",
|
||||||
COUNT(DISTINCT d2.id) FILTER (WHERE d2."updatedAt" >= NOW() - INTERVAL '${days} days') as "documentsEdited",
|
COUNT(DISTINCT d2.id) FILTER (WHERE d2."updatedAt" >= NOW() - make_interval(days => ${safeDays})) as "documentsEdited",
|
||||||
COUNT(DISTINCT v."documentId") FILTER (WHERE v."createdAt" >= NOW() - INTERVAL '${days} days') as "documentsViewed",
|
COUNT(DISTINCT v."documentId") FILTER (WHERE v."createdAt" >= NOW() - make_interval(days => ${safeDays})) as "documentsViewed",
|
||||||
COUNT(DISTINCT c.id) FILTER (WHERE c."createdAt" >= NOW() - INTERVAL '${days} days') as "commentsAdded"
|
COUNT(DISTINCT c.id) FILTER (WHERE c."createdAt" >= NOW() - make_interval(days => ${safeDays})) as "commentsAdded"
|
||||||
FROM users u
|
FROM users u
|
||||||
LEFT JOIN documents d ON d."createdById" = u.id
|
LEFT JOIN documents d ON d."createdById" = u.id
|
||||||
LEFT JOIN documents d2 ON d2."lastModifiedById" = u.id
|
LEFT JOIN documents d2 ON d2."lastModifiedById" = u.id
|
||||||
@@ -127,7 +152,7 @@ const getUserActivityAnalytics: BaseTool<{ user_id?: string; days?: number }> =
|
|||||||
GROUP BY u.id, u.name, u.email
|
GROUP BY u.id, u.name, u.email
|
||||||
ORDER BY "documentsCreated" DESC
|
ORDER BY "documentsCreated" DESC
|
||||||
LIMIT 20
|
LIMIT 20
|
||||||
`);
|
`, params);
|
||||||
|
|
||||||
// Activity by day of week
|
// Activity by day of week
|
||||||
const activityByDay = await pgClient.query(`
|
const activityByDay = await pgClient.query(`
|
||||||
@@ -135,7 +160,7 @@ const getUserActivityAnalytics: BaseTool<{ user_id?: string; days?: number }> =
|
|||||||
EXTRACT(DOW FROM d."createdAt") as "dayOfWeek",
|
EXTRACT(DOW FROM d."createdAt") as "dayOfWeek",
|
||||||
COUNT(*) as "documentsCreated"
|
COUNT(*) as "documentsCreated"
|
||||||
FROM documents d
|
FROM documents d
|
||||||
WHERE d."createdAt" >= NOW() - INTERVAL '${days} days'
|
WHERE d."createdAt" >= NOW() - make_interval(days => ${safeDays})
|
||||||
AND d."deletedAt" IS NULL
|
AND d."deletedAt" IS NULL
|
||||||
GROUP BY EXTRACT(DOW FROM d."createdAt")
|
GROUP BY EXTRACT(DOW FROM d."createdAt")
|
||||||
ORDER BY "dayOfWeek"
|
ORDER BY "dayOfWeek"
|
||||||
@@ -147,7 +172,7 @@ const getUserActivityAnalytics: BaseTool<{ user_id?: string; days?: number }> =
|
|||||||
EXTRACT(HOUR FROM d."createdAt") as "hour",
|
EXTRACT(HOUR FROM d."createdAt") as "hour",
|
||||||
COUNT(*) as "documentsCreated"
|
COUNT(*) as "documentsCreated"
|
||||||
FROM documents d
|
FROM documents d
|
||||||
WHERE d."createdAt" >= NOW() - INTERVAL '${days} days'
|
WHERE d."createdAt" >= NOW() - make_interval(days => ${safeDays})
|
||||||
AND d."deletedAt" IS NULL
|
AND d."deletedAt" IS NULL
|
||||||
GROUP BY EXTRACT(HOUR FROM d."createdAt")
|
GROUP BY EXTRACT(HOUR FROM d."createdAt")
|
||||||
ORDER BY "hour"
|
ORDER BY "hour"
|
||||||
@@ -158,7 +183,7 @@ const getUserActivityAnalytics: BaseTool<{ user_id?: string; days?: number }> =
|
|||||||
activeUsers: activeUsers.rows,
|
activeUsers: activeUsers.rows,
|
||||||
activityByDayOfWeek: activityByDay.rows,
|
activityByDayOfWeek: activityByDay.rows,
|
||||||
activityByHour: activityByHour.rows,
|
activityByHour: activityByHour.rows,
|
||||||
periodDays: days,
|
periodDays: safeDays,
|
||||||
}, null, 2) }],
|
}, null, 2) }],
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
@@ -177,11 +202,20 @@ const getContentInsights: BaseTool<{ collection_id?: string }> = {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
handler: async (args, pgClient): Promise<ToolResponse> => {
|
handler: async (args, pgClient): Promise<ToolResponse> => {
|
||||||
const collectionCondition = args.collection_id
|
// Validate collection_id FIRST before using it
|
||||||
? `AND d."collectionId" = '${args.collection_id}'`
|
if (args.collection_id && !isValidUUID(args.collection_id)) {
|
||||||
: '';
|
throw new Error('Invalid collection_id');
|
||||||
|
}
|
||||||
|
|
||||||
if (args.collection_id && !isValidUUID(args.collection_id)) throw new Error('Invalid collection_id');
|
// Build parameterized query
|
||||||
|
const params: any[] = [];
|
||||||
|
let paramIdx = 1;
|
||||||
|
let collectionCondition = '';
|
||||||
|
|
||||||
|
if (args.collection_id) {
|
||||||
|
collectionCondition = `AND d."collectionId" = $${paramIdx++}`;
|
||||||
|
params.push(args.collection_id);
|
||||||
|
}
|
||||||
|
|
||||||
// Most viewed documents
|
// Most viewed documents
|
||||||
const mostViewed = await pgClient.query(`
|
const mostViewed = await pgClient.query(`
|
||||||
@@ -196,7 +230,7 @@ const getContentInsights: BaseTool<{ collection_id?: string }> = {
|
|||||||
GROUP BY d.id, d.title, d.emoji, c.name
|
GROUP BY d.id, d.title, d.emoji, c.name
|
||||||
ORDER BY "viewCount" DESC
|
ORDER BY "viewCount" DESC
|
||||||
LIMIT 10
|
LIMIT 10
|
||||||
`);
|
`, params);
|
||||||
|
|
||||||
// Most starred documents
|
// Most starred documents
|
||||||
const mostStarred = await pgClient.query(`
|
const mostStarred = await pgClient.query(`
|
||||||
@@ -211,7 +245,7 @@ const getContentInsights: BaseTool<{ collection_id?: string }> = {
|
|||||||
HAVING COUNT(s.id) > 0
|
HAVING COUNT(s.id) > 0
|
||||||
ORDER BY "starCount" DESC
|
ORDER BY "starCount" DESC
|
||||||
LIMIT 10
|
LIMIT 10
|
||||||
`);
|
`, params);
|
||||||
|
|
||||||
// Stale documents (not updated in 90 days)
|
// Stale documents (not updated in 90 days)
|
||||||
const staleDocuments = await pgClient.query(`
|
const staleDocuments = await pgClient.query(`
|
||||||
@@ -228,7 +262,7 @@ const getContentInsights: BaseTool<{ collection_id?: string }> = {
|
|||||||
${collectionCondition}
|
${collectionCondition}
|
||||||
ORDER BY d."updatedAt" ASC
|
ORDER BY d."updatedAt" ASC
|
||||||
LIMIT 20
|
LIMIT 20
|
||||||
`);
|
`, params);
|
||||||
|
|
||||||
// Documents without views
|
// Documents without views
|
||||||
const neverViewed = await pgClient.query(`
|
const neverViewed = await pgClient.query(`
|
||||||
@@ -244,7 +278,7 @@ const getContentInsights: BaseTool<{ collection_id?: string }> = {
|
|||||||
${collectionCondition}
|
${collectionCondition}
|
||||||
ORDER BY d."createdAt" DESC
|
ORDER BY d."createdAt" DESC
|
||||||
LIMIT 20
|
LIMIT 20
|
||||||
`);
|
`, params);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
content: [{ type: 'text', text: JSON.stringify({
|
content: [{ type: 'text', text: JSON.stringify({
|
||||||
@@ -270,11 +304,19 @@ const getCollectionStats: BaseTool<{ collection_id?: string }> = {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
handler: async (args, pgClient): Promise<ToolResponse> => {
|
handler: async (args, pgClient): Promise<ToolResponse> => {
|
||||||
const collectionCondition = args.collection_id
|
// Validate collection_id FIRST before using it
|
||||||
? `AND c.id = '${args.collection_id}'`
|
if (args.collection_id && !isValidUUID(args.collection_id)) {
|
||||||
: '';
|
throw new Error('Invalid collection_id');
|
||||||
|
}
|
||||||
|
|
||||||
if (args.collection_id && !isValidUUID(args.collection_id)) throw new Error('Invalid collection_id');
|
// Build parameterized query
|
||||||
|
const params: any[] = [];
|
||||||
|
let collectionCondition = '';
|
||||||
|
|
||||||
|
if (args.collection_id) {
|
||||||
|
collectionCondition = `AND c.id = $1`;
|
||||||
|
params.push(args.collection_id);
|
||||||
|
}
|
||||||
|
|
||||||
const stats = await pgClient.query(`
|
const stats = await pgClient.query(`
|
||||||
SELECT
|
SELECT
|
||||||
@@ -293,7 +335,7 @@ const getCollectionStats: BaseTool<{ collection_id?: string }> = {
|
|||||||
WHERE c."deletedAt" IS NULL ${collectionCondition}
|
WHERE c."deletedAt" IS NULL ${collectionCondition}
|
||||||
GROUP BY c.id, c.name, c.icon, c.color
|
GROUP BY c.id, c.name, c.icon, c.color
|
||||||
ORDER BY "documentCount" DESC
|
ORDER BY "documentCount" DESC
|
||||||
`);
|
`, params);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
content: [{ type: 'text', text: JSON.stringify({ data: stats.rows }, null, 2) }],
|
content: [{ type: 'text', text: JSON.stringify({ data: stats.rows }, null, 2) }],
|
||||||
@@ -314,14 +356,18 @@ const getGrowthMetrics: BaseTool<{ period?: string }> = {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
handler: async (args, pgClient): Promise<ToolResponse> => {
|
handler: async (args, pgClient): Promise<ToolResponse> => {
|
||||||
const period = args.period || 'month';
|
// Validate period against allowed values
|
||||||
const intervals: Record<string, string> = {
|
const allowedPeriods = ['week', 'month', 'quarter', 'year'];
|
||||||
week: '7 days',
|
const period = validatePeriod(args.period, allowedPeriods, 'month');
|
||||||
month: '30 days',
|
|
||||||
quarter: '90 days',
|
// Map periods to safe integer days (no string interpolation needed)
|
||||||
year: '365 days',
|
const periodDays: Record<string, number> = {
|
||||||
|
week: 7,
|
||||||
|
month: 30,
|
||||||
|
quarter: 90,
|
||||||
|
year: 365,
|
||||||
};
|
};
|
||||||
const interval = intervals[period] || '30 days';
|
const safeDays = periodDays[period];
|
||||||
|
|
||||||
// Document growth by day
|
// Document growth by day
|
||||||
const documentGrowth = await pgClient.query(`
|
const documentGrowth = await pgClient.query(`
|
||||||
@@ -330,7 +376,7 @@ const getGrowthMetrics: BaseTool<{ period?: string }> = {
|
|||||||
COUNT(*) as "newDocuments",
|
COUNT(*) as "newDocuments",
|
||||||
SUM(COUNT(*)) OVER (ORDER BY DATE(d."createdAt")) as "cumulativeDocuments"
|
SUM(COUNT(*)) OVER (ORDER BY DATE(d."createdAt")) as "cumulativeDocuments"
|
||||||
FROM documents d
|
FROM documents d
|
||||||
WHERE d."createdAt" >= NOW() - INTERVAL '${interval}'
|
WHERE d."createdAt" >= NOW() - make_interval(days => ${safeDays})
|
||||||
AND d."deletedAt" IS NULL
|
AND d."deletedAt" IS NULL
|
||||||
GROUP BY DATE(d."createdAt")
|
GROUP BY DATE(d."createdAt")
|
||||||
ORDER BY date
|
ORDER BY date
|
||||||
@@ -343,7 +389,7 @@ const getGrowthMetrics: BaseTool<{ period?: string }> = {
|
|||||||
COUNT(*) as "newUsers",
|
COUNT(*) as "newUsers",
|
||||||
SUM(COUNT(*)) OVER (ORDER BY DATE(u."createdAt")) as "cumulativeUsers"
|
SUM(COUNT(*)) OVER (ORDER BY DATE(u."createdAt")) as "cumulativeUsers"
|
||||||
FROM users u
|
FROM users u
|
||||||
WHERE u."createdAt" >= NOW() - INTERVAL '${interval}'
|
WHERE u."createdAt" >= NOW() - make_interval(days => ${safeDays})
|
||||||
AND u."deletedAt" IS NULL
|
AND u."deletedAt" IS NULL
|
||||||
GROUP BY DATE(u."createdAt")
|
GROUP BY DATE(u."createdAt")
|
||||||
ORDER BY date
|
ORDER BY date
|
||||||
@@ -355,7 +401,7 @@ const getGrowthMetrics: BaseTool<{ period?: string }> = {
|
|||||||
DATE(c."createdAt") as date,
|
DATE(c."createdAt") as date,
|
||||||
COUNT(*) as "newCollections"
|
COUNT(*) as "newCollections"
|
||||||
FROM collections c
|
FROM collections c
|
||||||
WHERE c."createdAt" >= NOW() - INTERVAL '${interval}'
|
WHERE c."createdAt" >= NOW() - make_interval(days => ${safeDays})
|
||||||
AND c."deletedAt" IS NULL
|
AND c."deletedAt" IS NULL
|
||||||
GROUP BY DATE(c."createdAt")
|
GROUP BY DATE(c."createdAt")
|
||||||
ORDER BY date
|
ORDER BY date
|
||||||
@@ -364,10 +410,10 @@ const getGrowthMetrics: BaseTool<{ period?: string }> = {
|
|||||||
// Period comparison
|
// Period comparison
|
||||||
const comparison = await pgClient.query(`
|
const comparison = await pgClient.query(`
|
||||||
SELECT
|
SELECT
|
||||||
(SELECT COUNT(*) FROM documents WHERE "createdAt" >= NOW() - INTERVAL '${interval}' AND "deletedAt" IS NULL) as "currentPeriodDocs",
|
(SELECT COUNT(*) FROM documents WHERE "createdAt" >= NOW() - make_interval(days => ${safeDays}) AND "deletedAt" IS NULL) as "currentPeriodDocs",
|
||||||
(SELECT COUNT(*) FROM documents WHERE "createdAt" >= NOW() - INTERVAL '${interval}' * 2 AND "createdAt" < NOW() - INTERVAL '${interval}' AND "deletedAt" IS NULL) as "previousPeriodDocs",
|
(SELECT COUNT(*) FROM documents WHERE "createdAt" >= NOW() - make_interval(days => ${safeDays * 2}) AND "createdAt" < NOW() - make_interval(days => ${safeDays}) AND "deletedAt" IS NULL) as "previousPeriodDocs",
|
||||||
(SELECT COUNT(*) FROM users WHERE "createdAt" >= NOW() - INTERVAL '${interval}' AND "deletedAt" IS NULL) as "currentPeriodUsers",
|
(SELECT COUNT(*) FROM users WHERE "createdAt" >= NOW() - make_interval(days => ${safeDays}) AND "deletedAt" IS NULL) as "currentPeriodUsers",
|
||||||
(SELECT COUNT(*) FROM users WHERE "createdAt" >= NOW() - INTERVAL '${interval}' * 2 AND "createdAt" < NOW() - INTERVAL '${interval}' AND "deletedAt" IS NULL) as "previousPeriodUsers"
|
(SELECT COUNT(*) FROM users WHERE "createdAt" >= NOW() - make_interval(days => ${safeDays * 2}) AND "createdAt" < NOW() - make_interval(days => ${safeDays}) AND "deletedAt" IS NULL) as "previousPeriodUsers"
|
||||||
`);
|
`);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -395,7 +441,8 @@ const getSearchAnalytics: BaseTool<{ days?: number }> = {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
handler: async (args, pgClient): Promise<ToolResponse> => {
|
handler: async (args, pgClient): Promise<ToolResponse> => {
|
||||||
const days = args.days || 30;
|
// Validate and sanitize days parameter
|
||||||
|
const safeDays = validateDaysInterval(args.days, 30, 365);
|
||||||
|
|
||||||
// Popular search queries
|
// Popular search queries
|
||||||
const popularQueries = await pgClient.query(`
|
const popularQueries = await pgClient.query(`
|
||||||
@@ -404,7 +451,7 @@ const getSearchAnalytics: BaseTool<{ days?: number }> = {
|
|||||||
COUNT(*) as "searchCount",
|
COUNT(*) as "searchCount",
|
||||||
COUNT(DISTINCT "userId") as "uniqueSearchers"
|
COUNT(DISTINCT "userId") as "uniqueSearchers"
|
||||||
FROM search_queries
|
FROM search_queries
|
||||||
WHERE "createdAt" >= NOW() - INTERVAL '${days} days'
|
WHERE "createdAt" >= NOW() - make_interval(days => ${safeDays})
|
||||||
GROUP BY query
|
GROUP BY query
|
||||||
ORDER BY "searchCount" DESC
|
ORDER BY "searchCount" DESC
|
||||||
LIMIT 20
|
LIMIT 20
|
||||||
@@ -417,7 +464,7 @@ const getSearchAnalytics: BaseTool<{ days?: number }> = {
|
|||||||
COUNT(*) as "searches",
|
COUNT(*) as "searches",
|
||||||
COUNT(DISTINCT "userId") as "uniqueSearchers"
|
COUNT(DISTINCT "userId") as "uniqueSearchers"
|
||||||
FROM search_queries
|
FROM search_queries
|
||||||
WHERE "createdAt" >= NOW() - INTERVAL '${days} days'
|
WHERE "createdAt" >= NOW() - make_interval(days => ${safeDays})
|
||||||
GROUP BY DATE("createdAt")
|
GROUP BY DATE("createdAt")
|
||||||
ORDER BY date
|
ORDER BY date
|
||||||
`);
|
`);
|
||||||
@@ -428,7 +475,7 @@ const getSearchAnalytics: BaseTool<{ days?: number }> = {
|
|||||||
query,
|
query,
|
||||||
COUNT(*) as "searchCount"
|
COUNT(*) as "searchCount"
|
||||||
FROM search_queries
|
FROM search_queries
|
||||||
WHERE "createdAt" >= NOW() - INTERVAL '${days} days'
|
WHERE "createdAt" >= NOW() - make_interval(days => ${safeDays})
|
||||||
AND results = 0
|
AND results = 0
|
||||||
GROUP BY query
|
GROUP BY query
|
||||||
ORDER BY "searchCount" DESC
|
ORDER BY "searchCount" DESC
|
||||||
@@ -440,7 +487,7 @@ const getSearchAnalytics: BaseTool<{ days?: number }> = {
|
|||||||
popularQueries: popularQueries.rows,
|
popularQueries: popularQueries.rows,
|
||||||
searchVolume: searchVolume.rows,
|
searchVolume: searchVolume.rows,
|
||||||
zeroResultQueries: zeroResults.rows,
|
zeroResultQueries: zeroResults.rows,
|
||||||
periodDays: days,
|
periodDays: safeDays,
|
||||||
}, null, 2) }],
|
}, null, 2) }],
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -4,10 +4,28 @@
|
|||||||
* @author Descomplicar® | @link descomplicar.pt | @copyright 2026
|
* @author Descomplicar® | @link descomplicar.pt | @copyright 2026
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Pool } from 'pg';
|
import { Pool, PoolClient } from 'pg';
|
||||||
import { BaseTool, ToolResponse } from '../types/tools.js';
|
import { BaseTool, ToolResponse } from '../types/tools.js';
|
||||||
import { isValidUUID } from '../utils/security.js';
|
import { isValidUUID } from '../utils/security.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Execute operations within a transaction
|
||||||
|
*/
|
||||||
|
async function withTransaction<T>(pool: Pool, callback: (client: PoolClient) => Promise<T>): Promise<T> {
|
||||||
|
const client = await pool.connect();
|
||||||
|
try {
|
||||||
|
await client.query('BEGIN');
|
||||||
|
const result = await callback(client);
|
||||||
|
await client.query('COMMIT');
|
||||||
|
return result;
|
||||||
|
} catch (error) {
|
||||||
|
await client.query('ROLLBACK');
|
||||||
|
throw error;
|
||||||
|
} finally {
|
||||||
|
client.release();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* bulk.archive_documents - Archive multiple documents
|
* bulk.archive_documents - Archive multiple documents
|
||||||
*/
|
*/
|
||||||
@@ -30,12 +48,15 @@ const bulkArchiveDocuments: BaseTool<{ document_ids: string[] }> = {
|
|||||||
if (!isValidUUID(id)) throw new Error(`Invalid document ID: ${id}`);
|
if (!isValidUUID(id)) throw new Error(`Invalid document ID: ${id}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const result = await pgClient.query(`
|
// Use transaction for atomic operation
|
||||||
UPDATE documents
|
const result = await withTransaction(pgClient, async (client) => {
|
||||||
SET "archivedAt" = NOW(), "updatedAt" = NOW()
|
return await client.query(`
|
||||||
WHERE id = ANY($1) AND "archivedAt" IS NULL AND "deletedAt" IS NULL
|
UPDATE documents
|
||||||
RETURNING id, title
|
SET "archivedAt" = NOW(), "updatedAt" = NOW()
|
||||||
`, [args.document_ids]);
|
WHERE id = ANY($1) AND "archivedAt" IS NULL AND "deletedAt" IS NULL
|
||||||
|
RETURNING id, title
|
||||||
|
`, [args.document_ids]);
|
||||||
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
content: [{ type: 'text', text: JSON.stringify({
|
content: [{ type: 'text', text: JSON.stringify({
|
||||||
@@ -69,15 +90,18 @@ const bulkDeleteDocuments: BaseTool<{ document_ids: string[] }> = {
|
|||||||
if (!isValidUUID(id)) throw new Error(`Invalid document ID: ${id}`);
|
if (!isValidUUID(id)) throw new Error(`Invalid document ID: ${id}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const deletedById = await pgClient.query(`SELECT id FROM users WHERE role = 'admin' AND "deletedAt" IS NULL LIMIT 1`);
|
// Use transaction for atomic operation
|
||||||
const userId = deletedById.rows.length > 0 ? deletedById.rows[0].id : null;
|
const result = await withTransaction(pgClient, async (client) => {
|
||||||
|
const deletedById = await client.query(`SELECT id FROM users WHERE role = 'admin' AND "deletedAt" IS NULL LIMIT 1`);
|
||||||
|
const userId = deletedById.rows.length > 0 ? deletedById.rows[0].id : null;
|
||||||
|
|
||||||
const result = await pgClient.query(`
|
return await client.query(`
|
||||||
UPDATE documents
|
UPDATE documents
|
||||||
SET "deletedAt" = NOW(), "deletedById" = $2, "updatedAt" = NOW()
|
SET "deletedAt" = NOW(), "deletedById" = $2, "updatedAt" = NOW()
|
||||||
WHERE id = ANY($1) AND "deletedAt" IS NULL
|
WHERE id = ANY($1) AND "deletedAt" IS NULL
|
||||||
RETURNING id, title
|
RETURNING id, title
|
||||||
`, [args.document_ids, userId]);
|
`, [args.document_ids, userId]);
|
||||||
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
content: [{ type: 'text', text: JSON.stringify({
|
content: [{ type: 'text', text: JSON.stringify({
|
||||||
@@ -115,19 +139,22 @@ const bulkMoveDocuments: BaseTool<{ document_ids: string[]; collection_id: strin
|
|||||||
if (!isValidUUID(id)) throw new Error(`Invalid document ID: ${id}`);
|
if (!isValidUUID(id)) throw new Error(`Invalid document ID: ${id}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Verify collection exists
|
// Use transaction for atomic operation
|
||||||
const collectionCheck = await pgClient.query(
|
const result = await withTransaction(pgClient, async (client) => {
|
||||||
`SELECT id FROM collections WHERE id = $1 AND "deletedAt" IS NULL`,
|
// Verify collection exists
|
||||||
[args.collection_id]
|
const collectionCheck = await client.query(
|
||||||
);
|
`SELECT id FROM collections WHERE id = $1 AND "deletedAt" IS NULL`,
|
||||||
if (collectionCheck.rows.length === 0) throw new Error('Collection not found');
|
[args.collection_id]
|
||||||
|
);
|
||||||
|
if (collectionCheck.rows.length === 0) throw new Error('Collection not found');
|
||||||
|
|
||||||
const result = await pgClient.query(`
|
return await client.query(`
|
||||||
UPDATE documents
|
UPDATE documents
|
||||||
SET "collectionId" = $2, "parentDocumentId" = $3, "updatedAt" = NOW()
|
SET "collectionId" = $2, "parentDocumentId" = $3, "updatedAt" = NOW()
|
||||||
WHERE id = ANY($1) AND "deletedAt" IS NULL
|
WHERE id = ANY($1) AND "deletedAt" IS NULL
|
||||||
RETURNING id, title, "collectionId"
|
RETURNING id, title, "collectionId"
|
||||||
`, [args.document_ids, args.collection_id, args.parent_document_id || null]);
|
`, [args.document_ids, args.collection_id, args.parent_document_id || null]);
|
||||||
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
content: [{ type: 'text', text: JSON.stringify({
|
content: [{ type: 'text', text: JSON.stringify({
|
||||||
@@ -161,12 +188,15 @@ const bulkRestoreDocuments: BaseTool<{ document_ids: string[] }> = {
|
|||||||
if (!isValidUUID(id)) throw new Error(`Invalid document ID: ${id}`);
|
if (!isValidUUID(id)) throw new Error(`Invalid document ID: ${id}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const result = await pgClient.query(`
|
// Use transaction for atomic operation
|
||||||
UPDATE documents
|
const result = await withTransaction(pgClient, async (client) => {
|
||||||
SET "deletedAt" = NULL, "deletedById" = NULL, "updatedAt" = NOW()
|
return await client.query(`
|
||||||
WHERE id = ANY($1) AND "deletedAt" IS NOT NULL
|
UPDATE documents
|
||||||
RETURNING id, title
|
SET "deletedAt" = NULL, "deletedById" = NULL, "updatedAt" = NOW()
|
||||||
`, [args.document_ids]);
|
WHERE id = ANY($1) AND "deletedAt" IS NOT NULL
|
||||||
|
RETURNING id, title
|
||||||
|
`, [args.document_ids]);
|
||||||
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
content: [{ type: 'text', text: JSON.stringify({
|
content: [{ type: 'text', text: JSON.stringify({
|
||||||
@@ -204,29 +234,35 @@ const bulkAddUsersToCollection: BaseTool<{ user_ids: string[]; collection_id: st
|
|||||||
}
|
}
|
||||||
|
|
||||||
const permission = args.permission || 'read_write';
|
const permission = args.permission || 'read_write';
|
||||||
const creatorResult = await pgClient.query(`SELECT id FROM users WHERE role = 'admin' AND "deletedAt" IS NULL LIMIT 1`);
|
|
||||||
const createdById = creatorResult.rows.length > 0 ? creatorResult.rows[0].id : args.user_ids[0];
|
|
||||||
|
|
||||||
const added: string[] = [];
|
// Use transaction for atomic operation
|
||||||
const skipped: string[] = [];
|
const { added, skipped } = await withTransaction(pgClient, async (client) => {
|
||||||
|
const creatorResult = await client.query(`SELECT id FROM users WHERE role = 'admin' AND "deletedAt" IS NULL LIMIT 1`);
|
||||||
|
const createdById = creatorResult.rows.length > 0 ? creatorResult.rows[0].id : args.user_ids[0];
|
||||||
|
|
||||||
for (const userId of args.user_ids) {
|
const addedList: string[] = [];
|
||||||
// Check if already exists
|
const skippedList: string[] = [];
|
||||||
const existing = await pgClient.query(
|
|
||||||
`SELECT "userId" FROM collection_users WHERE "userId" = $1 AND "collectionId" = $2`,
|
|
||||||
[userId, args.collection_id]
|
|
||||||
);
|
|
||||||
|
|
||||||
if (existing.rows.length > 0) {
|
for (const userId of args.user_ids) {
|
||||||
skipped.push(userId);
|
// Check if already exists
|
||||||
} else {
|
const existing = await client.query(
|
||||||
await pgClient.query(`
|
`SELECT "userId" FROM collection_users WHERE "userId" = $1 AND "collectionId" = $2`,
|
||||||
INSERT INTO collection_users ("userId", "collectionId", permission, "createdById", "createdAt", "updatedAt")
|
[userId, args.collection_id]
|
||||||
VALUES ($1, $2, $3, $4, NOW(), NOW())
|
);
|
||||||
`, [userId, args.collection_id, permission, createdById]);
|
|
||||||
added.push(userId);
|
if (existing.rows.length > 0) {
|
||||||
|
skippedList.push(userId);
|
||||||
|
} else {
|
||||||
|
await client.query(`
|
||||||
|
INSERT INTO collection_users ("userId", "collectionId", permission, "createdById", "createdAt", "updatedAt")
|
||||||
|
VALUES ($1, $2, $3, $4, NOW(), NOW())
|
||||||
|
`, [userId, args.collection_id, permission, createdById]);
|
||||||
|
addedList.push(userId);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
return { added: addedList, skipped: skippedList };
|
||||||
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
content: [{ type: 'text', text: JSON.stringify({
|
content: [{ type: 'text', text: JSON.stringify({
|
||||||
@@ -264,11 +300,14 @@ const bulkRemoveUsersFromCollection: BaseTool<{ user_ids: string[]; collection_i
|
|||||||
if (!isValidUUID(id)) throw new Error(`Invalid user ID: ${id}`);
|
if (!isValidUUID(id)) throw new Error(`Invalid user ID: ${id}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const result = await pgClient.query(`
|
// Use transaction for atomic operation
|
||||||
DELETE FROM collection_users
|
const result = await withTransaction(pgClient, async (client) => {
|
||||||
WHERE "userId" = ANY($1) AND "collectionId" = $2
|
return await client.query(`
|
||||||
RETURNING "userId"
|
DELETE FROM collection_users
|
||||||
`, [args.user_ids, args.collection_id]);
|
WHERE "userId" = ANY($1) AND "collectionId" = $2
|
||||||
|
RETURNING "userId"
|
||||||
|
`, [args.user_ids, args.collection_id]);
|
||||||
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
content: [{ type: 'text', text: JSON.stringify({
|
content: [{ type: 'text', text: JSON.stringify({
|
||||||
|
|||||||
@@ -4,10 +4,28 @@
|
|||||||
* @author Descomplicar® | @link descomplicar.pt | @copyright 2026
|
* @author Descomplicar® | @link descomplicar.pt | @copyright 2026
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Pool } from 'pg';
|
import { Pool, PoolClient } from 'pg';
|
||||||
import { BaseTool, ToolResponse } from '../types/tools.js';
|
import { BaseTool, ToolResponse } from '../types/tools.js';
|
||||||
import { isValidUUID, sanitizeInput } from '../utils/security.js';
|
import { isValidUUID, sanitizeInput } from '../utils/security.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Execute operations within a transaction
|
||||||
|
*/
|
||||||
|
async function withTransaction<T>(pool: Pool, callback: (client: PoolClient) => Promise<T>): Promise<T> {
|
||||||
|
const client = await pool.connect();
|
||||||
|
try {
|
||||||
|
await client.query('BEGIN');
|
||||||
|
const result = await callback(client);
|
||||||
|
await client.query('COMMIT');
|
||||||
|
return result;
|
||||||
|
} catch (error) {
|
||||||
|
await client.query('ROLLBACK');
|
||||||
|
throw error;
|
||||||
|
} finally {
|
||||||
|
client.release();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
interface CreateDeskProjectDocArgs {
|
interface CreateDeskProjectDocArgs {
|
||||||
collection_id: string;
|
collection_id: string;
|
||||||
desk_project_id: number;
|
desk_project_id: number;
|
||||||
@@ -69,113 +87,118 @@ const createDeskProjectDoc: BaseTool<CreateDeskProjectDocArgs> = {
|
|||||||
if (!isValidUUID(args.collection_id)) throw new Error('Invalid collection_id');
|
if (!isValidUUID(args.collection_id)) throw new Error('Invalid collection_id');
|
||||||
if (args.template_id && !isValidUUID(args.template_id)) throw new Error('Invalid template_id');
|
if (args.template_id && !isValidUUID(args.template_id)) throw new Error('Invalid template_id');
|
||||||
|
|
||||||
// Verify collection exists
|
|
||||||
const collection = await pgClient.query(
|
|
||||||
`SELECT id, "teamId" FROM collections WHERE id = $1 AND "deletedAt" IS NULL`,
|
|
||||||
[args.collection_id]
|
|
||||||
);
|
|
||||||
if (collection.rows.length === 0) throw new Error('Collection not found');
|
|
||||||
|
|
||||||
const teamId = collection.rows[0].teamId;
|
|
||||||
|
|
||||||
// Get admin user
|
|
||||||
const userResult = await pgClient.query(
|
|
||||||
`SELECT id FROM users WHERE role = 'admin' AND "deletedAt" IS NULL LIMIT 1`
|
|
||||||
);
|
|
||||||
if (userResult.rows.length === 0) throw new Error('No admin user found');
|
|
||||||
const userId = userResult.rows[0].id;
|
|
||||||
|
|
||||||
// Get template content if specified
|
|
||||||
let baseContent = '';
|
|
||||||
if (args.template_id) {
|
|
||||||
const template = await pgClient.query(
|
|
||||||
`SELECT text FROM documents WHERE id = $1 AND template = true AND "deletedAt" IS NULL`,
|
|
||||||
[args.template_id]
|
|
||||||
);
|
|
||||||
if (template.rows.length > 0) {
|
|
||||||
baseContent = template.rows[0].text || '';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Build document content
|
|
||||||
const includeTasks = args.include_tasks !== false;
|
const includeTasks = args.include_tasks !== false;
|
||||||
const projectName = sanitizeInput(args.desk_project_name);
|
const projectName = sanitizeInput(args.desk_project_name);
|
||||||
const customerName = args.desk_customer_name ? sanitizeInput(args.desk_customer_name) : null;
|
const customerName = args.desk_customer_name ? sanitizeInput(args.desk_customer_name) : null;
|
||||||
|
|
||||||
let content = baseContent || '';
|
// Use transaction for atomic operation (document + comment must be created together)
|
||||||
|
const newDoc = await withTransaction(pgClient, async (client) => {
|
||||||
|
// Verify collection exists
|
||||||
|
const collection = await client.query(
|
||||||
|
`SELECT id, "teamId" FROM collections WHERE id = $1 AND "deletedAt" IS NULL`,
|
||||||
|
[args.collection_id]
|
||||||
|
);
|
||||||
|
if (collection.rows.length === 0) throw new Error('Collection not found');
|
||||||
|
|
||||||
// Add project header if no template
|
const teamId = collection.rows[0].teamId;
|
||||||
if (!args.template_id) {
|
|
||||||
content = `## Informações do Projecto\n\n`;
|
// Get admin user
|
||||||
content += `| Campo | Valor |\n`;
|
const userResult = await client.query(
|
||||||
content += `|-------|-------|\n`;
|
`SELECT id FROM users WHERE role = 'admin' AND "deletedAt" IS NULL LIMIT 1`
|
||||||
content += `| **ID Desk** | #${args.desk_project_id} |\n`;
|
);
|
||||||
content += `| **Nome** | ${projectName} |\n`;
|
if (userResult.rows.length === 0) throw new Error('No admin user found');
|
||||||
if (customerName) {
|
const userId = userResult.rows[0].id;
|
||||||
content += `| **Cliente** | ${customerName} |\n`;
|
|
||||||
|
// Get template content if specified
|
||||||
|
let baseContent = '';
|
||||||
|
if (args.template_id) {
|
||||||
|
const template = await client.query(
|
||||||
|
`SELECT text FROM documents WHERE id = $1 AND template = true AND "deletedAt" IS NULL`,
|
||||||
|
[args.template_id]
|
||||||
|
);
|
||||||
|
if (template.rows.length > 0) {
|
||||||
|
baseContent = template.rows[0].text || '';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
content += `| **Criado em** | ${new Date().toISOString().split('T')[0]} |\n`;
|
|
||||||
content += `\n`;
|
|
||||||
|
|
||||||
if (args.desk_project_description) {
|
// Build document content
|
||||||
content += `## Descrição\n\n${sanitizeInput(args.desk_project_description)}\n\n`;
|
let content = baseContent || '';
|
||||||
|
|
||||||
|
// Add project header if no template
|
||||||
|
if (!args.template_id) {
|
||||||
|
content = `## Informações do Projecto\n\n`;
|
||||||
|
content += `| Campo | Valor |\n`;
|
||||||
|
content += `|-------|-------|\n`;
|
||||||
|
content += `| **ID Desk** | #${args.desk_project_id} |\n`;
|
||||||
|
content += `| **Nome** | ${projectName} |\n`;
|
||||||
|
if (customerName) {
|
||||||
|
content += `| **Cliente** | ${customerName} |\n`;
|
||||||
|
}
|
||||||
|
content += `| **Criado em** | ${new Date().toISOString().split('T')[0]} |\n`;
|
||||||
|
content += `\n`;
|
||||||
|
|
||||||
|
if (args.desk_project_description) {
|
||||||
|
content += `## Descrição\n\n${sanitizeInput(args.desk_project_description)}\n\n`;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// Add tasks section
|
// Add tasks section
|
||||||
if (includeTasks && args.tasks && args.tasks.length > 0) {
|
if (includeTasks && args.tasks && args.tasks.length > 0) {
|
||||||
content += `## Tarefas\n\n`;
|
content += `## Tarefas\n\n`;
|
||||||
content += `| ID | Tarefa | Estado | Responsável |\n`;
|
content += `| ID | Tarefa | Estado | Responsável |\n`;
|
||||||
content += `|----|--------|--------|-------------|\n`;
|
content += `|----|--------|--------|-------------|\n`;
|
||||||
|
|
||||||
for (const task of args.tasks) {
|
for (const task of args.tasks) {
|
||||||
const assignees = task.assignees?.join(', ') || '-';
|
const assignees = task.assignees?.join(', ') || '-';
|
||||||
const statusEmoji = task.status === 'complete' ? '✅' : task.status === 'in_progress' ? '🔄' : '⬜';
|
const statusEmoji = task.status === 'complete' ? '✅' : task.status === 'in_progress' ? '🔄' : '⬜';
|
||||||
content += `| #${task.id} | ${sanitizeInput(task.name)} | ${statusEmoji} ${task.status} | ${assignees} |\n`;
|
content += `| #${task.id} | ${sanitizeInput(task.name)} | ${statusEmoji} ${task.status} | ${assignees} |\n`;
|
||||||
|
}
|
||||||
|
content += `\n`;
|
||||||
}
|
}
|
||||||
content += `\n`;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add sync metadata section
|
// Add sync metadata section
|
||||||
content += `---\n\n`;
|
content += `---\n\n`;
|
||||||
content += `> **Desk Sync:** Este documento está vinculado ao projecto Desk #${args.desk_project_id}\n`;
|
content += `> **Desk Sync:** Este documento está vinculado ao projecto Desk #${args.desk_project_id}\n`;
|
||||||
content += `> Última sincronização: ${new Date().toISOString()}\n`;
|
content += `> Última sincronização: ${new Date().toISOString()}\n`;
|
||||||
|
|
||||||
// Create document
|
// Create document
|
||||||
const result = await pgClient.query(`
|
const result = await client.query(`
|
||||||
INSERT INTO documents (
|
INSERT INTO documents (
|
||||||
id, title, text, emoji, "collectionId", "teamId",
|
id, title, text, emoji, "collectionId", "teamId",
|
||||||
"createdById", "lastModifiedById", template,
|
"createdById", "lastModifiedById", template,
|
||||||
"createdAt", "updatedAt"
|
"createdAt", "updatedAt"
|
||||||
)
|
)
|
||||||
VALUES (
|
VALUES (
|
||||||
gen_random_uuid(), $1, $2, '📋', $3, $4, $5, $5, false, NOW(), NOW()
|
gen_random_uuid(), $1, $2, '📋', $3, $4, $5, $5, false, NOW(), NOW()
|
||||||
)
|
)
|
||||||
RETURNING id, title, "createdAt"
|
RETURNING id, title, "createdAt"
|
||||||
`, [
|
`, [
|
||||||
projectName,
|
projectName,
|
||||||
content,
|
content,
|
||||||
args.collection_id,
|
args.collection_id,
|
||||||
teamId,
|
teamId,
|
||||||
userId,
|
userId,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const newDoc = result.rows[0];
|
const doc = result.rows[0];
|
||||||
|
|
||||||
// Store Desk reference in document metadata (using a comment as metadata storage)
|
// Store Desk reference in document metadata (using a comment as metadata storage)
|
||||||
await pgClient.query(`
|
await client.query(`
|
||||||
INSERT INTO comments (id, "documentId", "createdById", data, "createdAt", "updatedAt")
|
INSERT INTO comments (id, "documentId", "createdById", data, "createdAt", "updatedAt")
|
||||||
VALUES (gen_random_uuid(), $1, $2, $3, NOW(), NOW())
|
VALUES (gen_random_uuid(), $1, $2, $3, NOW(), NOW())
|
||||||
`, [
|
`, [
|
||||||
newDoc.id,
|
doc.id,
|
||||||
userId,
|
userId,
|
||||||
JSON.stringify({
|
JSON.stringify({
|
||||||
type: 'desk_sync_metadata',
|
type: 'desk_sync_metadata',
|
||||||
desk_project_id: args.desk_project_id,
|
desk_project_id: args.desk_project_id,
|
||||||
desk_customer_name: customerName,
|
desk_customer_name: customerName,
|
||||||
synced_at: new Date().toISOString(),
|
synced_at: new Date().toISOString(),
|
||||||
}),
|
}),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
return doc;
|
||||||
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
content: [{ type: 'text', text: JSON.stringify({
|
content: [{ type: 'text', text: JSON.stringify({
|
||||||
@@ -217,37 +240,65 @@ const linkDeskTask: BaseTool<LinkDeskTaskArgs> = {
|
|||||||
handler: async (args, pgClient): Promise<ToolResponse> => {
|
handler: async (args, pgClient): Promise<ToolResponse> => {
|
||||||
if (!isValidUUID(args.document_id)) throw new Error('Invalid document_id');
|
if (!isValidUUID(args.document_id)) throw new Error('Invalid document_id');
|
||||||
|
|
||||||
// Verify document exists
|
|
||||||
const document = await pgClient.query(
|
|
||||||
`SELECT id, title, text FROM documents WHERE id = $1 AND "deletedAt" IS NULL`,
|
|
||||||
[args.document_id]
|
|
||||||
);
|
|
||||||
if (document.rows.length === 0) throw new Error('Document not found');
|
|
||||||
|
|
||||||
const doc = document.rows[0];
|
|
||||||
const linkType = args.link_type || 'reference';
|
const linkType = args.link_type || 'reference';
|
||||||
const taskName = sanitizeInput(args.desk_task_name);
|
const taskName = sanitizeInput(args.desk_task_name);
|
||||||
|
|
||||||
// Get admin user
|
// Use transaction for atomic operation
|
||||||
const userResult = await pgClient.query(
|
const result = await withTransaction(pgClient, async (client) => {
|
||||||
`SELECT id FROM users WHERE role = 'admin' AND "deletedAt" IS NULL LIMIT 1`
|
// Verify document exists
|
||||||
);
|
const document = await client.query(
|
||||||
const userId = userResult.rows.length > 0 ? userResult.rows[0].id : null;
|
`SELECT id, title, text FROM documents WHERE id = $1 AND "deletedAt" IS NULL`,
|
||||||
|
[args.document_id]
|
||||||
|
);
|
||||||
|
if (document.rows.length === 0) throw new Error('Document not found');
|
||||||
|
|
||||||
// Check if link already exists (search in comments)
|
const doc = document.rows[0];
|
||||||
const existingLink = await pgClient.query(`
|
|
||||||
SELECT id FROM comments
|
|
||||||
WHERE "documentId" = $1
|
|
||||||
AND data::text LIKE $2
|
|
||||||
`, [args.document_id, `%"desk_task_id":${args.desk_task_id}%`]);
|
|
||||||
|
|
||||||
if (existingLink.rows.length > 0) {
|
// Get admin user
|
||||||
// Update existing link
|
const userResult = await client.query(
|
||||||
await pgClient.query(`
|
`SELECT id FROM users WHERE role = 'admin' AND "deletedAt" IS NULL LIMIT 1`
|
||||||
UPDATE comments
|
);
|
||||||
SET data = $1, "updatedAt" = NOW()
|
const userId = userResult.rows.length > 0 ? userResult.rows[0].id : null;
|
||||||
WHERE id = $2
|
|
||||||
|
// Check if link already exists (search in comments)
|
||||||
|
const existingLink = await client.query(`
|
||||||
|
SELECT id FROM comments
|
||||||
|
WHERE "documentId" = $1
|
||||||
|
AND data::text LIKE $2
|
||||||
|
`, [args.document_id, `%"desk_task_id":${args.desk_task_id}%`]);
|
||||||
|
|
||||||
|
if (existingLink.rows.length > 0) {
|
||||||
|
// Update existing link
|
||||||
|
await client.query(`
|
||||||
|
UPDATE comments
|
||||||
|
SET data = $1, "updatedAt" = NOW()
|
||||||
|
WHERE id = $2
|
||||||
|
`, [
|
||||||
|
JSON.stringify({
|
||||||
|
type: 'desk_task_link',
|
||||||
|
desk_task_id: args.desk_task_id,
|
||||||
|
desk_task_name: taskName,
|
||||||
|
desk_project_id: args.desk_project_id || null,
|
||||||
|
link_type: linkType,
|
||||||
|
sync_status: args.sync_status || false,
|
||||||
|
updated_at: new Date().toISOString(),
|
||||||
|
}),
|
||||||
|
existingLink.rows[0].id,
|
||||||
|
]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
action: 'updated',
|
||||||
|
doc,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create new link
|
||||||
|
await client.query(`
|
||||||
|
INSERT INTO comments (id, "documentId", "createdById", data, "createdAt", "updatedAt")
|
||||||
|
VALUES (gen_random_uuid(), $1, $2, $3, NOW(), NOW())
|
||||||
`, [
|
`, [
|
||||||
|
args.document_id,
|
||||||
|
userId,
|
||||||
JSON.stringify({
|
JSON.stringify({
|
||||||
type: 'desk_task_link',
|
type: 'desk_task_link',
|
||||||
desk_task_id: args.desk_task_id,
|
desk_task_id: args.desk_task_id,
|
||||||
@@ -255,65 +306,35 @@ const linkDeskTask: BaseTool<LinkDeskTaskArgs> = {
|
|||||||
desk_project_id: args.desk_project_id || null,
|
desk_project_id: args.desk_project_id || null,
|
||||||
link_type: linkType,
|
link_type: linkType,
|
||||||
sync_status: args.sync_status || false,
|
sync_status: args.sync_status || false,
|
||||||
updated_at: new Date().toISOString(),
|
created_at: new Date().toISOString(),
|
||||||
}),
|
}),
|
||||||
existingLink.rows[0].id,
|
|
||||||
]);
|
]);
|
||||||
|
|
||||||
return {
|
// Optionally append reference to document text
|
||||||
content: [{ type: 'text', text: JSON.stringify({
|
if (linkType === 'reference') {
|
||||||
action: 'updated',
|
const refText = `\n\n---\n> 🔗 **Tarefa Desk:** #${args.desk_task_id} - ${taskName}`;
|
||||||
documentId: args.document_id,
|
|
||||||
documentTitle: doc.title,
|
|
||||||
deskTask: {
|
|
||||||
id: args.desk_task_id,
|
|
||||||
name: taskName,
|
|
||||||
projectId: args.desk_project_id,
|
|
||||||
},
|
|
||||||
linkType,
|
|
||||||
syncStatus: args.sync_status || false,
|
|
||||||
message: `Updated link to Desk task #${args.desk_task_id}`,
|
|
||||||
}, null, 2) }],
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create new link
|
// Only append if not already present
|
||||||
await pgClient.query(`
|
if (!doc.text?.includes(`#${args.desk_task_id}`)) {
|
||||||
INSERT INTO comments (id, "documentId", "createdById", data, "createdAt", "updatedAt")
|
await client.query(`
|
||||||
VALUES (gen_random_uuid(), $1, $2, $3, NOW(), NOW())
|
UPDATE documents
|
||||||
`, [
|
SET text = text || $1, "updatedAt" = NOW()
|
||||||
args.document_id,
|
WHERE id = $2
|
||||||
userId,
|
`, [refText, args.document_id]);
|
||||||
JSON.stringify({
|
}
|
||||||
type: 'desk_task_link',
|
|
||||||
desk_task_id: args.desk_task_id,
|
|
||||||
desk_task_name: taskName,
|
|
||||||
desk_project_id: args.desk_project_id || null,
|
|
||||||
link_type: linkType,
|
|
||||||
sync_status: args.sync_status || false,
|
|
||||||
created_at: new Date().toISOString(),
|
|
||||||
}),
|
|
||||||
]);
|
|
||||||
|
|
||||||
// Optionally append reference to document text
|
|
||||||
if (linkType === 'reference') {
|
|
||||||
const refText = `\n\n---\n> 🔗 **Tarefa Desk:** #${args.desk_task_id} - ${taskName}`;
|
|
||||||
|
|
||||||
// Only append if not already present
|
|
||||||
if (!doc.text?.includes(`#${args.desk_task_id}`)) {
|
|
||||||
await pgClient.query(`
|
|
||||||
UPDATE documents
|
|
||||||
SET text = text || $1, "updatedAt" = NOW()
|
|
||||||
WHERE id = $2
|
|
||||||
`, [refText, args.document_id]);
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
return {
|
||||||
|
action: 'created',
|
||||||
|
doc,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
content: [{ type: 'text', text: JSON.stringify({
|
content: [{ type: 'text', text: JSON.stringify({
|
||||||
action: 'created',
|
action: result.action,
|
||||||
documentId: args.document_id,
|
documentId: args.document_id,
|
||||||
documentTitle: doc.title,
|
documentTitle: result.doc.title,
|
||||||
deskTask: {
|
deskTask: {
|
||||||
id: args.desk_task_id,
|
id: args.desk_task_id,
|
||||||
name: taskName,
|
name: taskName,
|
||||||
@@ -321,7 +342,9 @@ const linkDeskTask: BaseTool<LinkDeskTaskArgs> = {
|
|||||||
},
|
},
|
||||||
linkType,
|
linkType,
|
||||||
syncStatus: args.sync_status || false,
|
syncStatus: args.sync_status || false,
|
||||||
message: `Linked Desk task #${args.desk_task_id} to document "${doc.title}"`,
|
message: result.action === 'updated'
|
||||||
|
? `Updated link to Desk task #${args.desk_task_id}`
|
||||||
|
: `Linked Desk task #${args.desk_task_id} to document "${result.doc.title}"`,
|
||||||
}, null, 2) }],
|
}, null, 2) }],
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -4,10 +4,28 @@
|
|||||||
* @author Descomplicar® | @link descomplicar.pt | @copyright 2026
|
* @author Descomplicar® | @link descomplicar.pt | @copyright 2026
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Pool } from 'pg';
|
import { Pool, PoolClient } from 'pg';
|
||||||
import { BaseTool, ToolResponse } from '../types/tools.js';
|
import { BaseTool, ToolResponse } from '../types/tools.js';
|
||||||
import { isValidUUID, sanitizeInput } from '../utils/security.js';
|
import { isValidUUID, sanitizeInput } from '../utils/security.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Execute operations within a transaction
|
||||||
|
*/
|
||||||
|
async function withTransaction<T>(pool: Pool, callback: (client: PoolClient) => Promise<T>): Promise<T> {
|
||||||
|
const client = await pool.connect();
|
||||||
|
try {
|
||||||
|
await client.query('BEGIN');
|
||||||
|
const result = await callback(client);
|
||||||
|
await client.query('COMMIT');
|
||||||
|
return result;
|
||||||
|
} catch (error) {
|
||||||
|
await client.query('ROLLBACK');
|
||||||
|
throw error;
|
||||||
|
} finally {
|
||||||
|
client.release();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
interface ExportCollectionArgs {
|
interface ExportCollectionArgs {
|
||||||
collection_id: string;
|
collection_id: string;
|
||||||
include_children?: boolean;
|
include_children?: boolean;
|
||||||
@@ -188,105 +206,110 @@ const importMarkdownFolder: BaseTool<ImportMarkdownArgs> = {
|
|||||||
|
|
||||||
const createHierarchy = args.create_hierarchy !== false;
|
const createHierarchy = args.create_hierarchy !== false;
|
||||||
|
|
||||||
// Verify collection exists
|
// Use transaction for atomic import (all documents or none)
|
||||||
const collection = await pgClient.query(
|
const { imported, errors } = await withTransaction(pgClient, async (client) => {
|
||||||
`SELECT id, "teamId" FROM collections WHERE id = $1 AND "deletedAt" IS NULL`,
|
// Verify collection exists
|
||||||
[args.collection_id]
|
const collection = await client.query(
|
||||||
);
|
`SELECT id, "teamId" FROM collections WHERE id = $1 AND "deletedAt" IS NULL`,
|
||||||
if (collection.rows.length === 0) throw new Error('Collection not found');
|
[args.collection_id]
|
||||||
|
);
|
||||||
|
if (collection.rows.length === 0) throw new Error('Collection not found');
|
||||||
|
|
||||||
const teamId = collection.rows[0].teamId;
|
const teamId = collection.rows[0].teamId;
|
||||||
|
|
||||||
// Get admin user for createdById
|
// Get admin user for createdById
|
||||||
const userResult = await pgClient.query(
|
const userResult = await client.query(
|
||||||
`SELECT id FROM users WHERE role = 'admin' AND "deletedAt" IS NULL LIMIT 1`
|
`SELECT id FROM users WHERE role = 'admin' AND "deletedAt" IS NULL LIMIT 1`
|
||||||
);
|
);
|
||||||
if (userResult.rows.length === 0) throw new Error('No admin user found');
|
if (userResult.rows.length === 0) throw new Error('No admin user found');
|
||||||
const userId = userResult.rows[0].id;
|
const userId = userResult.rows[0].id;
|
||||||
|
|
||||||
const imported: Array<{ id: string; title: string; path: string }> = [];
|
const importedList: Array<{ id: string; title: string; path: string }> = [];
|
||||||
const errors: Array<{ title: string; error: string }> = [];
|
const errorList: Array<{ title: string; error: string }> = [];
|
||||||
const pathToId: Record<string, string> = {};
|
const pathToId: Record<string, string> = {};
|
||||||
|
|
||||||
// First pass: create all documents (sorted by path depth)
|
// First pass: create all documents (sorted by path depth)
|
||||||
const sortedDocs = [...args.documents].sort((a, b) => {
|
const sortedDocs = [...args.documents].sort((a, b) => {
|
||||||
const depthA = (a.parent_path || '').split('/').filter(Boolean).length;
|
const depthA = (a.parent_path || '').split('/').filter(Boolean).length;
|
||||||
const depthB = (b.parent_path || '').split('/').filter(Boolean).length;
|
const depthB = (b.parent_path || '').split('/').filter(Boolean).length;
|
||||||
return depthA - depthB;
|
return depthA - depthB;
|
||||||
});
|
});
|
||||||
|
|
||||||
for (const doc of sortedDocs) {
|
for (const doc of sortedDocs) {
|
||||||
try {
|
try {
|
||||||
let parentDocumentId: string | null = null;
|
let parentDocumentId: string | null = null;
|
||||||
|
|
||||||
// Resolve parent if specified
|
// Resolve parent if specified
|
||||||
if (doc.parent_path && createHierarchy) {
|
if (doc.parent_path && createHierarchy) {
|
||||||
const parentPath = doc.parent_path.trim();
|
const parentPath = doc.parent_path.trim();
|
||||||
|
|
||||||
if (pathToId[parentPath]) {
|
if (pathToId[parentPath]) {
|
||||||
parentDocumentId = pathToId[parentPath];
|
parentDocumentId = pathToId[parentPath];
|
||||||
} else {
|
} else {
|
||||||
// Try to find existing parent by title
|
// Try to find existing parent by title
|
||||||
const parentTitle = parentPath.split('/').pop();
|
const parentTitle = parentPath.split('/').pop();
|
||||||
const existingParent = await pgClient.query(
|
const existingParent = await client.query(
|
||||||
`SELECT id FROM documents WHERE title = $1 AND "collectionId" = $2 AND "deletedAt" IS NULL LIMIT 1`,
|
`SELECT id FROM documents WHERE title = $1 AND "collectionId" = $2 AND "deletedAt" IS NULL LIMIT 1`,
|
||||||
[parentTitle, args.collection_id]
|
[parentTitle, args.collection_id]
|
||||||
);
|
);
|
||||||
|
|
||||||
if (existingParent.rows.length > 0) {
|
if (existingParent.rows.length > 0) {
|
||||||
parentDocumentId = existingParent.rows[0].id;
|
parentDocumentId = existingParent.rows[0].id;
|
||||||
if (parentDocumentId) {
|
if (parentDocumentId) {
|
||||||
pathToId[parentPath] = parentDocumentId;
|
pathToId[parentPath] = parentDocumentId;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// Strip YAML frontmatter if present
|
// Strip YAML frontmatter if present
|
||||||
let content = doc.content;
|
let content = doc.content;
|
||||||
if (content.startsWith('---')) {
|
if (content.startsWith('---')) {
|
||||||
const endOfFrontmatter = content.indexOf('---', 3);
|
const endOfFrontmatter = content.indexOf('---', 3);
|
||||||
if (endOfFrontmatter !== -1) {
|
if (endOfFrontmatter !== -1) {
|
||||||
content = content.substring(endOfFrontmatter + 3).trim();
|
content = content.substring(endOfFrontmatter + 3).trim();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Create document
|
||||||
|
const result = await client.query(`
|
||||||
|
INSERT INTO documents (
|
||||||
|
id, title, text, emoji, "collectionId", "teamId", "parentDocumentId",
|
||||||
|
"createdById", "lastModifiedById", template, "createdAt", "updatedAt"
|
||||||
|
)
|
||||||
|
VALUES (
|
||||||
|
gen_random_uuid(), $1, $2, $3, $4, $5, $6, $7, $7, false, NOW(), NOW()
|
||||||
|
)
|
||||||
|
RETURNING id, title
|
||||||
|
`, [
|
||||||
|
sanitizeInput(doc.title),
|
||||||
|
content,
|
||||||
|
doc.emoji || null,
|
||||||
|
args.collection_id,
|
||||||
|
teamId,
|
||||||
|
parentDocumentId,
|
||||||
|
userId,
|
||||||
|
]);
|
||||||
|
|
||||||
|
const newDoc = result.rows[0];
|
||||||
|
const fullPath = doc.parent_path ? `${doc.parent_path}/${doc.title}` : doc.title;
|
||||||
|
pathToId[fullPath] = newDoc.id;
|
||||||
|
|
||||||
|
importedList.push({
|
||||||
|
id: newDoc.id,
|
||||||
|
title: newDoc.title,
|
||||||
|
path: fullPath,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
errorList.push({
|
||||||
|
title: doc.title,
|
||||||
|
error: error instanceof Error ? error.message : String(error),
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create document
|
|
||||||
const result = await pgClient.query(`
|
|
||||||
INSERT INTO documents (
|
|
||||||
id, title, text, emoji, "collectionId", "teamId", "parentDocumentId",
|
|
||||||
"createdById", "lastModifiedById", template, "createdAt", "updatedAt"
|
|
||||||
)
|
|
||||||
VALUES (
|
|
||||||
gen_random_uuid(), $1, $2, $3, $4, $5, $6, $7, $7, false, NOW(), NOW()
|
|
||||||
)
|
|
||||||
RETURNING id, title
|
|
||||||
`, [
|
|
||||||
sanitizeInput(doc.title),
|
|
||||||
content,
|
|
||||||
doc.emoji || null,
|
|
||||||
args.collection_id,
|
|
||||||
teamId,
|
|
||||||
parentDocumentId,
|
|
||||||
userId,
|
|
||||||
]);
|
|
||||||
|
|
||||||
const newDoc = result.rows[0];
|
|
||||||
const fullPath = doc.parent_path ? `${doc.parent_path}/${doc.title}` : doc.title;
|
|
||||||
pathToId[fullPath] = newDoc.id;
|
|
||||||
|
|
||||||
imported.push({
|
|
||||||
id: newDoc.id,
|
|
||||||
title: newDoc.title,
|
|
||||||
path: fullPath,
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
errors.push({
|
|
||||||
title: doc.title,
|
|
||||||
error: error instanceof Error ? error.message : String(error),
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
return { imported: importedList, errors: errorList };
|
||||||
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
content: [{ type: 'text', text: JSON.stringify({
|
content: [{ type: 'text', text: JSON.stringify({
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
|
|
||||||
import { Pool } from 'pg';
|
import { Pool } from 'pg';
|
||||||
import { BaseTool, ToolResponse, PaginationArgs } from '../types/tools.js';
|
import { BaseTool, ToolResponse, PaginationArgs } from '../types/tools.js';
|
||||||
import { validatePagination, isValidUUID, sanitizeInput } from '../utils/security.js';
|
import { validatePagination, isValidUUID, sanitizeInput, validateDaysInterval } from '../utils/security.js';
|
||||||
|
|
||||||
interface SearchQueryListArgs extends PaginationArgs {
|
interface SearchQueryListArgs extends PaginationArgs {
|
||||||
user_id?: string;
|
user_id?: string;
|
||||||
@@ -137,8 +137,10 @@ const getSearchQueryStats: BaseTool<SearchQueryStatsArgs> = {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
handler: async (args, pgClient): Promise<ToolResponse> => {
|
handler: async (args, pgClient): Promise<ToolResponse> => {
|
||||||
const days = args.days || 30;
|
// Validate and sanitize days parameter
|
||||||
const conditions: string[] = [`sq."createdAt" > NOW() - INTERVAL '${days} days'`];
|
const safeDays = validateDaysInterval(args.days, 30, 365);
|
||||||
|
|
||||||
|
const conditions: string[] = [`sq."createdAt" > NOW() - make_interval(days => ${safeDays})`];
|
||||||
const params: any[] = [];
|
const params: any[] = [];
|
||||||
let paramIndex = 1;
|
let paramIndex = 1;
|
||||||
|
|
||||||
@@ -219,7 +221,7 @@ const getSearchQueryStats: BaseTool<SearchQueryStatsArgs> = {
|
|||||||
${whereClause}
|
${whereClause}
|
||||||
GROUP BY DATE(sq."createdAt")
|
GROUP BY DATE(sq."createdAt")
|
||||||
ORDER BY date DESC
|
ORDER BY date DESC
|
||||||
LIMIT ${days}
|
LIMIT ${safeDays}
|
||||||
`,
|
`,
|
||||||
params
|
params
|
||||||
);
|
);
|
||||||
@@ -228,7 +230,7 @@ const getSearchQueryStats: BaseTool<SearchQueryStatsArgs> = {
|
|||||||
content: [{
|
content: [{
|
||||||
type: 'text',
|
type: 'text',
|
||||||
text: JSON.stringify({
|
text: JSON.stringify({
|
||||||
period: `Last ${days} days`,
|
period: `Last ${safeDays} days`,
|
||||||
overall: overallStats.rows[0],
|
overall: overallStats.rows[0],
|
||||||
popularSearches: popularSearches.rows,
|
popularSearches: popularSearches.rows,
|
||||||
zeroResultSearches: zeroResultSearches.rows,
|
zeroResultSearches: zeroResultSearches.rows,
|
||||||
|
|||||||
@@ -113,3 +113,50 @@ export function validateSortField(field: string | undefined, allowedFields: stri
|
|||||||
if (!field) return defaultField;
|
if (!field) return defaultField;
|
||||||
return allowedFields.includes(field) ? field : defaultField;
|
return allowedFields.includes(field) ? field : defaultField;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate and sanitize days interval for SQL INTERVAL
|
||||||
|
* Prevents SQL injection by ensuring the value is a safe integer
|
||||||
|
*/
|
||||||
|
export function validateDaysInterval(days: unknown, defaultDays = 30, maxDays = 365): number {
|
||||||
|
const parsed = parseInt(String(days), 10);
|
||||||
|
if (isNaN(parsed) || parsed < 1) return defaultDays;
|
||||||
|
return Math.min(parsed, maxDays);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate ISO date format
|
||||||
|
* Accepts: YYYY-MM-DD or YYYY-MM-DDTHH:mm:ss(.sss)?Z?
|
||||||
|
*/
|
||||||
|
export function isValidISODate(date: string): boolean {
|
||||||
|
const isoRegex = /^\d{4}-\d{2}-\d{2}(T\d{2}:\d{2}:\d{2}(\.\d{3})?Z?)?$/;
|
||||||
|
if (!isoRegex.test(date)) return false;
|
||||||
|
const d = new Date(date);
|
||||||
|
return !isNaN(d.getTime());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate period parameter against allowed values
|
||||||
|
*/
|
||||||
|
export function validatePeriod(period: string | undefined, allowedPeriods: string[], defaultPeriod: string): string {
|
||||||
|
if (!period) return defaultPeriod;
|
||||||
|
return allowedPeriods.includes(period) ? period : defaultPeriod;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Rate limit store cleanup interval (5 minutes)
|
||||||
|
const RATE_LIMIT_CLEANUP_INTERVAL = 300000;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clean up expired rate limit entries
|
||||||
|
*/
|
||||||
|
function cleanupRateLimitStore(): void {
|
||||||
|
const now = Date.now();
|
||||||
|
for (const [key, entry] of rateLimitStore.entries()) {
|
||||||
|
if (now > entry.resetAt) {
|
||||||
|
rateLimitStore.delete(key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start cleanup interval
|
||||||
|
setInterval(cleanupRateLimitStore, RATE_LIMIT_CLEANUP_INTERVAL);
|
||||||
|
|||||||
Reference in New Issue
Block a user