1
0
postauto/autopost/server.js

968 lines
32 KiB
JavaScript
Raw Normal View History

2025-03-12 12:47:51 +01:00
const express = require('express');
const session = require('express-session');
const FileStore = require('session-file-store')(session);
2025-03-12 12:47:51 +01:00
const path = require('path');
const fs = require('fs');
const AnsiToHtml = require('ansi-to-html');
const convert = new AnsiToHtml();
const { exec } = require('child_process');
const os = require('os');
const crypto = require('crypto'); // CSRF
const argon2 = require('argon2'); // Pour l'authentification DB
2025-03-12 12:47:51 +01:00
const config = require('./config');
2025-06-23 14:27:18 +02:00
const db = require('./db');
const chokidar = require('chokidar');
2025-08-10 11:49:16 +02:00
2025-06-23 14:27:18 +02:00
db.testConnection(); // vérification au démarrage
2025-03-12 12:47:51 +01:00
function resolveTrustProxy(v) {
if (v == null) return 0;
if (v === true || v === 'true' || v === 'all') return true;
if (typeof v === 'number' || /^\d+$/.test(String(v))) return Number(v);
if (Array.isArray(v)) return v;
return String(v); // ex: "loopback,uniquelocal,127.0.0.1/8"
}
// -------- Helpers sécurité / robustesse --------
function esc(s) {
return String(s).replace(/[&<>"'`=]/g, c => ({
'&': '&amp;', '<': '&lt;', '>': '&gt;',
'"': '&quot;', "'": '&#39;', '`': '&#96;', '=': '&#61;'
}[c]));
}
function safeBaseName(input) {
if (typeof input !== 'string') return null;
const onlyName = path.basename(input);
if (onlyName !== input) return null;
if (!/^[\w .\-()[\]]{1,200}$/.test(onlyName)) return null;
return path.parse(onlyName).name; // sans extension
}
function clamp(n, min, max) {
n = parseInt(n, 10);
if (isNaN(n)) n = min;
return Math.max(min, Math.min(max, n));
}
// -------- Mini moteur de template (fichier HTML avec placeholders) --------
const DEV = process.env.NODE_ENV !== 'production';
const TEMPLATE_AUTPOST_PATH = path.join(__dirname, 'views', 'autopost.html');
let _tplAutopostCache = null;
function getAutopostTemplate() {
if (DEV) {
return fs.readFileSync(TEMPLATE_AUTPOST_PATH, 'utf8');
}
if (!_tplAutopostCache) {
_tplAutopostCache = fs.readFileSync(TEMPLATE_AUTPOST_PATH, 'utf8');
}
return _tplAutopostCache;
}
// Remplace {{PLACEHOLDER}} par data[PLACEHOLDER]
function renderTemplate(tpl, data) {
return tpl.replace(/{{\s*([A-Z0-9_]+)\s*}}/g, (_, k) => {
return Object.prototype.hasOwnProperty.call(data, k) ? String(data[k]) : '';
});
}
function renderRow(row) {
let statusText = '';
let statusClass = '';
switch (parseInt(row.status)) {
case 0: statusText = 'EN ATTENTE'; statusClass = 'bg-cyan-500 text-black font-bold'; break;
case 1: statusText = 'ENVOI TERMINÉ'; statusClass = 'bg-green-300 text-black font-bold'; break;
case 2: statusText = 'ERREUR'; statusClass = 'bg-red-300 text-black font-bold'; break;
case 3: statusText = 'DEJA DISPONIBLE'; statusClass = 'bg-pink-300 text-black font-bold'; break;
case 4: statusText = 'EN COURS'; statusClass = 'bg-yellow-300 text-black font-bold'; break;
default: statusText = 'INCONNU';
}
const logLink = (parseInt(row.status) === 1 || parseInt(row.status) === 2 || parseInt(row.status) === 4)
? ' | <a href="#" class="log-link text-blue-400 hover:underline" data-filename="' + esc(row.nom) + '">Log</a>'
: '';
const mediainfoLink = (parseInt(row.status) === 0 || parseInt(row.status) === 1 || parseInt(row.status) === 2)
? ' | <a href="#" class="mediainfo-link text-blue-400 hover:underline" data-filename="' + esc(row.nom) + '">MediaInfo/BDInfo</a>'
: '';
const dlLink = (parseInt(row.status) === 1)
? ' | <a href="/autopost/dl?name=' + encodeURIComponent(row.nom) + '" class="dl-link text-blue-400 hover:underline">DL</a>'
: '';
Ajout des fonctionnalités de sélection multiple et unification de l'authentification ## Nouvelles fonctionnalités ### Sélection multiple et actions en lot - Ajout d'une colonne de checkboxes avec case "Tout sélectionner" - Panneau d'actions en lot (édition et suppression de plusieurs éléments) - Modals dédiées pour l'édition et suppression en lot - Gestion intelligente de la sélection (état indéterminé) - Routes serveur `/bulk-edit` et `/bulk-delete` avec validation sécurisée ### Amélioration des modals de confirmation - Modal de confirmation pour le renvoi (remplace le confirm() basique) - Interface cohérente avec les autres modals - Gestion clavier (Escape/Enter) pour toutes les modals ### Unification du système d'authentification - Fusion des deux systèmes de login (DB + config) en une seule route - Priorité à la base de données avec fallback sur le fichier config - Logs détaillés avec émojis pour faciliter le débogage - Robustesse améliorée (admin de secours si DB en panne) ## Améliorations configuration et posteur - Configuration API pour le renvoi vers le site principal (config.js) - Correction du calcul de taille pour les liens symboliques (posteur.sh) - Support amélioré du mode symlink avec option -L pour du - Ajout .gitignore pour exclure le dossier .specstory ## Améliorations techniques - Interface utilisateur moderne avec compteur de sélection - Mise à jour visuelle en temps réel - Validation côté serveur avec gestion d'erreurs - Conservation de toutes les fonctionnalités existantes
2025-09-27 15:06:16 +02:00
const resendLink = (parseInt(row.status) === 1)
? ' | <a href="#" class="resend-link text-green-400 hover:underline" data-id="' + row.id + '" data-filename="' + esc(row.nom) + '">Renvoyer</a>'
: '';
return `
<tr id="row-${row.id}" data-status="${row.status}" class="odd:bg-gray-800 even:bg-gray-700">
Ajout des fonctionnalités de sélection multiple et unification de l'authentification ## Nouvelles fonctionnalités ### Sélection multiple et actions en lot - Ajout d'une colonne de checkboxes avec case "Tout sélectionner" - Panneau d'actions en lot (édition et suppression de plusieurs éléments) - Modals dédiées pour l'édition et suppression en lot - Gestion intelligente de la sélection (état indéterminé) - Routes serveur `/bulk-edit` et `/bulk-delete` avec validation sécurisée ### Amélioration des modals de confirmation - Modal de confirmation pour le renvoi (remplace le confirm() basique) - Interface cohérente avec les autres modals - Gestion clavier (Escape/Enter) pour toutes les modals ### Unification du système d'authentification - Fusion des deux systèmes de login (DB + config) en une seule route - Priorité à la base de données avec fallback sur le fichier config - Logs détaillés avec émojis pour faciliter le débogage - Robustesse améliorée (admin de secours si DB en panne) ## Améliorations configuration et posteur - Configuration API pour le renvoi vers le site principal (config.js) - Correction du calcul de taille pour les liens symboliques (posteur.sh) - Support amélioré du mode symlink avec option -L pour du - Ajout .gitignore pour exclure le dossier .specstory ## Améliorations techniques - Interface utilisateur moderne avec compteur de sélection - Mise à jour visuelle en temps réel - Validation côté serveur avec gestion d'erreurs - Conservation de toutes les fonctionnalités existantes
2025-09-27 15:06:16 +02:00
<td class="px-4 py-2 border border-gray-700 text-center">
<input type="checkbox" class="row-checkbox checkbox-custom" data-id="${row.id}" data-name="${esc(row.nom)}">
Ajout des fonctionnalités de sélection multiple et unification de l'authentification ## Nouvelles fonctionnalités ### Sélection multiple et actions en lot - Ajout d'une colonne de checkboxes avec case "Tout sélectionner" - Panneau d'actions en lot (édition et suppression de plusieurs éléments) - Modals dédiées pour l'édition et suppression en lot - Gestion intelligente de la sélection (état indéterminé) - Routes serveur `/bulk-edit` et `/bulk-delete` avec validation sécurisée ### Amélioration des modals de confirmation - Modal de confirmation pour le renvoi (remplace le confirm() basique) - Interface cohérente avec les autres modals - Gestion clavier (Escape/Enter) pour toutes les modals ### Unification du système d'authentification - Fusion des deux systèmes de login (DB + config) en une seule route - Priorité à la base de données avec fallback sur le fichier config - Logs détaillés avec émojis pour faciliter le débogage - Robustesse améliorée (admin de secours si DB en panne) ## Améliorations configuration et posteur - Configuration API pour le renvoi vers le site principal (config.js) - Correction du calcul de taille pour les liens symboliques (posteur.sh) - Support amélioré du mode symlink avec option -L pour du - Ajout .gitignore pour exclure le dossier .specstory ## Améliorations techniques - Interface utilisateur moderne avec compteur de sélection - Mise à jour visuelle en temps réel - Validation côté serveur avec gestion d'erreurs - Conservation de toutes les fonctionnalités existantes
2025-09-27 15:06:16 +02:00
</td>
<td class="px-4 py-2 border border-gray-700">${esc(row.nom)}</td>
<td class="px-4 py-2 border border-gray-700 status-text whitespace-nowrap ${statusClass}">${statusText}</td>
<td class="px-4 py-2 border border-gray-700">${row.id}</td>
<td class="px-4 py-2 border border-gray-700 whitespace-nowrap">
<a href="#" class="edit-link text-blue-400 hover:underline" data-id="${row.id}" data-status="${row.status}">Editer</a> |
<a href="#" class="delete-link text-blue-400 hover:underline" data-id="${row.id}">Supprimer</a>
Ajout des fonctionnalités de sélection multiple et unification de l'authentification ## Nouvelles fonctionnalités ### Sélection multiple et actions en lot - Ajout d'une colonne de checkboxes avec case "Tout sélectionner" - Panneau d'actions en lot (édition et suppression de plusieurs éléments) - Modals dédiées pour l'édition et suppression en lot - Gestion intelligente de la sélection (état indéterminé) - Routes serveur `/bulk-edit` et `/bulk-delete` avec validation sécurisée ### Amélioration des modals de confirmation - Modal de confirmation pour le renvoi (remplace le confirm() basique) - Interface cohérente avec les autres modals - Gestion clavier (Escape/Enter) pour toutes les modals ### Unification du système d'authentification - Fusion des deux systèmes de login (DB + config) en une seule route - Priorité à la base de données avec fallback sur le fichier config - Logs détaillés avec émojis pour faciliter le débogage - Robustesse améliorée (admin de secours si DB en panne) ## Améliorations configuration et posteur - Configuration API pour le renvoi vers le site principal (config.js) - Correction du calcul de taille pour les liens symboliques (posteur.sh) - Support amélioré du mode symlink avec option -L pour du - Ajout .gitignore pour exclure le dossier .specstory ## Améliorations techniques - Interface utilisateur moderne avec compteur de sélection - Mise à jour visuelle en temps réel - Validation côté serveur avec gestion d'erreurs - Conservation de toutes les fonctionnalités existantes
2025-09-27 15:06:16 +02:00
${logLink}${mediainfoLink}${dlLink}${resendLink}
</td>
</tr>`;
}
function renderPaginationHTML(page, totalPages) {
const prevDisabled = page <= 1;
const nextDisabled = page >= totalPages;
const prev = prevDisabled
? `<span class="px-3 py-2 ml-0 leading-tight text-gray-500 bg-gray-200 border border-gray-300 rounded-l-lg">Précédent</span>`
: `<a href="/autopost/?page=${page - 1}" data-page="${page - 1}" class="px-3 py-2 ml-0 leading-tight text-gray-500 bg-white border border-gray-300 rounded-l-lg hover:bg-gray-100 hover:text-gray-700">Précédent</a>`;
const next = nextDisabled
? `<span class="px-3 py-2 leading-tight text-gray-500 bg-gray-200 border border-gray-300 rounded-r-lg">Suivant</span>`
: `<a href="/autopost/?page=${page + 1}" data-page="${page + 1}" class="px-3 py-2 leading-tight text-gray-500 bg-white border border-gray-300 rounded-r-lg hover:bg-gray-100 hover:text-gray-700">Suivant</a>`;
return `
<ul class="inline-flex items-center -space-x-px">
<li>${prev}</li>
<li><span class="px-4 py-2 leading-tight text-gray-700 bg-white border border-gray-300">Page ${page} sur ${totalPages}</span></li>
<li>${next}</li>
</ul>`;
}
2025-03-12 12:47:51 +01:00
const app = express();
const port = config.port;
const background_color = (config?.background_color ?? '').trim() || 'slate-900';
2025-03-12 12:47:51 +01:00
app.disable('x-powered-by'); // header express off
app.use(express.json()); // utile si un jour POST JSON
2025-03-12 12:47:51 +01:00
// Middleware pour parser les formulaires POST
app.use(express.urlencoded({ extended: true }));
app.set('trust proxy', resolveTrustProxy(config.trustProxy));
/* --- Session 7 jours, expiration glissante --- */
const SEVEN_DAYS_MS = 7 * 24 * 60 * 60 * 1000;
const SEVEN_DAYS_S = Math.floor(SEVEN_DAYS_MS / 1000);
2025-03-12 12:47:51 +01:00
app.use(session({
store: new FileStore({
path: config.sessionStorePath || './sessions',
ttl: SEVEN_DAYS_S, // côté store (secondes)
retries: 0
}),
secret: config.sessionSecret,
resave: false,
saveUninitialized: false,
rolling: true, // renouvelle à chaque requête
cookie: {
maxAge: SEVEN_DAYS_MS, // côté navigateur (ms)
sameSite: 'lax',
secure: !!config.cookieSecure, // true seulement si HTTPS
}
2025-03-12 12:47:51 +01:00
}));
app.use(express.static('public'));
const autopostRouter = express.Router();
// Servez /public à la racine ET sous /autopost (utile si l'app est proxifiée sous /autopost)
app.use(express.static(path.join(__dirname, 'public')));
app.use('/autopost', express.static(path.join(__dirname, 'public'), {
setHeaders: (res, path) => {
if (path.endsWith('.css')) {
res.setHeader('Content-Type', 'text/css');
}
}
}));
// Servez aussi les vendors sous /autopost
2025-10-12 15:31:57 +02:00
const tailwindPath = path.join(__dirname, '/node_modules/@tailwindcss/browser/dist');
const jqueryPath = path.join(__dirname, '/node_modules/jquery/dist');
console.log('[STATIC] Tailwind path:', tailwindPath, '- exists:', fs.existsSync(tailwindPath));
console.log('[STATIC] jQuery path:', jqueryPath, '- exists:', fs.existsSync(jqueryPath));
2025-08-10 11:49:16 +02:00
app.use('/js', express.static(
2025-10-12 15:31:57 +02:00
tailwindPath,
2025-10-12 14:48:08 +02:00
{ setHeaders: (res, filepath) => {
2025-10-12 15:31:57 +02:00
console.log('[STATIC] /js serving:', filepath);
2025-10-12 14:48:08 +02:00
if (filepath.endsWith('.js')) {
res.setHeader('Content-Type', 'application/javascript');
}
}}
2025-08-10 11:49:16 +02:00
));
app.use('/jquery', express.static(
2025-10-12 15:31:57 +02:00
jqueryPath,
2025-10-12 14:48:08 +02:00
{ setHeaders: (res, filepath) => {
2025-10-12 15:31:57 +02:00
console.log('[STATIC] /jquery serving:', filepath);
2025-10-12 14:48:08 +02:00
if (filepath.endsWith('.js')) {
res.setHeader('Content-Type', 'application/javascript');
}
}}
));
// Miroir des vendors sous /autopost (si reverse-proxy ne forwarde que /autopost/*)
app.use('/autopost/js', express.static(
2025-10-12 15:31:57 +02:00
tailwindPath,
2025-10-12 14:48:08 +02:00
{ setHeaders: (res, filepath) => {
2025-10-12 15:31:57 +02:00
console.log('[STATIC] /autopost/js serving:', filepath);
2025-10-12 14:48:08 +02:00
if (filepath.endsWith('.js')) {
res.setHeader('Content-Type', 'application/javascript');
}
}}
));
app.use('/autopost/jquery', express.static(
2025-10-12 15:31:57 +02:00
jqueryPath,
2025-10-12 14:48:08 +02:00
{ setHeaders: (res, filepath) => {
2025-10-12 15:31:57 +02:00
console.log('[STATIC] /autopost/jquery serving:', filepath);
2025-10-12 14:48:08 +02:00
if (filepath.endsWith('.js')) {
res.setHeader('Content-Type', 'application/javascript');
}
}}
2025-08-10 11:49:16 +02:00
));
2025-03-15 15:26:12 +01:00
2025-06-23 14:27:18 +02:00
// --------------------------- Auth non protégée -----------------------------
2025-03-12 12:47:51 +01:00
autopostRouter.get('/login', (req, res) => {
res.send(`
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<title>Login ${esc(config.name)}</title>
<!-- Inclusion de Tailwind CSS via le CDN -->
2025-10-12 14:56:17 +02:00
<script src="/js/index.global.js" type="application/javascript"></script>
</head>
<body class="bg-${background_color} flex items-center justify-center min-h-screen">
<div class="bg-slate-700 p-8 rounded-lg shadow-md w-80">
<h2 class="text-center text-2xl font-bold text-white mb-4">Authentification</h2>
<form method="POST" action="/autopost/login">
<input type="text" name="username" placeholder="Identifiant" required
class="w-full p-2 mb-4 rounded border border-gray-300"/>
<input type="password" name="password" placeholder="Mot de passe" required
class="w-full p-2 mb-4 rounded border border-gray-300"/>
<button type="submit"
class="w-full p-2 bg-blue-600 text-white rounded hover:bg-blue-700">
Se connecter
</button>
</form>
</div>
</body>
</html>
2025-03-12 12:47:51 +01:00
`);
});
Ajout des fonctionnalités de sélection multiple et unification de l'authentification ## Nouvelles fonctionnalités ### Sélection multiple et actions en lot - Ajout d'une colonne de checkboxes avec case "Tout sélectionner" - Panneau d'actions en lot (édition et suppression de plusieurs éléments) - Modals dédiées pour l'édition et suppression en lot - Gestion intelligente de la sélection (état indéterminé) - Routes serveur `/bulk-edit` et `/bulk-delete` avec validation sécurisée ### Amélioration des modals de confirmation - Modal de confirmation pour le renvoi (remplace le confirm() basique) - Interface cohérente avec les autres modals - Gestion clavier (Escape/Enter) pour toutes les modals ### Unification du système d'authentification - Fusion des deux systèmes de login (DB + config) en une seule route - Priorité à la base de données avec fallback sur le fichier config - Logs détaillés avec émojis pour faciliter le débogage - Robustesse améliorée (admin de secours si DB en panne) ## Améliorations configuration et posteur - Configuration API pour le renvoi vers le site principal (config.js) - Correction du calcul de taille pour les liens symboliques (posteur.sh) - Support amélioré du mode symlink avec option -L pour du - Ajout .gitignore pour exclure le dossier .specstory ## Améliorations techniques - Interface utilisateur moderne avec compteur de sélection - Mise à jour visuelle en temps réel - Validation côté serveur avec gestion d'erreurs - Conservation de toutes les fonctionnalités existantes
2025-09-27 15:06:16 +02:00
autopostRouter.post('/login', async (req, res) => {
const { username, password } = req.body;
console.log(`[LOGIN] Tentative de connexion pour "${username}"`);
console.log(`[LOGIN] Password reçu: longueur ${password ? password.length : 0} caractères`);
// 1. Essayer d'abord l'authentification via la base de données
try {
console.log(`[LOGIN] Tentative d'authentification DB pour "${username}"`);
const [rows] = await db.query(
'SELECT * FROM core_members WHERE name = ? AND member_group_id IN (1,2) LIMIT 1',
[username]
);
console.log(`[LOGIN] Nombre de résultats DB: ${rows.length}`);
if (rows.length > 0) {
const member = rows[0];
console.log(`[LOGIN] Utilisateur trouvé en DB:`, {
member_id: member.member_id,
name: member.name,
group: member.member_group_id
});
// Vérification du hash Argon2id
console.log(`[LOGIN] Vérification du mot de passe avec Argon2id`);
const valid = await argon2.verify(member.members_pass_hash, password);
console.log(`[LOGIN] Résultat vérification Argon2id: ${valid}`);
if (valid) {
console.log(`[LOGIN] ✅ Authentification DB réussie pour "${member.name}"`);
2025-06-23 14:27:18 +02:00
req.session.authenticated = true;
Ajout des fonctionnalités de sélection multiple et unification de l'authentification ## Nouvelles fonctionnalités ### Sélection multiple et actions en lot - Ajout d'une colonne de checkboxes avec case "Tout sélectionner" - Panneau d'actions en lot (édition et suppression de plusieurs éléments) - Modals dédiées pour l'édition et suppression en lot - Gestion intelligente de la sélection (état indéterminé) - Routes serveur `/bulk-edit` et `/bulk-delete` avec validation sécurisée ### Amélioration des modals de confirmation - Modal de confirmation pour le renvoi (remplace le confirm() basique) - Interface cohérente avec les autres modals - Gestion clavier (Escape/Enter) pour toutes les modals ### Unification du système d'authentification - Fusion des deux systèmes de login (DB + config) en une seule route - Priorité à la base de données avec fallback sur le fichier config - Logs détaillés avec émojis pour faciliter le débogage - Robustesse améliorée (admin de secours si DB en panne) ## Améliorations configuration et posteur - Configuration API pour le renvoi vers le site principal (config.js) - Correction du calcul de taille pour les liens symboliques (posteur.sh) - Support amélioré du mode symlink avec option -L pour du - Ajout .gitignore pour exclure le dossier .specstory ## Améliorations techniques - Interface utilisateur moderne avec compteur de sélection - Mise à jour visuelle en temps réel - Validation côté serveur avec gestion d'erreurs - Conservation de toutes les fonctionnalités existantes
2025-09-27 15:06:16 +02:00
req.session.user_id = member.member_id;
req.session.user_name = member.name;
return res.redirect('/autopost');
} else {
console.warn(`[LOGIN] ❌ Mot de passe DB incorrect pour "${member.name}"`);
}
2025-06-23 14:27:18 +02:00
} else {
Ajout des fonctionnalités de sélection multiple et unification de l'authentification ## Nouvelles fonctionnalités ### Sélection multiple et actions en lot - Ajout d'une colonne de checkboxes avec case "Tout sélectionner" - Panneau d'actions en lot (édition et suppression de plusieurs éléments) - Modals dédiées pour l'édition et suppression en lot - Gestion intelligente de la sélection (état indéterminé) - Routes serveur `/bulk-edit` et `/bulk-delete` avec validation sécurisée ### Amélioration des modals de confirmation - Modal de confirmation pour le renvoi (remplace le confirm() basique) - Interface cohérente avec les autres modals - Gestion clavier (Escape/Enter) pour toutes les modals ### Unification du système d'authentification - Fusion des deux systèmes de login (DB + config) en une seule route - Priorité à la base de données avec fallback sur le fichier config - Logs détaillés avec émojis pour faciliter le débogage - Robustesse améliorée (admin de secours si DB en panne) ## Améliorations configuration et posteur - Configuration API pour le renvoi vers le site principal (config.js) - Correction du calcul de taille pour les liens symboliques (posteur.sh) - Support amélioré du mode symlink avec option -L pour du - Ajout .gitignore pour exclure le dossier .specstory ## Améliorations techniques - Interface utilisateur moderne avec compteur de sélection - Mise à jour visuelle en temps réel - Validation côté serveur avec gestion d'erreurs - Conservation de toutes les fonctionnalités existantes
2025-09-27 15:06:16 +02:00
console.log(`[LOGIN] Aucun utilisateur trouvé en DB pour "${username}"`);
2025-06-23 14:27:18 +02:00
}
Ajout des fonctionnalités de sélection multiple et unification de l'authentification ## Nouvelles fonctionnalités ### Sélection multiple et actions en lot - Ajout d'une colonne de checkboxes avec case "Tout sélectionner" - Panneau d'actions en lot (édition et suppression de plusieurs éléments) - Modals dédiées pour l'édition et suppression en lot - Gestion intelligente de la sélection (état indéterminé) - Routes serveur `/bulk-edit` et `/bulk-delete` avec validation sécurisée ### Amélioration des modals de confirmation - Modal de confirmation pour le renvoi (remplace le confirm() basique) - Interface cohérente avec les autres modals - Gestion clavier (Escape/Enter) pour toutes les modals ### Unification du système d'authentification - Fusion des deux systèmes de login (DB + config) en une seule route - Priorité à la base de données avec fallback sur le fichier config - Logs détaillés avec émojis pour faciliter le débogage - Robustesse améliorée (admin de secours si DB en panne) ## Améliorations configuration et posteur - Configuration API pour le renvoi vers le site principal (config.js) - Correction du calcul de taille pour les liens symboliques (posteur.sh) - Support amélioré du mode symlink avec option -L pour du - Ajout .gitignore pour exclure le dossier .specstory ## Améliorations techniques - Interface utilisateur moderne avec compteur de sélection - Mise à jour visuelle en temps réel - Validation côté serveur avec gestion d'erreurs - Conservation de toutes les fonctionnalités existantes
2025-09-27 15:06:16 +02:00
} catch (err) {
console.error(`[LOGIN] Erreur DB:`, err);
// Continue vers le fallback config
}
// 2. Fallback : authentification via fichier config
console.log(`[LOGIN] Tentative d'authentification config pour "${username}"`);
if (username === config.auth.username && password === config.auth.password) {
console.log(`[LOGIN] ✅ Authentification config réussie pour "${username}"`);
req.session.authenticated = true;
req.session.user_name = username;
return res.redirect('/autopost');
} else {
console.log(`[LOGIN] ❌ Authentification config échouée pour "${username}"`);
}
// 3. Échec des deux méthodes
console.warn(`[LOGIN] 🚫 Échec total d'authentification pour "${username}"`);
res.redirect('login?e=1');
2025-03-12 12:47:51 +01:00
});
2025-08-10 11:49:16 +02:00
2025-03-12 12:47:51 +01:00
autopostRouter.get('/logout', (req, res) => {
req.session.destroy(() => res.redirect('/autopost/login'));
2025-03-12 12:47:51 +01:00
});
// --------------------------- Favicon dynamique -----------------------------
autopostRouter.get('/favicon.ico', (req, res) => {
const initials = config.name.substring(0, 2).toUpperCase();
const svg = `<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32">
<defs>
<linearGradient id="grad" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" style="stop-color:#3B82F6;stop-opacity:1" />
<stop offset="100%" style="stop-color:#1D4ED8;stop-opacity:1" />
</linearGradient>
</defs>
<rect width="32" height="32" rx="6" fill="url(#grad)"/>
<text x="16" y="22" font-family="Arial, sans-serif" font-size="14" font-weight="bold"
text-anchor="middle" fill="white">${initials}</text>
</svg>`;
res.setHeader('Content-Type', 'image/svg+xml');
res.setHeader('Cache-Control', 'public, max-age=86400'); // Cache 24h
res.send(svg);
});
2025-03-12 12:47:51 +01:00
function checkAuth(req, res, next) {
if (req.session && req.session.authenticated) {
next();
} else {
res.redirect(req.baseUrl + '/login');
}
2025-03-12 12:47:51 +01:00
}
autopostRouter.use(checkAuth);
// CSRF : générer un token pour les vues protégées
autopostRouter.use((req, res, next) => {
if (!req.session.csrf) req.session.csrf = crypto.randomUUID();
next();
});
// CSRF : exiger le token sur POST (sauf /login qui est défini avant)
function requireCsrf(req, res, next) {
if (req.method === 'POST') {
const t = req.get('x-csrf-token');
if (!t || !req.session || t !== req.session.csrf) {
return res.status(403).send('CSRF token invalide');
}
}
next();
}
autopostRouter.use(requireCsrf);
// ---- Long-polling refresh (robuste) ----
let lpVersion = 1;
const lpWaiters = new Set();
const LP_TIMEOUT_MS = 25000;
function lpNotify(source, filePath) {
lpVersion += 1;
//console.log('[LP] notify', { version: lpVersion, source, file: filePath });
for (const waiter of lpWaiters) {
clearTimeout(waiter.timer);
try { waiter.res.json({ version: lpVersion }); } catch (_) {}
}
lpWaiters.clear();
}
// Endpoint long-polling
autopostRouter.get('/updates', (req, res) => {
const since = parseInt(req.query.since, 10) || 0;
if (lpVersion > since) {
return res.json({ version: lpVersion });
}
const waiter = {
res,
timer: setTimeout(() => {
lpWaiters.delete(waiter);
res.json({ version: lpVersion }); // heartbeat/timeout
}, LP_TIMEOUT_MS)
};
lpWaiters.add(waiter);
req.on('close', () => {
clearTimeout(waiter.timer);
lpWaiters.delete(waiter);
});
});
// Watcher fichiers (JSON / BDINFO / LOG) + fallback scan
const infoDir = path.resolve(config.infodirectory || config.logdirectory || '.');
const logDir = path.resolve(config.logdirectory || config.infodirectory || '.');
const watchPatterns = Array.from(new Set([
path.join(infoDir, '*.json'),
path.join(infoDir, '*.JSON'),
path.join(infoDir, '*.bdinfo.txt'),
path.join(infoDir, '*.BDINFO.TXT'),
path.join(logDir, '*.log')
]));
const lpWatcher = chokidar.watch(watchPatterns, {
ignoreInitial: true,
awaitWriteFinish: { stabilityThreshold: 1200, pollInterval: 250 },
usePolling: true, // important pour NFS/SMB/Docker
interval: 1000,
depth: 0,
ignorePermissionErrors: true,
persistent: true
});
lpWatcher.on('add', (filePath) => lpNotify('add', filePath));
lpWatcher.on('change', (filePath) => lpNotify('change', filePath));
2025-08-13 21:58:04 +02:00
//lpWatcher.on('ready', () => console.log('[LP] watcher ready on', watchPatterns));
lpWatcher.on('error', (err) => console.error('[LP] watcher error:', err));
// Fallback scan (toutes les 2s) : signature (nb fichiers + dernier mtime)
const fsp = fs.promises; // réutilise ton import fs existant
let lastSig = null;
async function computeSignature() {
async function listSig(dir, rx) {
try {
const names = await fsp.readdir(dir);
const files = names.filter(n => rx.test(n));
let latest = 0;
await Promise.all(files.map(async (n) => {
try {
const st = await fsp.stat(path.join(dir, n));
if (st.mtimeMs > latest) latest = st.mtimeMs;
} catch (_) {}
}));
return { count: files.length, latest };
} catch {
return { count: 0, latest: 0 };
}
}
const a = await listSig(infoDir, /(?:\.json|\.bdinfo\.txt)$/i);
const b = await listSig(logDir, /(?:\.log)$/i);
const files = a.count + b.count;
const latest = Math.max(a.latest, b.latest);
return files + ':' + Math.floor(latest);
}
async function periodicScan() {
try {
const sig = await computeSignature();
if (lastSig === null) {
lastSig = sig; // baseline
} else if (sig !== lastSig) {
lastSig = sig;
lpNotify('scan', infoDir + '|' + logDir);
}
} catch (e) {
console.error('[LP] periodic scan error:', e.message || e);
} finally {
setTimeout(periodicScan, 2000);
}
}
periodicScan();
2025-06-23 14:27:18 +02:00
// --------------------------- PAGE PRINCIPALE -----------------------------
autopostRouter.get('/', async (req, res) => {
2025-03-12 12:47:51 +01:00
const limit = 100; // enregistrements par page
const page = clamp(req.query.page, 1, 1_000_000);
2025-03-12 12:47:51 +01:00
const offset = (page - 1) * limit;
2025-06-23 14:27:18 +02:00
try {
const [stats] = await db.query(`
SELECT
COUNT(*) AS total,
SUM(status = 0) AS attente,
SUM(status = 1) AS termine,
SUM(status = 2) AS erreur,
SUM(status = 3) AS deja,
SUM(status = 4) AS encours
FROM \`${config.DB_TABLE}\`
`);
2025-06-23 14:27:18 +02:00
// Récupérer le nombre total d'enregistrements
2025-08-08 23:07:13 +02:00
const [countResult] = await db.query(`SELECT COUNT(*) as total FROM \`${config.DB_TABLE}\``);
2025-06-23 14:27:18 +02:00
const totalRecords = countResult[0].total;
const totalPages = Math.max(1, Math.ceil(totalRecords / limit));
2025-03-12 12:47:51 +01:00
2025-06-23 14:27:18 +02:00
const [rows] = await db.query(
2025-08-08 23:07:13 +02:00
`SELECT nom, status, id FROM \`${config.DB_TABLE}\` ORDER BY (status = 2) DESC, id DESC LIMIT ? OFFSET ?`,
2025-06-23 14:27:18 +02:00
[limit, offset]
);
const tableRowsHTML = rows.map(renderRow).join('\n');
const paginationHTML = renderPaginationHTML(page, totalPages);
// JSON damorçage pour le client
const BOOTSTRAP_JSON = JSON.stringify({
csrf: req.session.csrf,
page,
totalPages,
limit
}).replace(/</g, '\\u003c'); // anti </script>
const tpl = getAutopostTemplate();
const html = renderTemplate(tpl, {
TITLE: `Suivi Autopost ${esc(config.name)}`,
CSRF_TOKEN: esc(req.session.csrf),
BACKGROUND_CLASS: background_color, // ex: slate-900
APP_NAME: esc(config.name),
STAT_ATTENTE: stats[0].attente || 0,
STAT_TERMINE: stats[0].termine || 0,
STAT_ERREUR: stats[0].erreur || 0,
STAT_DEJA: stats[0].deja || 0,
PAGINATION_HTML: paginationHTML,
TABLE_ROWS: tableRowsHTML,
BOOTSTRAP_JSON
});
2025-06-23 14:27:18 +02:00
res.send(html);
} catch (err) {
console.error(err.message);
res.status(500).send("Erreur lors de la requête DB.");
}
2025-03-12 12:47:51 +01:00
});
2025-06-23 14:27:18 +02:00
// --------------------------- Recherche -----------------------------
autopostRouter.get('/search', async (req, res) => {
const q = String(req.query.q || "").slice(0, 200);
const page = clamp(req.query.page, 1, 1_000_000);
const limit = clamp(req.query.limit, 1, 500) || 100;
2025-08-12 10:15:05 +02:00
const offset = (page - 1) * limit;
const searchQuery = `%${q}%`;
try {
const [countRows] = await db.query(
`SELECT COUNT(*) AS total FROM \`${config.DB_TABLE}\` WHERE nom LIKE ?`,
[searchQuery]
);
const total = countRows[0].total;
const totalPages = Math.max(1, Math.ceil(total / limit));
const [rows] = await db.query(
`SELECT nom, status, id
FROM \`${config.DB_TABLE}\`
WHERE nom LIKE ?
ORDER BY (status = 2) DESC, id DESC
LIMIT ? OFFSET ?`,
[searchQuery, limit, offset]
);
res.json({ rows, page, limit, total, totalPages });
} catch (err) {
console.error(err.message);
res.status(500).json({ error: "Erreur lors de la requête." });
}
2025-03-12 12:47:51 +01:00
});
autopostRouter.get('/log', (req, res) => {
const base = safeBaseName(req.query.name);
if (!base) {
return res.status(400).json({ error: "Nom de fichier invalide." });
2025-03-12 12:47:51 +01:00
}
const logFilePath = path.join(config.logdirectory, base + '.log');
2025-03-12 12:47:51 +01:00
fs.readFile(logFilePath, 'utf8', (err, data) => {
if (err) {
console.error(err.message);
return res.status(500).json({ error: "Erreur lors du chargement du fichier log." });
}
2025-08-11 15:22:45 +02:00
// 1) enlever les codes curseur (début de ligne/clear) mais garder les couleurs (…m)
let s = data.replace(/\x1b\[[0-9;]*[GK]/g, '');
// 2) supprimer TOUT segment de progression
2025-08-11 15:22:45 +02:00
const ANSI_M = '(?:\\x1b\\[[0-9;]*m)*';
const NUM = '\\d{1,3}(?:\\.\\d+)?';
const PROG = new RegExp(
ANSI_M + '\\s*' + NUM + '\\s*%' +
'\\s*' + ANSI_M + '\\[[^\\]]*\\]' +
'(?:\\s*' + ANSI_M + NUM + '\\s*MiB\\/s,\\s*ETA\\s*[^\\x1bG\\n]*)?',
'g'
);
s = s.replace(PROG, '');
// 3) nettoyer les résidus "-G" / "G" avant G[INFO]
2025-08-11 15:22:45 +02:00
s = s.replace(/-?\s*G(?=\[INFO\])/g, '');
// 4) normalisation
2025-08-11 15:22:45 +02:00
s = s.replace(/[ \t]+/g, ' ')
.replace(/\r/g, '')
.replace(/\n{3,}/g, '\n\n')
.replace(/(?!^)(?=\[INFO\])/gm, '\n')
.replace(/(?!^)Calculating\s*:/gm, '\nCalculating :')
.replace(/(?!^)Writing\s*:/gm, '\nWriting :')
.replace(/(?!^)Finalizing\s*:/gm, '\nFinalizing :')
.replace(/(?!^)Finished\s*:/gm, '\nFinished :')
.trim();
const htmlContent = convert.toHtml(s);
2025-03-12 12:47:51 +01:00
res.json({ content: htmlContent });
});
2025-08-11 15:22:45 +02:00
2025-03-12 12:47:51 +01:00
});
2025-06-23 14:27:18 +02:00
// --------------------------- Mediainfo -----------------------------
2025-03-12 12:47:51 +01:00
autopostRouter.get('/mediainfo', (req, res) => {
const base = safeBaseName(req.query.name);
if (!base) {
return res.status(400).json({ error: "Nom de fichier invalide." });
2025-03-12 12:47:51 +01:00
}
const jsonPath = path.join(config.infodirectory, base + '.json');
const bdinfoPath = path.join(config.infodirectory, base + '.bdinfo.txt');
// Vérifie lequel des deux existe, priorité au .json
fs.access(jsonPath, fs.constants.F_OK, (errJson) => {
if (!errJson) {
// .json existe
readAndSend(jsonPath);
} else {
// .json n'existe pas, on essaie .bdinfo.txt
fs.access(bdinfoPath, fs.constants.F_OK, (errBdinfo) => {
if (!errBdinfo) {
readAndSend(bdinfoPath);
} else {
res.status(404).json({ error: "Aucun fichier mediainfo trouvé." });
}
});
2025-03-12 12:47:51 +01:00
}
});
function readAndSend(filePath) {
fs.readFile(filePath, 'utf8', (err, data) => {
if (err) {
console.error(err.message);
return res.status(500).json({ error: "Erreur lors du chargement du fichier mediainfo." });
}
// Essaie de prettifier si JSON
try {
const obj = JSON.parse(data);
const pretty = JSON.stringify(obj, null, 2);
res.json({ content: pretty });
} catch (e) {
res.json({ content: data });
}
});
}
2025-03-12 12:47:51 +01:00
});
2025-06-23 14:27:18 +02:00
// --------------------------- Download -----------------------------
2025-03-12 12:47:51 +01:00
autopostRouter.get('/dl', (req, res) => {
const base = safeBaseName(req.query.name);
if (!base) {
return res.status(400).send("Nom de fichier invalide.");
2025-03-12 12:47:51 +01:00
}
const subfolder = base.charAt(0).toUpperCase();
const archiveFilePath = path.join(config.finishdirectory, subfolder, base + '.7z');
2025-03-12 12:47:51 +01:00
console.log("Tentative de téléchargement (archive 7z) :", archiveFilePath);
2025-04-17 19:24:47 +02:00
const tmpDir = path.join(__dirname, 'tmp');
fs.mkdirSync(tmpDir, { recursive: true }); // s'assurer que le répertoire existe
2025-03-12 12:47:51 +01:00
const extractedFilePath = path.join(tmpDir, base + '.nzb');
const command = `7z e "${archiveFilePath}" -o"${tmpDir}" -y`;
exec(command, (error, stdout, stderr) => {
if (error) {
console.error(`Erreur lors de la décompression: ${error.message}`);
return res.status(500).send("Erreur lors de la décompression.");
}
res.download(extractedFilePath, base + '.nzb', (err) => {
if (err) {
console.error("Erreur lors du téléchargement :", err);
res.status(500).send("Erreur lors du téléchargement.");
}
fs.unlink(extractedFilePath, (err) => {
if (err) {
console.error("Erreur lors de la suppression du fichier temporaire :", err);
}
});
});
});
});
2025-06-23 14:27:18 +02:00
// --------------------------- Édition -----------------------------
autopostRouter.post('/edit/:id', async (req, res) => {
const id = req.params.id;
const newStatus = req.body.status;
try {
await db.query(`UPDATE \`${config.DB_TABLE}\` SET status = ? WHERE id = ?`, [newStatus, id]);
if (req.xhr || req.get('accept')?.includes('json')) {
res.json({ success: true });
} else {
res.redirect("/autopost/");
2025-03-12 12:47:51 +01:00
}
} catch (err) {
console.error(err.message);
res.status(500).send("Erreur lors de la mise à jour.");
}
2025-03-12 12:47:51 +01:00
});
2025-06-23 14:27:18 +02:00
// --------------------------- Suppression -----------------------------
autopostRouter.post('/delete/:id', async (req, res) => {
const id = req.params.id;
try {
await db.query(`DELETE FROM \`${config.DB_TABLE}\` WHERE id = ?`, [id]);
if (req.xhr || req.get('accept')?.includes('json')) {
res.json({ success: true });
} else {
res.redirect("/autopost/");
2025-03-12 12:47:51 +01:00
}
} catch (err) {
console.error(err.message);
res.status(500).json({ error: "Erreur lors de la suppression." });
}
2025-03-12 12:47:51 +01:00
});
Ajout des fonctionnalités de sélection multiple et unification de l'authentification ## Nouvelles fonctionnalités ### Sélection multiple et actions en lot - Ajout d'une colonne de checkboxes avec case "Tout sélectionner" - Panneau d'actions en lot (édition et suppression de plusieurs éléments) - Modals dédiées pour l'édition et suppression en lot - Gestion intelligente de la sélection (état indéterminé) - Routes serveur `/bulk-edit` et `/bulk-delete` avec validation sécurisée ### Amélioration des modals de confirmation - Modal de confirmation pour le renvoi (remplace le confirm() basique) - Interface cohérente avec les autres modals - Gestion clavier (Escape/Enter) pour toutes les modals ### Unification du système d'authentification - Fusion des deux systèmes de login (DB + config) en une seule route - Priorité à la base de données avec fallback sur le fichier config - Logs détaillés avec émojis pour faciliter le débogage - Robustesse améliorée (admin de secours si DB en panne) ## Améliorations configuration et posteur - Configuration API pour le renvoi vers le site principal (config.js) - Correction du calcul de taille pour les liens symboliques (posteur.sh) - Support amélioré du mode symlink avec option -L pour du - Ajout .gitignore pour exclure le dossier .specstory ## Améliorations techniques - Interface utilisateur moderne avec compteur de sélection - Mise à jour visuelle en temps réel - Validation côté serveur avec gestion d'erreurs - Conservation de toutes les fonctionnalités existantes
2025-09-27 15:06:16 +02:00
// --------------------------- Opérations en lot -----------------------------
// Édition en lot
autopostRouter.post('/bulk-edit', async (req, res) => {
const { ids, status } = req.body;
if (!Array.isArray(ids) || ids.length === 0) {
return res.status(400).json({ error: 'Liste d\'IDs invalide' });
}
const statusInt = parseInt(status, 10);
if (![0, 1, 2, 3, 4].includes(statusInt)) {
return res.status(400).json({ error: 'Statut invalide' });
}
// Valider que tous les IDs sont des entiers positifs
const validIds = ids.filter(id => parseInt(id, 10) > 0).map(id => parseInt(id, 10));
if (validIds.length === 0) {
return res.status(400).json({ error: 'Aucun ID valide' });
}
try {
const placeholders = validIds.map(() => '?').join(',');
const query = `UPDATE \`${config.DB_TABLE}\` SET status = ? WHERE id IN (${placeholders})`;
const params = [statusInt, ...validIds];
const [result] = await db.query(query, params);
res.json({
message: `${result.affectedRows} élément(s) mis à jour`,
updated: result.affectedRows
});
} catch (err) {
console.error(err.message);
res.status(500).json({ error: 'Erreur DB lors de la mise à jour en lot' });
}
});
// Suppression en lot
autopostRouter.post('/bulk-delete', async (req, res) => {
const { ids } = req.body;
if (!Array.isArray(ids) || ids.length === 0) {
return res.status(400).json({ error: 'Liste d\'IDs invalide' });
}
// Valider que tous les IDs sont des entiers positifs
const validIds = ids.filter(id => parseInt(id, 10) > 0).map(id => parseInt(id, 10));
if (validIds.length === 0) {
return res.status(400).json({ error: 'Aucun ID valide' });
}
try {
const placeholders = validIds.map(() => '?').join(',');
const query = `DELETE FROM \`${config.DB_TABLE}\` WHERE id IN (${placeholders})`;
const [result] = await db.query(query, validIds);
res.json({
message: `${result.affectedRows} élément(s) supprimé(s)`,
deleted: result.affectedRows
});
} catch (err) {
console.error(err.message);
res.status(500).json({ error: 'Erreur DB lors de la suppression en lot' });
}
});
// --------------------------- Renvoi -----------------------------
autopostRouter.post('/resend/:id', async (req, res) => {
const id = req.params.id;
try {
// Récupérer les informations de l'enregistrement
const [rows] = await db.query(`SELECT nom, status FROM \`${config.DB_TABLE}\` WHERE id = ?`, [id]);
if (rows.length === 0) {
return res.status(404).json({ error: "Enregistrement non trouvé." });
}
const row = rows[0];
const fileName = row.nom;
const status = parseInt(row.status);
// Vérifier que le statut est "ENVOI TERMINÉ" (status = 1)
if (status !== 1) {
return res.status(400).json({ error: "Seuls les enregistrements avec statut 'ENVOI TERMINÉ' peuvent être renvoyés." });
}
const base = safeBaseName(fileName);
if (!base) {
return res.status(400).json({ error: "Nom de fichier invalide." });
}
// Chemins des fichiers nécessaires
const subfolder = base.charAt(0).toUpperCase();
const nzbArchivePath = path.join(config.finishdirectory, subfolder, base + '.7z');
const jsonPath = path.join(config.infodirectory, base + '.json');
const bdinfoPath = path.join(config.infodirectory, base + '.bdinfo.txt');
const quicksummaryPath = path.join(config.infodirectory, base + '.quicksummary.txt');
// Vérifier que l'archive NZB existe
if (!fs.existsSync(nzbArchivePath)) {
return res.status(404).json({ error: "Archive NZB non trouvée." });
}
// Extraire le NZB temporairement
const tmpDir = path.join(__dirname, 'tmp');
fs.mkdirSync(tmpDir, { recursive: true });
const tmpNzbPath = path.join(tmpDir, base + '.nzb');
const extractCommand = `7z e "${nzbArchivePath}" -o"${tmpDir}" -y`;
exec(extractCommand, (extractError) => {
if (extractError) {
console.error(`Erreur lors de l'extraction: ${extractError.message}`);
return res.status(500).json({ error: "Erreur lors de l'extraction du NZB." });
}
// Vérifier si c'est un ISO (BDInfo) ou autre (JSON)
const isIso = fileName.toLowerCase().endsWith('.iso');
let curlCommand;
if (isIso) {
// Pour les ISO : bdinfo_full + bdinfo_mini + nzb
if (!fs.existsSync(bdinfoPath) || !fs.existsSync(quicksummaryPath)) {
fs.unlinkSync(tmpNzbPath);
return res.status(404).json({ error: "Fichiers BDInfo non trouvés." });
}
curlCommand = `curl -s -k -L -m 60 \\
-F rlsname=${base} \\
-F bdinfo_full=@${bdinfoPath} \\
-F bdinfo_mini=@${quicksummaryPath} \\
-F nzb=@${tmpNzbPath} \\
-F upload=upload "${config.apiUrl}${config.apiKey}"`;
} else {
// Pour les autres : generated_nfo_json + nzb
if (!fs.existsSync(jsonPath)) {
fs.unlinkSync(tmpNzbPath);
return res.status(404).json({ error: "Fichier JSON non trouvé." });
}
curlCommand = `curl -s -k -L -m 60 \\
-F rlsname=${base} \\
-F generated_nfo_json=@${jsonPath} \\
-F nzb=@${tmpNzbPath} \\
-F upload=upload "${config.apiUrl}${config.apiKey}"`;
}
// Exécuter la commande curl
exec(curlCommand, (curlError, stdout, stderr) => {
// Nettoyer le fichier temporaire
fs.unlinkSync(tmpNzbPath);
// Log des sorties curl pour debug
console.log(`=== CURL OUTPUT pour ${fileName} ===`);
console.log(`Command: ${curlCommand}`);
if (stdout) console.log(`STDOUT: ${stdout}`);
if (stderr) console.log(`STDERR: ${stderr}`);
console.log(`================================`);
if (curlError) {
console.error(`Erreur lors du renvoi: ${curlError.message}`);
return res.status(500).json({ error: "Erreur lors du renvoi vers le site principal." });
}
console.log(`Renvoi réussi pour ${fileName}`);
res.json({ success: true, message: "Renvoi effectué avec succès." });
});
});
} catch (err) {
console.error(err.message);
res.status(500).json({ error: "Erreur lors du renvoi." });
}
});
2025-08-12 10:15:05 +02:00
// --------------------------- Filtrage -----------------------------
2025-08-11 18:43:12 +02:00
autopostRouter.get('/filter', async (req, res) => {
2025-08-12 10:15:05 +02:00
const status = Number.isFinite(parseInt(req.query.status, 10))
? clamp(req.query.status, 0, 4)
2025-08-12 10:15:05 +02:00
: null;
if (status === null) {
return res.status(400).json({ error: "Status non fourni." });
}
const page = clamp(req.query.page, 1, 1_000_000);
const limit = clamp(req.query.limit, 1, 500) || 100;
2025-08-12 10:15:05 +02:00
const offset = (page - 1) * limit;
try {
const [countRows] = await db.query(
`SELECT COUNT(*) AS total FROM \`${config.DB_TABLE}\` WHERE status = ?`,
[status]
);
const total = countRows[0].total;
const totalPages = Math.max(1, Math.ceil(total / limit));
const [rows] = await db.query(
`SELECT nom, status, id
FROM \`${config.DB_TABLE}\`
WHERE status = ?
ORDER BY id DESC
LIMIT ? OFFSET ?`,
[status, limit, offset]
);
res.json({ rows, page, limit, total, totalPages });
} catch (err) {
console.error(err.message);
res.status(500).json({ error: "Erreur lors de la requête." });
}
2025-08-11 18:43:12 +02:00
});
// --------------------------- STATS -----------------------------
autopostRouter.get('/stats', async (req, res) => {
try {
const [rows] = await db.query(`
SELECT
COUNT(*) AS total,
SUM(status = 0) AS attente,
SUM(status = 1) AS termine,
SUM(status = 2) AS erreur,
SUM(status = 3) AS deja,
SUM(status = 4) AS encours
FROM \`${config.DB_TABLE}\`
`);
res.json(rows[0]);
} catch (e) {
console.error(e);
res.status(500).json({ error: 'Erreur stats' });
}
});
2025-06-23 14:27:18 +02:00
// Redirection accès direct GET /edit/:id
2025-03-12 12:47:51 +01:00
autopostRouter.get('/edit/:id', (req, res) => {
res.redirect("/autopost/");
2025-03-12 12:47:51 +01:00
});
app.use('/autopost', autopostRouter);
// Endpoint santé simple
app.get('/healthz', (req, res) => res.type('text').send('ok'));
2025-03-12 12:47:51 +01:00
app.listen(port, () => {
console.log(`Serveur démarré sur http://localhost:${port}/autopost`);
2025-03-12 12:47:51 +01:00
});