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 router.post('/login', async (req: Request, res: Response) => { try { const { email, password } = req.body; if (!email || !password) { return res.status(400).json({ error: 'Email et mot de passe requis' }); } const pool = getPool(); const [rows]: any = await pool.execute( 'SELECT * FROM users WHERE email = ? AND is_active = TRUE', [email] ); if (!rows.length) { return res.status(401).json({ error: 'Identifiants incorrects' }); } const user = rows[0]; const validPassword = await bcrypt.compare(password, user.password); if (!validPassword) { return res.status(401).json({ error: 'Identifiants incorrects' }); } const secret = process.env.JWT_SECRET || 'santinova-jwt-secret'; const expiresIn = process.env.JWT_EXPIRES_IN || '24h'; const token = (jwt.sign as any)( { id: user.id, email: user.email, role: user.role, firstName: user.first_name, lastName: user.last_name, }, secret, { expiresIn } ); res.json({ token, user: { id: user.id, email: user.email, firstName: user.first_name, lastName: user.last_name, role: user.role, }, }); } catch (error: any) { console.error('Erreur login:', error); res.status(500).json({ error: 'Erreur serveur' }); } }); // GET /api/auth/me router.get('/me', authenticate, async (req: AuthRequest, res: Response) => { try { const pool = getPool(); const [rows]: any = await pool.execute( 'SELECT id, email, first_name, last_name, role, is_active, created_at FROM users WHERE id = ?', [req.user!.id] ); if (!rows.length) { return res.status(404).json({ error: 'Utilisateur non trouvé' }); } const user = rows[0]; res.json({ id: user.id, email: user.email, firstName: user.first_name, lastName: user.last_name, role: user.role, isActive: user.is_active, createdAt: user.created_at, }); } catch (error: any) { console.error('Erreur me:', error); res.status(500).json({ error: 'Erreur serveur' }); } }); // GET /api/auth/users - Liste des utilisateurs (admin) router.get('/users', authenticate, authorize('admin'), async (req: AuthRequest, res: Response) => { try { const pool = getPool(); const [rows]: any = await pool.execute( 'SELECT id, email, first_name, last_name, role, is_active, created_at, updated_at FROM users ORDER BY created_at DESC' ); const users = rows.map((u: any) => ({ id: u.id, email: u.email, firstName: u.first_name, lastName: u.last_name, role: u.role, isActive: u.is_active, createdAt: u.created_at, updatedAt: u.updated_at, })); res.json(users); } catch (error: any) { console.error('Erreur liste users:', error); res.status(500).json({ error: 'Erreur serveur' }); } }); // POST /api/auth/users - Créer un utilisateur (admin) router.post('/users', authenticate, authorize('admin'), async (req: AuthRequest, res: Response) => { try { const { email, password, firstName, lastName, role } = req.body; if (!email || !password || !firstName || !lastName) { return res.status(400).json({ error: 'Tous les champs sont requis' }); } const hashedPassword = await bcrypt.hash(password, 12); const pool = getPool(); const [result]: any = await pool.execute( 'INSERT INTO users (email, password, first_name, last_name, role) VALUES (?, ?, ?, ?, ?)', [email, hashedPassword, firstName, lastName, role || 'standard'] ); res.status(201).json({ id: result.insertId, email, firstName, lastName, role: role || 'standard', }); } catch (error: any) { if (error.code === 'ER_DUP_ENTRY') { return res.status(409).json({ error: 'Cet email est déjà utilisé' }); } console.error('Erreur création user:', error); res.status(500).json({ error: 'Erreur serveur' }); } }); // PUT /api/auth/users/:id - Modifier un utilisateur (admin) router.put('/users/:id', authenticate, authorize('admin'), async (req: AuthRequest, res: Response) => { try { const { id } = req.params; const { email, firstName, lastName, role, isActive, password } = req.body; const pool = getPool(); let query = 'UPDATE users SET email = ?, first_name = ?, last_name = ?, role = ?, is_active = ?'; let params: any[] = [email, firstName, lastName, role, isActive !== false]; if (password) { const hashedPassword = await bcrypt.hash(password, 12); query += ', password = ?'; params.push(hashedPassword); } query += ' WHERE id = ?'; params.push(id); await pool.execute(query, params); res.json({ message: 'Utilisateur mis à jour' }); } catch (error: any) { console.error('Erreur update user:', error); res.status(500).json({ error: 'Erreur serveur' }); } }); // DELETE /api/auth/users/:id - Désactiver un utilisateur (admin) router.delete('/users/:id', authenticate, authorize('admin'), async (req: AuthRequest, res: Response) => { try { const { id } = req.params; const pool = getPool(); await pool.execute('UPDATE users SET is_active = FALSE WHERE id = ?', [id]); res.json({ message: 'Utilisateur désactivé' }); } catch (error: any) { console.error('Erreur delete user:', error); res.status(500).json({ error: 'Erreur serveur' }); } }); // 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\nstandard@exemple.com,Marie,Martin,standard\nreadonly@exemple.com,Paul,Durand,readonly\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', 'standard', 'readonly']; 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] || 'standard').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 : 'standard'; // 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;