Skip to content

Commit 171920b

Browse files
authored
Merge pull request #14 from Scripts-front/2.0.0
implementa autenticação automática com login/senha do Portainer
2 parents f74551f + 9c105ea commit 171920b

File tree

2 files changed

+153
-38
lines changed

2 files changed

+153
-38
lines changed

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
"module": "index.ts",
44
"type": "module",
55
"private": true,
6-
"version": "1.2.0",
6+
"version": "2.0.0",
77
"scripts": {
88
"start": "bun run src/index.ts",
99
"dev": "nodemon src/index.ts",

src/index.ts

Lines changed: 152 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -13,32 +13,74 @@ app.use(express.json());
1313
// Configurações principais
1414
const PORT = process.env.PORT || 3000;
1515
const PORTAINER_URL = process.env.PORTAINER_URL || 'http://localhost:9000';
16-
const PORTAINER_API_KEY = process.env.PORTAINER_API_KEY || '';
17-
const PORTAINER_JWT = process.env.PORTAINER_JWT || '';
16+
const PORTAINER_USERNAME = process.env.PORTAINER_USERNAME || 'admin';
17+
const PORTAINER_PASSWORD = process.env.PORTAINER_PASSWORD || '';
1818
const PORTAINER_ENDPOINT_ID = parseInt(process.env.PORTAINER_ENDPOINT_ID) || 1;
1919
const AUTH_TOKEN = process.env.AUTH_TOKEN;
2020
const DOMAIN = process.env.DOMAIN;
2121

2222
const httpsAgent = new https.Agent({ rejectUnauthorized: false });
2323

24-
// ✅ Função para obter headers de autenticação do Portainer
25-
const getPortainerHeaders = () => {
26-
// Prioriza JWT se disponível, senão usa API Key
27-
if (PORTAINER_JWT) {
28-
return {
29-
'Authorization': `Bearer ${PORTAINER_JWT}`,
30-
'Content-Type': 'application/json'
31-
};
32-
} else if (PORTAINER_API_KEY) {
33-
return {
34-
'X-API-Key': PORTAINER_API_KEY,
35-
'Content-Type': 'application/json'
24+
// Cache do JWT (em memória)
25+
let jwtCache = {
26+
token: null,
27+
expiresAt: null
28+
};
29+
30+
// ✅ Função para autenticar no Portainer e obter JWT
31+
const authenticatePortainer = async () => {
32+
try {
33+
console.log('🔐 Autenticando no Portainer...');
34+
35+
const response = await axios.post(
36+
`${PORTAINER_URL}/api/auth`,
37+
{
38+
username: PORTAINER_USERNAME,
39+
password: PORTAINER_PASSWORD
40+
},
41+
{
42+
headers: { 'Content-Type': 'application/json' },
43+
httpsAgent
44+
}
45+
);
46+
47+
const jwt = response.data.jwt;
48+
49+
// Cache do token por 8 horas (padrão do Portainer)
50+
jwtCache = {
51+
token: jwt,
52+
expiresAt: Date.now() + (8 * 60 * 60 * 1000)
3653
};
37-
} else {
38-
throw new Error('Nenhuma credencial do Portainer configurada (JWT ou API Key)');
54+
55+
console.log('✅ Autenticação bem-sucedida');
56+
return jwt;
57+
58+
} catch (error) {
59+
console.error('❌ Erro ao autenticar no Portainer:', error.response?.data || error.message);
60+
throw new Error('Falha na autenticação do Portainer');
3961
}
4062
};
4163

64+
// ✅ Função para obter JWT válido (usa cache ou renova)
65+
const getValidJWT = async () => {
66+
// Se tem token em cache e ainda é válido
67+
if (jwtCache.token && jwtCache.expiresAt > Date.now()) {
68+
return jwtCache.token;
69+
}
70+
71+
// Caso contrário, autentica novamente
72+
return await authenticatePortainer();
73+
};
74+
75+
// ✅ Função para obter headers com JWT válido
76+
const getPortainerHeaders = async () => {
77+
const jwt = await getValidJWT();
78+
return {
79+
'Authorization': `Bearer ${jwt}`,
80+
'Content-Type': 'application/json'
81+
};
82+
};
83+
4284
// Middleware de autenticação da API
4385
const authenticateToken = (req, res, next) => {
4486
const authHeader = req.headers['authorization'];
@@ -132,9 +174,11 @@ app.post('/api/stack', authenticateToken, async (req, res) => {
132174
});
133175
}
134176

135-
// 1️⃣ Pegar Swarm ID do endpoint
177+
// 1️⃣ Obter headers com JWT válido
178+
const headers = await getPortainerHeaders();
179+
180+
// 2️⃣ Pegar Swarm ID do endpoint
136181
console.log('📡 Buscando Swarm ID...');
137-
const headers = getPortainerHeaders();
138182

139183
const swarmResponse = await axios.get(
140184
`${PORTAINER_URL}/api/endpoints/${endpointId}/docker/swarm`,
@@ -144,7 +188,7 @@ app.post('/api/stack', authenticateToken, async (req, res) => {
144188
const swarmId = swarmResponse.data.ID;
145189
console.log('🆔 Swarm ID encontrado:', swarmId);
146190

147-
// 2️⃣ Gera o template da stack
191+
// 3️⃣ Gera o template da stack
148192
const stackContent = getStackTemplate(tipo, nome, rede, portaFinal);
149193
console.log('📄 Template gerado para tipo:', tipo);
150194
console.log('🔌 Porta exposta:', portaFinal);
@@ -153,15 +197,15 @@ app.post('/api/stack', authenticateToken, async (req, res) => {
153197
? `redis-${nome}-${portaFinal}`
154198
: nome;
155199

156-
// 3️⃣ Payload incluindo SwarmID
200+
// 4️⃣ Payload incluindo SwarmID
157201
const payload = {
158202
name: stackName,
159203
stackFileContent: stackContent,
160204
env: [],
161205
swarmID: swarmId
162206
};
163207

164-
// 4️⃣ Criar stack
208+
// 5️⃣ Criar stack
165209
const url = `${PORTAINER_URL}/api/stacks/create/swarm/string?endpointId=${endpointId}`;
166210

167211
console.log('🔗 URL de criação:', url);
@@ -184,6 +228,13 @@ app.post('/api/stack', authenticateToken, async (req, res) => {
184228

185229
} catch (error) {
186230
console.error('❌ Erro ao criar stack');
231+
232+
// Se o erro for de autenticação, limpa o cache e tenta novamente
233+
if (error.response?.status === 401) {
234+
console.log('🔄 Token expirado, limpando cache...');
235+
jwtCache = { token: null, expiresAt: null };
236+
}
237+
187238
if (error.response) {
188239
console.error('Status:', error.response.status);
189240
console.error('Body da resposta:', JSON.stringify(error.response.data, null, 2));
@@ -201,7 +252,7 @@ app.post('/api/stack', authenticateToken, async (req, res) => {
201252
// Endpoint para listar stacks
202253
app.get('/api/stacks', authenticateToken, async (req, res) => {
203254
try {
204-
const headers = getPortainerHeaders();
255+
const headers = await getPortainerHeaders();
205256

206257
const response = await axios.get(`${PORTAINER_URL}/api/stacks`, {
207258
headers,
@@ -211,15 +262,29 @@ app.get('/api/stacks', authenticateToken, async (req, res) => {
211262
res.json({ success: true, stacks: response.data });
212263
} catch (error) {
213264
console.error('Erro ao listar stacks:', error.response?.data || error.message);
265+
266+
// Se o erro for de autenticação, limpa o cache
267+
if (error.response?.status === 401) {
268+
jwtCache = { token: null, expiresAt: null };
269+
}
270+
214271
res.status(error.response?.status || 500).json({
215272
error: 'Erro ao listar stacks',
216273
details: error.response?.data || error.message
217274
});
218275
}
219276
});
220277

278+
279+
221280
// Health check
222-
app.get('/health', (req, res) => res.json({ status: 'ok', timestamp: new Date().toISOString() }));
281+
app.get('/health', (req, res) => {
282+
res.json({
283+
status: 'ok',
284+
timestamp: new Date().toISOString(),
285+
portainerAuth: jwtCache.token ? 'authenticated' : 'not_authenticated'
286+
});
287+
});
223288

224289
// Listar tipos
225290
app.get('/api/tipos', (req, res) => {
@@ -234,18 +299,68 @@ app.get('/api/tipos', (req, res) => {
234299
});
235300
});
236301

302+
// Status da autenticação
303+
app.get('/api/auth/status', authenticateToken, (req, res) => {
304+
res.json({
305+
authenticated: !!jwtCache.token,
306+
expiresAt: jwtCache.expiresAt ? new Date(jwtCache.expiresAt).toISOString() : null,
307+
timeRemaining: jwtCache.expiresAt ? Math.max(0, jwtCache.expiresAt - Date.now()) : 0
308+
});
309+
});
310+
311+
// Forçar reautenticação
312+
app.post('/api/auth/refresh', authenticateToken, async (req, res) => {
313+
try {
314+
jwtCache = { token: null, expiresAt: null };
315+
const jwt = await authenticatePortainer();
316+
317+
res.json({
318+
success: true,
319+
message: 'Autenticação renovada com sucesso',
320+
expiresAt: new Date(jwtCache.expiresAt).toISOString()
321+
});
322+
} catch (error) {
323+
res.status(500).json({
324+
error: 'Erro ao renovar autenticação',
325+
details: error.message
326+
});
327+
}
328+
});
329+
237330
// Inicialização do servidor
238-
app.listen(PORT, () => {
239-
console.log(`\n🌀 version: 1.2.0`);
240-
console.log(`🚀 API rodando na porta ${PORT}`);
241-
console.log(`📦 Portainer URL: ${PORTAINER_URL}`);
242-
console.log(`🔑 Autenticação Portainer: ${PORTAINER_JWT ? 'JWT' : PORTAINER_API_KEY ? 'API Key' : 'NENHUMA ⚠️'}`);
243-
console.log(`🌐 Endpoint ID padrão: ${PORTAINER_ENDPOINT_ID}`);
244-
console.log(`🐳 Modo Docker: ${process.env.DOCKER_ENV || false}`);
245-
console.log(`🔐 Auth Token API: ${AUTH_TOKEN ? '✅' : '❌'}`);
246-
console.log(`\n📝 Endpoints disponíveis:`);
247-
console.log(` POST /api/stack - Criar stack`);
248-
console.log(` GET /api/stacks - Listar stacks`);
249-
console.log(` GET /api/tipos - Listar tipos disponíveis`);
250-
console.log(` GET /health - Health check`);
251-
});
331+
const startServer = async () => {
332+
try {
333+
// Valida credenciais obrigatórias
334+
if (!PORTAINER_USERNAME || !PORTAINER_PASSWORD) {
335+
console.error('❌ ERRO: PORTAINER_USERNAME e PORTAINER_PASSWORD são obrigatórios!');
336+
process.exit(1);
337+
}
338+
339+
// Tenta autenticar no início
340+
await authenticatePortainer();
341+
342+
app.listen(PORT, () => {
343+
console.log(`\n🌀 version: 2.0.0`);
344+
console.log(`🚀 API rodando na porta ${PORT}`);
345+
console.log(`📦 Portainer URL: ${PORTAINER_URL}`);
346+
console.log(`👤 Usuário Portainer: ${PORTAINER_USERNAME}`);
347+
console.log(`🔐 Autenticação: JWT Automático ✅`);
348+
console.log(`🌐 Endpoint ID padrão: ${PORTAINER_ENDPOINT_ID}`);
349+
console.log(`🐳 Modo Docker: ${process.env.DOCKER_ENV || false}`);
350+
console.log(`🔐 Auth Token API: ${AUTH_TOKEN ? '✅' : '❌'}`);
351+
console.log(`\n📝 Endpoints disponíveis:`);
352+
console.log(` POST /api/stack - Criar stack`);
353+
console.log(` GET /api/stacks - Listar stacks`);
354+
console.log(` GET /api/tipos - Listar tipos disponíveis`);
355+
console.log(` GET /api/auth/status - Status da autenticação`);
356+
console.log(` POST /api/auth/refresh - Renovar autenticação`);
357+
console.log(` GET /health - Health check`);
358+
});
359+
360+
} catch (error) {
361+
console.error('❌ Erro ao iniciar servidor:', error.message);
362+
process.exit(1);
363+
}
364+
};
365+
366+
startServer();

0 commit comments

Comments
 (0)