1
0

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:
unfr
2025-09-27 15:06:16 +02:00
parent 2847ed5514
commit 3de9e74331
6 changed files with 623 additions and 14 deletions

View File

@@ -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))