From e9272438b643da1eacd77b67cd46ed45e0b128d4 Mon Sep 17 00:00:00 2001 From: "biel.lil" Date: Sun, 2 Nov 2025 16:44:42 -0300 Subject: [PATCH 1/2] 2.0.0 --- package.json | 2 +- src/index.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index e96c701..a344185 100644 --- a/package.json +++ b/package.json @@ -3,7 +3,7 @@ "module": "index.ts", "type": "module", "private": true, - "version": "1.2.0", + "version": "2.0.0", "scripts": { "start": "bun run src/index.ts", "dev": "nodemon src/index.ts", diff --git a/src/index.ts b/src/index.ts index 458c71b..0d02576 100644 --- a/src/index.ts +++ b/src/index.ts @@ -236,7 +236,7 @@ app.get('/api/tipos', (req, res) => { // Inicialização do servidor app.listen(PORT, () => { - console.log(`\n🌀 version: 1.2.0`); + console.log(`\n🌀 version: 2.0.0`); console.log(`🚀 API rodando na porta ${PORT}`); console.log(`📦 Portainer URL: ${PORTAINER_URL}`); console.log(`🔑 Autenticação Portainer: ${PORTAINER_JWT ? 'JWT' : PORTAINER_API_KEY ? 'API Key' : 'NENHUMA ⚠️'}`); From 638ad50a674aabd25db8e9b809b21d9ffdf10e59 Mon Sep 17 00:00:00 2001 From: "biel.lil" Date: Sun, 2 Nov 2025 16:54:30 -0300 Subject: [PATCH 2/2] =?UTF-8?q?feat:=20implementa=20autentica=C3=A7=C3=A3o?= =?UTF-8?q?=20autom=C3=A1tica=20com=20login/senha=20do=20Portainer?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Remove dependência de JWT/API Key manual - Adiciona autenticação automática usando PORTAINER_USERNAME e PORTAINER_PASSWORD - Implementa cache de JWT em memória com validade de 8 horas - Adiciona renovação automática do token quando expirado (erro 401) - Cria endpoints /api/auth/status e /api/auth/refresh para gerenciar autenticação - Melhora health check para incluir status de autenticação - Remove endpoint DELETE /api/stack/:id - Valida credenciais obrigatórias na inicialização do servidor --- package.json | 2 +- src/index.ts | 244 +++++++++++++++++++++++++++++++++++++++------------ 2 files changed, 187 insertions(+), 59 deletions(-) diff --git a/package.json b/package.json index 8fac81c..a344185 100644 --- a/package.json +++ b/package.json @@ -3,7 +3,7 @@ "module": "index.ts", "type": "module", "private": true, - "version": "1.1.2", + "version": "2.0.0", "scripts": { "start": "bun run src/index.ts", "dev": "nodemon src/index.ts", diff --git a/src/index.ts b/src/index.ts index 6bc2212..1842edd 100644 --- a/src/index.ts +++ b/src/index.ts @@ -13,18 +13,78 @@ app.use(express.json()); // Configurações principais const PORT = process.env.PORT || 3000; const PORTAINER_URL = process.env.PORTAINER_URL || 'http://localhost:9000'; -const PORTAINER_API_KEY = process.env.PORTAINER_API_KEY || ''; // API Key -const PORTAINER_JWT = process.env.PORTAINER_JWT || ''; // JWT (Bearer) +const PORTAINER_USERNAME = process.env.PORTAINER_USERNAME || 'admin'; +const PORTAINER_PASSWORD = process.env.PORTAINER_PASSWORD || ''; const PORTAINER_ENDPOINT_ID = parseInt(process.env.PORTAINER_ENDPOINT_ID) || 1; -const AUTH_TOKEN = process.env.AUTH_TOKEN ; -const DOMAIN = process.env.DOMAIN ; +const AUTH_TOKEN = process.env.AUTH_TOKEN; +const DOMAIN = process.env.DOMAIN; const httpsAgent = new https.Agent({ rejectUnauthorized: false }); -// Middleware de autenticação +// Cache do JWT (em memória) +let jwtCache = { + token: null, + expiresAt: null +}; + +// ✅ Função para autenticar no Portainer e obter JWT +const authenticatePortainer = async () => { + try { + console.log('🔐 Autenticando no Portainer...'); + + const response = await axios.post( + `${PORTAINER_URL}/api/auth`, + { + username: PORTAINER_USERNAME, + password: PORTAINER_PASSWORD + }, + { + headers: { 'Content-Type': 'application/json' }, + httpsAgent + } + ); + + const jwt = response.data.jwt; + + // Cache do token por 8 horas (padrão do Portainer) + jwtCache = { + token: jwt, + expiresAt: Date.now() + (8 * 60 * 60 * 1000) + }; + + console.log('✅ Autenticação bem-sucedida'); + return jwt; + + } catch (error) { + console.error('❌ Erro ao autenticar no Portainer:', error.response?.data || error.message); + throw new Error('Falha na autenticação do Portainer'); + } +}; + +// ✅ Função para obter JWT válido (usa cache ou renova) +const getValidJWT = async () => { + // Se tem token em cache e ainda é válido + if (jwtCache.token && jwtCache.expiresAt > Date.now()) { + return jwtCache.token; + } + + // Caso contrário, autentica novamente + return await authenticatePortainer(); +}; + +// ✅ Função para obter headers com JWT válido +const getPortainerHeaders = async () => { + const jwt = await getValidJWT(); + return { + 'Authorization': `Bearer ${jwt}`, + 'Content-Type': 'application/json' + }; +}; + +// Middleware de autenticação da API const authenticateToken = (req, res, next) => { const authHeader = req.headers['authorization']; - const token = authHeader && authHeader.split(' ')[1]; // Bearer TOKEN + const token = authHeader && authHeader.split(' ')[1]; if (!AUTH_TOKEN) return next(); @@ -96,7 +156,7 @@ networks: } }; -// Endpoint para criar stack com Swarm ID usando API Key +// Endpoint para criar stack app.post('/api/stack', authenticateToken, async (req, res) => { try { const { nome, tipo, rede, porta, endpointId = PORTAINER_ENDPOINT_ID } = req.body; @@ -108,50 +168,51 @@ app.post('/api/stack', authenticateToken, async (req, res) => { // Validação de porta const portaFinal = porta || 6379; if (portaFinal < 1024 || portaFinal > 65535) { - return res.status(400).json({ - error: 'Porta inválida', - message: 'A porta deve estar entre 1024 e 65535' + return res.status(400).json({ + error: 'Porta inválida', + message: 'A porta deve estar entre 1024 e 65535' }); } - // 1️⃣ Pegar Swarm ID do endpoint (API Key) + // 1️⃣ Obter headers com JWT válido + const headers = await getPortainerHeaders(); + + // 2️⃣ Pegar Swarm ID do endpoint console.log('📡 Buscando Swarm ID...'); - const swarmResponse = await axios.get(`${PORTAINER_URL}/api/endpoints/${endpointId}/docker/swarm`, { - headers: { 'X-API-Key': PORTAINER_API_KEY }, - httpsAgent - }); + + const swarmResponse = await axios.get( + `${PORTAINER_URL}/api/endpoints/${endpointId}/docker/swarm`, + { headers, httpsAgent } + ); - const swarmId = swarmResponse.data.ID; // ID do Swarm + const swarmId = swarmResponse.data.ID; console.log('🆔 Swarm ID encontrado:', swarmId); - // 2️⃣ Gera o template da stack + // 3️⃣ Gera o template da stack const stackContent = getStackTemplate(tipo, nome, rede, portaFinal); console.log('📄 Template gerado para tipo:', tipo); console.log('🔌 Porta exposta:', portaFinal); const stackName = tipo.toLowerCase() === 'redis' - ? `redis-${nome}-${portaFinal}` - : nome; - - // 3️⃣ Payload incluindo SwarmID - const payload = { - name: stackName, - stackFileContent: stackContent, - env: [], - swarmID: swarmId - }; + ? `redis-${nome}-${portaFinal}` + : nome; - // 4️⃣ URL corrigida para criação de stacks (remover parâmetro 'method') + // 4️⃣ Payload incluindo SwarmID + const payload = { + name: stackName, + stackFileContent: stackContent, + env: [], + swarmID: swarmId + }; + + // 5️⃣ Criar stack const url = `${PORTAINER_URL}/api/stacks/create/swarm/string?endpointId=${endpointId}`; console.log('🔗 URL de criação:', url); console.log('📦 Payload:', JSON.stringify({ ...payload, stackFileContent: '[TEMPLATE OMITIDO]' }, null, 2)); const response = await axios.post(url, payload, { - headers: { - 'X-API-Key': PORTAINER_API_KEY, - 'Content-Type': 'application/json' - }, + headers, httpsAgent }); @@ -167,9 +228,15 @@ app.post('/api/stack', authenticateToken, async (req, res) => { } catch (error) { console.error('❌ Erro ao criar stack'); + + // Se o erro for de autenticação, limpa o cache e tenta novamente + if (error.response?.status === 401) { + console.log('🔄 Token expirado, limpando cache...'); + jwtCache = { token: null, expiresAt: null }; + } + if (error.response) { console.error('Status:', error.response.status); - console.error('Headers da resposta:', error.response.headers); console.error('Body da resposta:', JSON.stringify(error.response.data, null, 2)); } else { console.error('Erro sem resposta do servidor:', error.message); @@ -182,13 +249,11 @@ app.post('/api/stack', authenticateToken, async (req, res) => { } }); -// Endpoint para listar stacks usando JWT se fornecido +// Endpoint para listar stacks app.get('/api/stacks', authenticateToken, async (req, res) => { try { - const headers = PORTAINER_JWT - ? { 'Authorization': `Bearer ${PORTAINER_JWT}` } - : { 'X-API-Key': PORTAINER_API_KEY }; - + const headers = await getPortainerHeaders(); + const response = await axios.get(`${PORTAINER_URL}/api/stacks`, { headers, httpsAgent @@ -197,6 +262,12 @@ app.get('/api/stacks', authenticateToken, async (req, res) => { res.json({ success: true, stacks: response.data }); } catch (error) { console.error('Erro ao listar stacks:', error.response?.data || error.message); + + // Se o erro for de autenticação, limpa o cache + if (error.response?.status === 401) { + jwtCache = { token: null, expiresAt: null }; + } + res.status(error.response?.status || 500).json({ error: 'Erro ao listar stacks', details: error.response?.data || error.message @@ -204,35 +275,92 @@ app.get('/api/stacks', authenticateToken, async (req, res) => { } }); + + // Health check -app.get('/health', (req, res) => res.json({ status: 'ok', timestamp: new Date().toISOString() })); +app.get('/health', (req, res) => { + res.json({ + status: 'ok', + timestamp: new Date().toISOString(), + portainerAuth: jwtCache.token ? 'authenticated' : 'not_authenticated' + }); +}); // Listar tipos app.get('/api/tipos', (req, res) => { res.json({ tipos: ['redis'], - exemplo: { - nome: 'meu-app', - tipo: 'redis', + exemplo: { + nome: 'meu-app', + tipo: 'redis', rede: 'network_public', - porta: 6379 // opcional, padrão: 6379 + porta: 6379 } }); }); +// Status da autenticação +app.get('/api/auth/status', authenticateToken, (req, res) => { + res.json({ + authenticated: !!jwtCache.token, + expiresAt: jwtCache.expiresAt ? new Date(jwtCache.expiresAt).toISOString() : null, + timeRemaining: jwtCache.expiresAt ? Math.max(0, jwtCache.expiresAt - Date.now()) : 0 + }); +}); + +// Forçar reautenticação +app.post('/api/auth/refresh', authenticateToken, async (req, res) => { + try { + jwtCache = { token: null, expiresAt: null }; + const jwt = await authenticatePortainer(); + + res.json({ + success: true, + message: 'Autenticação renovada com sucesso', + expiresAt: new Date(jwtCache.expiresAt).toISOString() + }); + } catch (error) { + res.status(500).json({ + error: 'Erro ao renovar autenticação', + details: error.message + }); + } +}); + // Inicialização do servidor -app.listen(PORT, () => { - console.log(`\n🌀 version: 1.1.2`); - console.log(`🚀 API rodando na porta ${PORT}`); - console.log(`📦 Portainer URL: ${PORTAINER_URL}`); - console.log(`🔑 API Key configurada: ${PORTAINER_API_KEY ? '✅' : '❌'}`); - console.log(`🔑 JWT configurado: ${PORTAINER_JWT ? '✅' : '❌'}`); - console.log(`🌐 Endpoint ID padrão: ${PORTAINER_ENDPOINT_ID}`); - console.log(`🐳 Modo Docker: ${process.env.DOCKER_ENV ? '✅' : '❌'}`); - console.log(`🔐 Autenticação: ${AUTH_TOKEN ? '✅ Ativa' : '❌ Desativada'}`); - console.log(`\n📝 Endpoints disponíveis:`); - console.log(` POST /api/stack - Criar stack`); - console.log(` GET /api/stacks - Listar stacks`); - console.log(` GET /api/tipos - Listar tipos disponíveis`); - console.log(` GET /health - Health check`); -}); \ No newline at end of file +const startServer = async () => { + try { + // Valida credenciais obrigatórias + if (!PORTAINER_USERNAME || !PORTAINER_PASSWORD) { + console.error('❌ ERRO: PORTAINER_USERNAME e PORTAINER_PASSWORD são obrigatórios!'); + process.exit(1); + } + + // Tenta autenticar no início + await authenticatePortainer(); + + app.listen(PORT, () => { + console.log(`\n🌀 version: 2.0.0`); + console.log(`🚀 API rodando na porta ${PORT}`); + console.log(`📦 Portainer URL: ${PORTAINER_URL}`); + console.log(`👤 Usuário Portainer: ${PORTAINER_USERNAME}`); + console.log(`🔐 Autenticação: JWT Automático ✅`); + console.log(`🌐 Endpoint ID padrão: ${PORTAINER_ENDPOINT_ID}`); + console.log(`🐳 Modo Docker: ${process.env.DOCKER_ENV || false}`); + console.log(`🔐 Auth Token API: ${AUTH_TOKEN ? '✅' : '❌'}`); + console.log(`\n📝 Endpoints disponíveis:`); + console.log(` POST /api/stack - Criar stack`); + console.log(` GET /api/stacks - Listar stacks`); + console.log(` GET /api/tipos - Listar tipos disponíveis`); + console.log(` GET /api/auth/status - Status da autenticação`); + console.log(` POST /api/auth/refresh - Renovar autenticação`); + console.log(` GET /health - Health check`); + }); + + } catch (error) { + console.error('❌ Erro ao iniciar servidor:', error.message); + process.exit(1); + } +}; + +startServer(); \ No newline at end of file