Files
jira-extractor/server.js
T
2026-05-08 07:21:31 +00:00

269 lines
9.8 KiB
JavaScript

import fs from 'fs';
import express from 'express';
import axios from 'axios';
import https from 'https';
import { HttpsProxyAgent } from 'https-proxy-agent'; // <-- AJOUT
// ==========================================
// Lecture du fichier config.ini (inchangé)
// ==========================================
function parseIni(filePath) {
const config = {};
let section = '';
const content = fs.readFileSync(filePath, 'utf8');
content.split('\n').forEach(line => {
line = line.trim();
if (!line || line.startsWith('#') || line.startsWith(';')) return;
const sectionMatch = line.match(/^\[(.+)\]$/);
if (sectionMatch) { section = sectionMatch[1]; config[section] = {}; return; }
const kv = line.match(/^([^=]+)=(.*)$/);
if (kv && section) config[section][kv[1].trim()] = kv[2].trim();
});
return config;
}
const config = parseIni('./config.ini');
if (!config.jira?.url || !config.proxy?.host) {
console.error('ERREUR: config.ini mal rempli.');
process.exit(1);
}
// ==========================================
// Configuration du client HTTP (Axios)
// ==========================================
// Construire l'URL du proxy
const proxyUrl = new URL(`http://${config.proxy.host}:${config.proxy.port}`);
if (config.proxy.user && config.proxy.pass) {
proxyUrl.username = config.proxy.user;
proxyUrl.password = config.proxy.pass;
}
// Créer l'agent de tunneling CONNECT
const proxyAgent = new HttpsProxyAgent(proxyUrl);
const jiraClient = axios.create({
baseURL: config.jira.url,
// IMPORTANT : désactiver le proxy interne d'Axios pour utiliser le nôtre
proxy: false,
httpsAgent: proxyAgent,
httpAgent: proxyAgent,
timeout: 60000
});
// ==========================================
// DEBUG : Intercepteurs de requêtes
// ==========================================
jiraClient.interceptors.request.use(request => {
const authHeader = request.headers['Authorization'] || '';
// On masque le token pour pas le laisser en clair dans les logs
const masked = authHeader.substring(0, 15) + '...[MASQUE]...' + authHeader.substring(authHeader.length - 5);
console.log(`\n[DEBUG REQUETE] ${request.method?.toUpperCase()} ${request.baseURL}${request.url}`);
console.log(`[DEBUG HEADER] Authorization: ${masked}`);
return request;
});
jiraClient.interceptors.response.use(
response => response,
error => {
if (error.response) {
console.error(`\n[DEBUG ERREUR JIRA] Statut : ${error.response.status}`);
console.error(`[DEBUG ERREUR JIRA] Headers:`, JSON.stringify(error.response.headers, null, 2));
console.error(`[DEBUG ERREUR JIRA] Body :`, JSON.stringify(error.response.data, null, 2));
} else {
console.error(`\n[DEBUG ERREUR RESEAU]`, error.message);
}
return Promise.reject(error);
}
);
// ==========================================
// Serveur Express
// ==========================================
const app = express();
app.use(express.json({ limit: '10mb' }));
app.use(express.static('.')); // Sert index.html à /
// Endpoint d'extraction
app.post('/api/extract', async (req, res) => {
const { username, apiToken, projectKey, jql, customFields } = req.body;
if (!username || !apiToken || !projectKey) {
return res.status(400).json({ error: "Les champs username, apiToken et projectKey sont requis." });
}
const auth = Buffer.from(`${username}:${apiToken}`).toString('base64');
// Construction de la liste des champs Jira à demander
const standardFields = [
'key','summary','description','status','assignee','reporter','priority',
'created','updated','duedate','resolution','resolutiondate','issuetype',
'labels','components','fixVersions','versions','project'
];
// Parser les custom fields (ex: "customfield_10001:Production Date")
const customFieldsMap = [];
if (customFields) {
customFields.split(',').forEach(cf => {
const [id, label] = cf.split(':').map(s => s.trim());
if (id) customFieldsMap.push({ id, label: label || id });
});
}
const fieldsToRequest = [
...standardFields,
...customFieldsMap.map(cf => cf.id)
].join(',');
const queryJql = jql || `project = "${projectKey}" ORDER BY created DESC`;
let startAt = 0;
let total = 0;
let allTickets = [];
try {
console.log(`[EXTRACT] Début pour ${projectKey} (JQL: ${queryJql.substring(0, 50)}...)`);
// Pagination automatique
while (true) {
const response = await jiraClient.get('/rest/api/2/search', {
params: {
jql: queryJql,
startAt: startAt,
maxResults: 100, // Max standard pour Jira
fields: fieldsToRequest
},
headers: {
//'Authorization': `Basic ${auth}`,
'Authorization': `Bearer ${apiToken}`,
'Accept': 'application/json'
}
});
if (startAt === 0) {
total = response.data.total;
console.log(`[EXTRACT] ${total} tickets trouvés. Récupération en cours...`);
}
if (total === 0) break;
// Mapping des tickets
for (const issue of response.data.issues) {
const f = issue.fields;
const ticket = {
key: issue.key,
url: `${config.jira.url}/browse/${issue.key}`,
summary: f.summary,
description: f.description,
status: f.status?.name,
statusCategory: f.status?.statusCategory?.name,
assignee: f.assignee?.displayName || f.assignee?.name,
reporter: f.reporter?.displayName || f.reporter?.name,
priority: f.priority?.name,
issueType: f.issuetype?.name,
created: f.created,
updated: f.updated,
dueDate: f.duedate,
resolution: f.resolution?.name,
resolutionDate: f.resolutiondate,
labels: f.labels || [],
components: f.components?.map(c => c.name).join(', ') || null,
fixVersions: f.fixVersions?.map(v => v.name).join(', ') || null,
projectName: f.project?.name
};
// Ajout des champs personnalisés
customFieldsMap.forEach(cf => {
const val = f[cf.id];
if (val !== null && val !== undefined) {
// Pour les dates Jira, formater proprement
if (typeof val === 'string' && (val.match(/^\d{4}-\d{2}-\d{2}/) || val.match(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}/))) {
ticket[cf.label] = val;
} else {
ticket[cf.label] = typeof val === 'object' ? (val.name || JSON.stringify(val)) : val;
}
} else {
ticket[cf.label] = null;
}
});
allTickets.push(ticket);
}
startAt += response.data.issues.length;
if (startAt >= total) break;
}
console.log(`[EXTRACT] Terminé. ${allTickets.length} tickets extraits.`);
res.json({ success: true, total: allTickets.length, tickets: allTickets });
} catch (error) {
console.error('[EXTRACT ERROR]', error.response?.data || error.message);
res.status(error.response?.status || 500).json({
error: error.response?.data?.errorMessages?.[0] || error.message,
details: error.response?.data || null
});
}
});
// Endpoint de mise à jour de ticket
app.put('/api/update', async (req, res) => {
const { username, apiToken, ticketKey, updates } = req.body;
if (!username || !apiToken || !ticketKey || !updates) {
return res.status(400).json({ error: "Les champs username, apiToken, ticketKey et updates sont requis." });
}
// Construire le payload pour l'API Jira
const fields = {};
const updateFields = {};
for (const [key, value] of Object.entries(updates)) {
if (key === 'duedate') {
// Due date est un champ standard
fields[key] = value || null;
} else if (key.startsWith('customfield_')) {
// Custom field
fields[key] = value || null;
}
}
try {
console.log(`[UPDATE] Mise à jour du ticket ${ticketKey}:`, updates);
const response = await jiraClient.put(`/rest/api/2/issue/${ticketKey}`, {
fields: fields
}, {
headers: {
'Authorization': `Bearer ${apiToken}`,
'Accept': 'application/json'
}
});
console.log(`[UPDATE] Ticket ${ticketKey} mis à jour avec succès.`);
res.json({ success: true, ticketKey, updatedFields: updates });
} catch (error) {
console.error('[UPDATE ERROR]', error.response?.data || error.message);
res.status(error.response?.status || 500).json({
error: error.response?.data?.errorMessages?.[0] || error.message,
details: error.response?.data || null
});
}
});
// Démarrage
const PORT = config.server?.port || 3000;
app.listen(PORT, '0.0.0.0', () => {
console.log('');
console.log('============================================');
console.log(' Jira Extractor API');
console.log(` Local : http://localhost:${PORT}`);
console.log(` Jira : ${config.jira.url}`);
console.log(` Proxy : ${config.proxy.host}:${config.proxy.port}`);
console.log('============================================');
console.log('');
});