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
This commit is contained in:
@@ -84,16 +84,22 @@ function renderRow(row) {
|
||||
const dlLink = (parseInt(row.status) === 1)
|
||||
? ' | <a href="/autopost/dl?name=' + encodeURIComponent(row.nom) + '" class="dl-link text-blue-400 hover:underline">DL</a>'
|
||||
: '';
|
||||
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">
|
||||
<td class="px-4 py-2 border border-gray-700 text-center">
|
||||
<input type="checkbox" class="row-checkbox rounded border-gray-600 bg-gray-700 text-blue-600 focus:ring-blue-500" data-id="${row.id}" data-name="${esc(row.nom)}">
|
||||
</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>
|
||||
${logLink}${mediainfoLink}${dlLink}
|
||||
${logLink}${mediainfoLink}${dlLink}${resendLink}
|
||||
</td>
|
||||
</tr>`;
|
||||
}
|
||||
@@ -207,14 +213,66 @@ autopostRouter.get('/login', (req, res) => {
|
||||
`);
|
||||
});
|
||||
|
||||
autopostRouter.post('/login', (req, res) => {
|
||||
const { username, password } = req.body;
|
||||
if (username === config.auth.username && password === config.auth.password) {
|
||||
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}"`);
|
||||
req.session.authenticated = true;
|
||||
res.redirect('/autopost');
|
||||
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}"`);
|
||||
}
|
||||
} else {
|
||||
res.send('Identifiants invalides. <a href="/autopost/login">Réessayer</a>');
|
||||
console.log(`[LOGIN] Aucun utilisateur trouvé en DB pour "${username}"`);
|
||||
}
|
||||
} 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');
|
||||
});
|
||||
|
||||
autopostRouter.get('/logout', (req, res) => {
|
||||
@@ -607,6 +665,184 @@ autopostRouter.post('/delete/:id', async (req, res) => {
|
||||
}
|
||||
});
|
||||
|
||||
// --------------------------- 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." });
|
||||
}
|
||||
});
|
||||
|
||||
// --------------------------- Filtrage -----------------------------
|
||||
autopostRouter.get('/filter', async (req, res) => {
|
||||
const status = Number.isFinite(parseInt(req.query.status, 10))
|
||||
|
||||
Reference in New Issue
Block a user