1
0
postauto/autopost/server.js

680 lines
22 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
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>'
: '';
return `
<tr id="row-${row.id}" data-status="${row.status}" class="odd:bg-gray-800 even:bg-gray-700">
<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>
${logLink}${mediainfoLink}${dlLink}
</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')));
// Servez aussi les vendors sous /autopost
2025-08-10 11:49:16 +02:00
app.use('/js', express.static(
path.join(__dirname, '../node_modules/@tailwindcss/browser/dist'),
{ setHeaders: (res) => res.type('application/javascript') }
2025-08-10 11:49:16 +02:00
));
app.use('/jquery', express.static(
path.join(__dirname, '../node_modules/jquery/dist'),
{ setHeaders: (res) => res.type('application/javascript') }
));
// Miroir des vendors sous /autopost (si reverse-proxy ne forwarde que /autopost/*)
app.use('/autopost/js', express.static(
path.join(__dirname, '../node_modules/@tailwindcss/browser/dist'),
{ setHeaders: (res) => res.type('application/javascript') }
));
app.use('/autopost/jquery', express.static(
path.join(__dirname, '../node_modules/jquery/dist'),
{ setHeaders: (res) => res.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 -->
<script src="/js/index.global.js"></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
`);
});
autopostRouter.post('/login', (req, res) => {
2025-06-23 14:27:18 +02:00
const { username, password } = req.body;
if (username === config.auth.username && password === config.auth.password) {
req.session.authenticated = true;
res.redirect('/autopost');
} else {
res.send('Identifiants invalides. <a href="/autopost/login">Réessayer</a>');
}
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
});
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
});
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
});