fix: conformité skills - branding Login (logos Itinova/Santinova), import CSV/Excel utilisateurs
This commit is contained in:
@@ -1,9 +1,30 @@
|
||||
import { Router, Request, Response } from 'express';
|
||||
import bcrypt from 'bcryptjs';
|
||||
import jwt from 'jsonwebtoken';
|
||||
import multer from 'multer';
|
||||
import * as XLSX from 'xlsx';
|
||||
import { getPool } from '../config/database';
|
||||
import { authenticate, AuthRequest, authorize } from '../middleware/auth';
|
||||
|
||||
// Multer en mémoire pour l'import CSV/XLSX
|
||||
const uploadMemory = multer({
|
||||
storage: multer.memoryStorage(),
|
||||
limits: { fileSize: 5 * 1024 * 1024 }, // 5 Mo max
|
||||
fileFilter: (_req, file, cb) => {
|
||||
const allowed = [
|
||||
'text/csv',
|
||||
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
||||
'application/vnd.ms-excel',
|
||||
'text/plain',
|
||||
];
|
||||
if (allowed.includes(file.mimetype) || file.originalname.match(/\.(csv|xlsx|xls)$/i)) {
|
||||
cb(null, true);
|
||||
} else {
|
||||
cb(new Error('Format non supporté. Utilisez .csv ou .xlsx'));
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
const router = Router();
|
||||
|
||||
// POST /api/auth/login
|
||||
@@ -195,4 +216,101 @@ router.delete('/users/:id', authenticate, authorize('admin'), async (req: AuthRe
|
||||
}
|
||||
});
|
||||
|
||||
// GET /api/auth/users/import-template - Télécharger le modèle CSV
|
||||
router.get('/users/import-template', authenticate, authorize('admin'), (_req: AuthRequest, res: Response) => {
|
||||
const csvContent = 'email,prenom,nom,role\nadmin@exemple.com,Jean,Dupont,admin\nutilisateur@exemple.com,Marie,Martin,operateur\n';
|
||||
res.setHeader('Content-Type', 'text/csv; charset=utf-8');
|
||||
res.setHeader('Content-Disposition', 'attachment; filename="modele_import_utilisateurs.csv"');
|
||||
res.send('\uFEFF' + csvContent); // BOM UTF-8 pour Excel
|
||||
});
|
||||
|
||||
// POST /api/auth/users/import - Import CSV/Excel utilisateurs (admin)
|
||||
router.post('/users/import', authenticate, authorize('admin'), uploadMemory.single('file'), async (req: AuthRequest, res: Response) => {
|
||||
try {
|
||||
if (!req.file) {
|
||||
return res.status(400).json({ error: 'Aucun fichier fourni' });
|
||||
}
|
||||
|
||||
const pool = getPool();
|
||||
const validRoles = ['admin', 'approbateur', 'validateur', 'operateur'];
|
||||
const results = { created: 0, updated: 0, errors: [] as string[] };
|
||||
|
||||
// Parsing CSV ou Excel
|
||||
let rows: any[][] = [];
|
||||
const ext = req.file.originalname.toLowerCase();
|
||||
|
||||
if (ext.endsWith('.csv') || req.file.mimetype === 'text/csv' || req.file.mimetype === 'text/plain') {
|
||||
// Parsing CSV
|
||||
const text = req.file.buffer.toString('utf-8').replace(/^\uFEFF/, ''); // supprimer BOM
|
||||
const lines = text.split(/\r?\n/).filter((l) => l.trim());
|
||||
rows = lines.map((line) => line.split(/[,;]/));
|
||||
} else {
|
||||
// Parsing Excel
|
||||
const workbook = XLSX.read(req.file.buffer, { type: 'buffer' });
|
||||
const sheet = workbook.Sheets[workbook.SheetNames[0]];
|
||||
rows = XLSX.utils.sheet_to_json(sheet, { header: 1 }) as any[][];
|
||||
}
|
||||
|
||||
if (rows.length < 2) {
|
||||
return res.status(400).json({ error: 'Le fichier est vide ou ne contient que l\'en-tête' });
|
||||
}
|
||||
|
||||
// Ignorer la ligne d'en-tête
|
||||
const dataRows = rows.slice(1);
|
||||
|
||||
for (let i = 0; i < dataRows.length; i++) {
|
||||
const row = dataRows[i];
|
||||
const lineNum = i + 2;
|
||||
|
||||
const email = String(row[0] || '').trim().toLowerCase();
|
||||
const prenom = String(row[1] || '').trim();
|
||||
const nom = String(row[2] || '').trim();
|
||||
const role = String(row[3] || 'operateur').trim().toLowerCase();
|
||||
|
||||
// Validation
|
||||
if (!email || !email.includes('@')) {
|
||||
results.errors.push(`Ligne ${lineNum} : email invalide ("${email}")`);
|
||||
continue;
|
||||
}
|
||||
if (!prenom || !nom) {
|
||||
results.errors.push(`Ligne ${lineNum} : prénom ou nom manquant`);
|
||||
continue;
|
||||
}
|
||||
const finalRole = validRoles.includes(role) ? role : 'operateur';
|
||||
|
||||
// Vérifier si l'utilisateur existe
|
||||
const [existing]: any = await pool.execute(
|
||||
'SELECT id FROM users WHERE email = ?',
|
||||
[email]
|
||||
);
|
||||
|
||||
if (existing.length > 0) {
|
||||
// Mise à jour
|
||||
await pool.execute(
|
||||
'UPDATE users SET first_name = ?, last_name = ?, role = ?, is_active = TRUE WHERE email = ?',
|
||||
[prenom, nom, finalRole, email]
|
||||
);
|
||||
results.updated++;
|
||||
} else {
|
||||
// Création avec mot de passe temporaire
|
||||
const tempPassword = Math.random().toString(36).slice(-8) + Math.random().toString(36).slice(-4).toUpperCase() + '!';
|
||||
const hashedPassword = await bcrypt.hash(tempPassword, 12);
|
||||
await pool.execute(
|
||||
'INSERT INTO users (email, password, first_name, last_name, role, is_active) VALUES (?, ?, ?, ?, ?, TRUE)',
|
||||
[email, hashedPassword, prenom, nom, finalRole]
|
||||
);
|
||||
results.created++;
|
||||
}
|
||||
}
|
||||
|
||||
res.json({
|
||||
message: `Import terminé : ${results.created} créé(s), ${results.updated} mis à jour, ${results.errors.length} erreur(s)`,
|
||||
...results,
|
||||
});
|
||||
} catch (error: any) {
|
||||
console.error('Erreur import users:', error);
|
||||
res.status(500).json({ error: error.message || 'Erreur serveur lors de l\'import' });
|
||||
}
|
||||
});
|
||||
|
||||
export default router;
|
||||
|
||||
Reference in New Issue
Block a user