Upload files to "/"
init commit
This commit is contained in:
@@ -0,0 +1,269 @@
|
||||
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('');
|
||||
});
|
||||
Reference in New Issue
Block a user