821eaf4fa3
init commit
269 lines
9.8 KiB
JavaScript
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('');
|
|
}); |