const express = require('express'); const cors = require('cors'); const sql = require('mssql'); require('dotenv').config(); const app = express(); const PORT = process.env.PORT || 3000; // Configuration base de données const dbConfig = { server: process.env.DB_SERVER, database: process.env.DB_DATABASE, user: process.env.DB_USER, password: process.env.DB_PASSWORD, options: { encrypt: true, trustServerCertificate: true, enableArithAbort: true } }; // Middleware app.use(cors({ origin: [ 'http://localhost:3001', 'http://localhost:8000', 'http://127.0.0.1:3001', 'https://mygtf.ensup-adm.net', 'https://mygtf.ensup-adm.net:3001', 'https://mygtf.ensup-adm.net:8000', 'http://mygtf.ensup-adm.net', 'http://nginx' ], credentials: true })); app.use(express.json()); // Log de toutes les requêtes app.use((req, res, next) => { console.log(`\n${'='.repeat(80)}`); console.log(`${new Date().toISOString()} - ${req.method} ${req.path}`); console.log(`Origin: ${req.get('origin') || 'inconnu'}`); if (req.query.user_email || req.query.email) { console.log(`Paramètres: ${JSON.stringify(req.query)}`); } if (req.body && Object.keys(req.body).length > 0) { console.log(`Body: ${JSON.stringify(req.body, null, 2)}`); } console.log(`${'='.repeat(80)}\n`); next(); }); // Variable pour stocker la connexion et l'état let pool = null; let systemStatus = { hasFormateurEmailColumn: false, hasFormateurLocal: false, operatingMode: 'unknown' }; // Fonction pour se connecter à la base async function connectDatabase() { try { console.log('🔌 Tentative de connexion à la base de données...'); console.log(`Server: ${dbConfig.server}`); console.log(`Database: ${dbConfig.database}`); console.log(`User: ${dbConfig.user}`); pool = await sql.connect(dbConfig); console.log('✅ Base de données connectée avec succès\n'); await checkSystemStatus(); return true; } catch (error) { console.error('❌ Erreur de connexion à la base:', error.message); console.error('Stack:', error.stack); return false; } } // Fonction pour vérifier l'état du système - MODE LOCAL UNIQUEMENT async function checkSystemStatus() { console.log('\n📊 === DIAGNOSTIC DU SYSTÈME (MODE LOCAL ONLY) ===\n'); try { // 1. Vérifier si la colonne formateur_email_fk existe console.log('1️⃣ Vérification colonne formateur_email_fk...'); const columnCheck = await pool.request().query(` SELECT COUNT(*) as count FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_NAME = 'declarations' AND COLUMN_NAME = 'formateur_email_fk' `); systemStatus.hasFormateurEmailColumn = columnCheck.recordset[0].count > 0; console.log(` ${systemStatus.hasFormateurEmailColumn ? '✅' : '❌'} Colonne formateur_email_fk: ${systemStatus.hasFormateurEmailColumn ? 'EXISTE' : 'N\'EXISTE PAS'}`); // 2. Vérifier/Créer la table formateurs_local console.log('\n2️⃣ Vérification table formateurs_local...'); try { await pool.request().query(` IF NOT EXISTS (SELECT * FROM sysobjects WHERE name='formateurs_local' AND xtype='U') CREATE TABLE formateurs_local ( id INT IDENTITY(1,1) PRIMARY KEY, Civilite NVARCHAR(10), Nom_brut NVARCHAR(255), givenname NVARCHAR(100), alternateEmailAddress NVARCHAR(255), Contrat NVARCHAR(100), userPrincipalName NVARCHAR(255) UNIQUE NOT NULL, surname NVARCHAR(100), mailnickname NVARCHAR(100), Password NVARCHAR(255), displayName NVARCHAR(255), Campus NVARCHAR(50), Jobtitle NVARCHAR(100), departement NVARCHAR(100) NOT NULL DEFAULT 'Non défini', role NVARCHAR(100) ) `); const countLocal = await pool.request().query(`SELECT COUNT(*) as count FROM formateurs_local`); systemStatus.hasFormateurLocal = true; console.log(' ✅ Table formateurs_local: OK'); console.log(` 📋 Nombre d'entrées: ${countLocal.recordset[0].count}`); if (countLocal.recordset[0].count > 0) { const localEntries = await pool.request().query(` SELECT TOP 10 * FROM formateurs_local ORDER BY userPrincipalName `); console.log(' 📄 Derniers formateurs locaux:'); localEntries.recordset.forEach((f, i) => { console.log(` ${i + 1}. ${f.Nom_brut || f.displayName} (${f.userPrincipalName}) - Campus: ${f.Campus}`); }); } else { console.log(' ⚠️ Aucun formateur dans la table locale'); } } catch (error) { systemStatus.hasFormateurLocal = false; console.log(' ⚠️ Table formateurs_local: ERREUR'); console.log(` Erreur: ${error.message}`); } // 3. Déterminer le mode de fonctionnement console.log('\n3️⃣ Détermination du mode de fonctionnement...'); if (systemStatus.hasFormateurEmailColumn && systemStatus.hasFormateurLocal) { systemStatus.operatingMode = 'local_only'; console.log(' ✅ Mode: LOCAL_ONLY (table formateurs_local uniquement)'); } else if (systemStatus.hasFormateurEmailColumn) { systemStatus.operatingMode = 'new_email_only'; console.log(' ⚠️ Mode: NEW_EMAIL_ONLY (dégradé)'); } else { systemStatus.operatingMode = 'legacy_hash'; console.log(' ⚠️ Mode: LEGACY_HASH (ancien système)'); } console.log('\n📊 === RÉSUMÉ DU DIAGNOSTIC ==='); console.log(` - Colonne formateur_email_fk: ${systemStatus.hasFormateurEmailColumn ? '✅' : '❌'}`); console.log(` - Table formateurs_local: ${systemStatus.hasFormateurLocal ? '✅' : '❌'}`); console.log(` - Mode de fonctionnement: ${systemStatus.operatingMode}`); console.log(` - Vue v_Formateurs_CD: DÉSACTIVÉE (mode local)`); console.log('================================\n'); } catch (error) { console.error('❌ Erreur lors du diagnostic:', error.message); console.error('Stack:', error.stack); systemStatus.operatingMode = 'legacy_hash'; } } // Route de diagnostic app.get('/api/diagnostic', async (req, res) => { try { await checkSystemStatus(); let recommendations = []; switch (systemStatus.operatingMode) { case 'local_only': recommendations.push('✅ Système en mode LOCAL - utilisation de la table formateurs_local'); recommendations.push('ℹ️ La vue v_Formateurs_CD est désactivée'); break; case 'new_email_only': recommendations.push('⚠️ Mode dégradé - sauvegarde par email uniquement'); recommendations.push('💡 Créer et remplir la table formateurs_local'); break; case 'legacy_hash': recommendations.push('🔄 Mode compatibilité - utilise l\'ancien système'); recommendations.push('💡 Appliquer la migration avec POST /api/migrate'); break; } res.json({ systemStatus, recommendations, currentMode: systemStatus.operatingMode }); } catch (error) { res.status(500).json({ error: error.message }); } }); // Route pour appliquer la migration app.post('/api/migrate', async (req, res) => { try { const steps = []; if (!systemStatus.hasFormateurEmailColumn) { try { await pool.request().query(` ALTER TABLE [dbo].[declarations] ADD [formateur_email_fk] [nvarchar](255) NULL `); steps.push('✅ Colonne formateur_email_fk ajoutée'); } catch (error) { if (!error.message.includes('already exists')) { throw error; } steps.push('ℹ️ Colonne formateur_email_fk déjà existante'); } } try { await pool.request().query(` CREATE NONCLUSTERED INDEX [IX_declarations_formateur_email_fk] ON [dbo].[declarations] ([formateur_email_fk]) `); steps.push('✅ Index créé'); } catch (error) { if (error.message.includes('already exists')) { steps.push('ℹ️ Index déjà existant'); } else { steps.push(`⚠️ Erreur index: ${error.message}`); } } await checkSystemStatus(); res.json({ success: true, steps, newStatus: systemStatus, message: `Migration appliquée - Mode: ${systemStatus.operatingMode}` }); } catch (error) { res.status(500).json({ success: false, error: error.message, message: 'Erreur lors de la migration' }); } }); // Route de test app.get('/api/test', (req, res) => { res.json({ message: 'Le serveur fonctionne en mode LOCAL ONLY !', timestamp: new Date().toISOString(), systemStatus }); }); // Route pour les formateurs - MODE LOCAL SIMPLIFIÉ app.get('/api/formateurs', async (req, res) => { console.log('\n🔍 === ROUTE /api/formateurs (LOCAL ONLY - SIMPLIFIÉ) ==='); const { email, search } = req.query; console.log(`📥 Paramètres reçus:`); console.log(` - email: ${email || 'non fourni'}`); console.log(` - search: ${search || 'non fourni'}`); try { // Requête simple et directe let query = ` SELECT userPrincipalName as email, ISNULL(Nom_brut, displayName) as nom_complet, Campus, Contrat FROM formateurs_local `; const request = pool.request(); if (email) { query += ` WHERE userPrincipalName = @email`; request.input('email', sql.NVarChar, email); console.log(`🔎 Recherche par email: ${email}`); } else if (search) { query += ` WHERE Nom_brut LIKE @search OR displayName LIKE @search OR userPrincipalName LIKE @search`; request.input('search', sql.NVarChar, `%${search}%`); console.log(`🔎 Recherche par texte: ${search}`); } query += ` ORDER BY ISNULL(Nom_brut, displayName)`; console.log('🚀 Exécution de la requête SQL...'); console.log(`📝 Query: ${query.substring(0, 200)}...`); const result = await request.query(query); console.log(`✅ ${result.recordset.length} formateur(s) trouvé(s)`); if (result.recordset.length > 0) { console.log('📄 Premier résultat:', result.recordset[0]); } return res.json({ success: true, data: result.recordset, source: 'formateurs_local', mode: 'local_only' }); } catch (error) { console.error('❌ ========== ERREUR /api/formateurs =========='); console.error(`Message: ${error.message}`); console.error(`Code: ${error.code}`); console.error(`Stack: ${error.stack}`); console.error('=============================================='); return res.status(500).json({ success: false, error: 'Erreur lors de la récupération des formateurs', message: error.message, code: error.code }); } }); // Conversion Time const toSQLTime = (timeStr) => { if (!timeStr) return null; const [hours, minutes] = timeStr.split(':').map(Number); return `${hours.toString().padStart(2, '0')}:${minutes.toString().padStart(2, '0')}:00`; }; // Route pour sauvegarder une déclaration app.post('/api/save_declaration', async (req, res) => { console.log('\n💾 === ROUTE /api/save_declaration ==='); try { const { id, date, activityType, hours, description, user, startTime, endTime } = req.body; console.log('📥 Données reçues:'); console.log(` - id: ${id || 'nouveau'}`); console.log(` - date: ${date}`); console.log(` - user.email: ${user?.email}`); if (!user?.email) { console.error('❌ Email requis manquant'); return res.status(400).json({ error: 'Email requis' }); } const heureDebutSQL = toSQLTime(startTime); const heureFinSQL = toSQLTime(endTime); // Récupérer l'ID du type "preparation" const typeResult = await pool.request() .query("SELECT id FROM types_demandes WHERE libelle = 'preparation'"); if (typeResult.recordset.length === 0) { return res.status(500).json({ error: 'Configuration manquante: le type "preparation" n\'existe pas.' }); } const typeDemandeId = typeResult.recordset[0].id; // Vérifier le campus et la clôture const moisAnnee = date.substring(0, 7); let campusFormateur = null; console.log(`🏫 Recherche du campus pour: ${user.email}`); try { const formateurInfo = await pool.request() .input('email', sql.NVarChar, user.email) .query('SELECT Campus FROM formateurs_local WHERE userPrincipalName = @email'); if (formateurInfo.recordset.length > 0) { campusFormateur = formateurInfo.recordset[0].Campus; console.log(` ✅ Campus trouvé: ${campusFormateur}`); } } catch (error) { console.log(' ⚠️ Erreur récupération campus:', error.message); } // Vérifier clôture try { const clotureCheck = await pool.request() .input('mois_annee', sql.VarChar, moisAnnee) .input('campus', sql.VarChar, campusFormateur) .query(` SELECT id, date_debut, date_fin, commentaire, campus FROM clotures_saisie WHERE mois_annee = @mois_annee AND (campus = @campus OR campus IS NULL) `); if (clotureCheck.recordset.length > 0) { const cloture = clotureCheck.recordset[0]; const campusMessage = cloture.campus ? ` (Campus: ${cloture.campus})` : ' (Tous les campus)'; return res.status(403).json({ success: false, error: `⛔ Cette période est clôturée${campusMessage}.`, cloture: cloture }); } } catch (error) { console.log('⚠️ Pas de vérification de clôture'); } // Validation formateur console.log('👤 Vérification formateur dans formateurs_local...'); const formateur = await pool.request() .input('email', sql.NVarChar, user.email) .query('SELECT userPrincipalName FROM formateurs_local WHERE userPrincipalName = @email'); if (formateur.recordset.length === 0) { console.error(`❌ Formateur ${user.email} non trouvé dans formateurs_local`); return res.status(403).json({ error: 'Formateur non trouvé dans la table locale. Veuillez contacter l\'administrateur.', email: user.email }); } const formateurEmail = formateur.recordset[0].userPrincipalName; console.log(`✅ Formateur validé: ${formateurEmail}`); if (id) { // MODIFICATION console.log(`✏️ MODIFICATION de la déclaration ID: ${id}`); await pool.request() .input('id', sql.Int, id) .input('typeDemandeId', sql.Int, typeDemandeId) .input('hours', sql.Float, hours) .input('description', sql.NVarChar, description || null) .input('heure_debut', sql.VarChar, heureDebutSQL) .input('heure_fin', sql.VarChar, heureFinSQL) .query(` UPDATE declarations SET type_demande_id=@typeDemandeId, duree=@hours, description=@description, heure_debut=@heure_debut, heure_fin=@heure_fin WHERE id=@id `); console.log('✅ Déclaration modifiée'); } else { // CRÉATION console.log(`➕ CRÉATION nouvelle déclaration`); await pool.request() .input('formateurEmail', sql.NVarChar, formateurEmail) .input('typeDemandeId', sql.Int, typeDemandeId) .input('date', sql.Date, date) .input('hours', sql.Float, hours) .input('description', sql.NVarChar, description || null) .input('heure_debut', sql.VarChar, heureDebutSQL) .input('heure_fin', sql.VarChar, heureFinSQL) .query(` INSERT INTO declarations (formateur_email_fk, type_demande_id, date, duree, description, heure_debut, heure_fin) VALUES (@formateurEmail, @typeDemandeId, @date, @hours, @description, @heure_debut, @heure_fin) `); console.log('✅ Déclaration créée'); } res.json({ success: true, message: 'Déclaration sauvegardée' }); } catch (error) { console.error("❌ ERREUR save_declaration:", error.message); res.status(500).json({ error: error.message }); } }); // Récupérer déclarations par email app.get('/api/get_declarations_by_email', async (req, res) => { console.log('\n📋 === ROUTE /api/get_declarations_by_email ==='); try { const { email } = req.query; console.log(`📥 Email: ${email}`); if (!email) { return res.status(400).json({ error: 'Email requis' }); } const query = ` SELECT d.id, d.formateur_email_fk AS formateur_email, f.Nom_brut AS formateur_nom, f.Campus, td.libelle AS activityType, d.date, d.duree, d.heure_debut, d.heure_fin, d.description, d.status FROM declarations d INNER JOIN types_demandes td ON d.type_demande_id = td.id LEFT JOIN formateurs_local f ON d.formateur_email_fk = f.userPrincipalName WHERE d.formateur_email_fk = @email ORDER BY d.date DESC `; const result = await pool.request() .input('email', sql.NVarChar, email) .query(query); console.log(`✅ ${result.recordset.length} déclaration(s) trouvée(s)`); res.json(result.recordset); } catch (error) { console.error('❌ ERREUR:', error.message); res.status(500).json({ error: error.message }); } }); // Récupérer toutes les déclarations app.get('/api/get_declarations', async (req, res) => { console.log('\n📋 === ROUTE /api/get_declarations ==='); try { const { user_email, admin } = req.query; if (user_email && !admin) { return app._router.handle({ ...req, query: { email: user_email } }, res); } const query = ` SELECT d.id, d.formateur_email_fk AS formateur_email, f.Nom_brut AS formateur_nom, f.Campus, td.libelle AS activityType, d.date, d.duree, d.heure_debut, d.heure_fin, d.description, d.status FROM declarations d INNER JOIN types_demandes td ON d.type_demande_id = td.id LEFT JOIN formateurs_local f ON d.formateur_email_fk = f.userPrincipalName ORDER BY d.date DESC `; const result = await pool.request().query(query); console.log(`✅ ${result.recordset.length} déclaration(s)`); res.json(result.recordset); } catch (error) { console.error('❌ ERREUR:', error.message); res.status(500).json({ error: error.message }); } }); // Supprimer une déclaration app.delete('/api/delete_declaration/:id', async (req, res) => { console.log('\n🗑️ === ROUTE /api/delete_declaration ==='); try { const { id } = req.params; if (!id) { return res.status(400).json({ error: 'ID requis' }); } const existing = await pool.request() .input('id', sql.Int, id) .query('SELECT * FROM declarations WHERE id = @id'); if (existing.recordset.length === 0) { return res.status(404).json({ error: 'Déclaration non trouvée' }); } await pool.request() .input('id', sql.Int, id) .query('DELETE FROM declarations WHERE id = @id'); console.log(`✅ Déclaration ${id} supprimée`); res.json({ success: true, message: 'Déclaration supprimée' }); } catch (error) { console.error('❌ ERREUR:', error.message); res.status(500).json({ error: error.message }); } }); // Log connexion app.post('/api/log-connexion', async (req, res) => { const quickResponse = () => { return res.json({ success: true, message: 'Connexion autorisée', logged: false }); }; try { const { email, nom, statut, messageErreur, campus } = req.body; if (!email) { return quickResponse(); } const ip = req.headers['x-forwarded-for'] || req.connection.remoteAddress || 'unknown'; const userAgent = req.headers['user-agent'] || 'unknown'; let estDansVue = false; try { const checkFormateur = await pool.request() .input('email', sql.NVarChar, email) .query(`SELECT COUNT(*) as count FROM formateurs_local WHERE userPrincipalName = @email`); estDansVue = checkFormateur.recordset[0].count > 0; } catch (error) { console.error('⚠️ Erreur vérification formateur'); } try { await pool.request().query(` IF NOT EXISTS (SELECT * FROM sysobjects WHERE name='logs_connexion' AND xtype='U') CREATE TABLE logs_connexion ( id INT IDENTITY(1,1) PRIMARY KEY, email_utilisateur NVARCHAR(255) NOT NULL, nom_utilisateur NVARCHAR(255), date_connexion DATETIME DEFAULT GETDATE(), adresse_ip NVARCHAR(50), user_agent NVARCHAR(500), statut_connexion VARCHAR(20), message_erreur NVARCHAR(MAX), est_dans_vue BIT DEFAULT 0, campus NVARCHAR(50) ) `); } catch (error) { // Table existe } try { await pool.request() .input('email', sql.NVarChar, email) .input('nom', sql.NVarChar, nom || null) .input('ip', sql.NVarChar, ip.substring(0, 50)) .input('userAgent', sql.NVarChar, userAgent.substring(0, 500)) .input('statut', sql.VarChar, (statut || 'success').substring(0, 20)) .input('messageErreur', sql.NVarChar, messageErreur || null) .input('estDansVue', sql.Bit, estDansVue) .input('campus', sql.NVarChar, campus || null) .query(` INSERT INTO logs_connexion (email_utilisateur, nom_utilisateur, adresse_ip, user_agent, statut_connexion, message_erreur, est_dans_vue, campus) VALUES (@email, @nom, @ip, @userAgent, @statut, @messageErreur, @estDansVue, @campus) `); return res.json({ success: true, message: 'Connexion enregistrée', logged: true, estDansVue: estDansVue }); } catch (error) { return quickResponse(); } } catch (error) { return quickResponse(); } }); // Logs connexion app.get('/api/logs-connexion', async (req, res) => { try { const { email, dateDebut, dateFin, limit = 100, statut, campus } = req.query; let query = `SELECT * FROM logs_connexion WHERE 1=1`; const request = pool.request(); if (email) { query += ` AND email_utilisateur = @email`; request.input('email', sql.NVarChar, email); } if (dateDebut) { query += ` AND date_connexion >= @dateDebut`; request.input('dateDebut', sql.DateTime, dateDebut); } if (dateFin) { query += ` AND date_connexion <= @dateFin`; request.input('dateFin', sql.DateTime, dateFin); } if (statut) { query += ` AND statut_connexion = @statut`; request.input('statut', sql.VarChar, statut); } if (campus) { query += ` AND campus = @campus`; request.input('campus', sql.NVarChar, campus); } query += ` ORDER BY date_connexion DESC`; const limitNum = parseInt(limit); if (limitNum > 0) { query = `SELECT TOP ${limitNum} * FROM (${query}) AS subquery`; } const result = await request.query(query); res.json({ success: true, logs: result.recordset, total: result.recordset.length }); } catch (error) { res.status(500).json({ success: false, error: error.message }); } }); // Stats connexion app.get('/api/stats-connexion', async (req, res) => { try { const { dateDebut, dateFin } = req.query; const request = pool.request(); let whereClause = ''; if (dateDebut) { whereClause += ' AND date_connexion >= @dateDebut'; request.input('dateDebut', sql.DateTime, dateDebut); } if (dateFin) { whereClause += ' AND date_connexion <= @dateFin'; request.input('dateFin', sql.DateTime, dateFin); } const stats = await request.query(` SELECT COUNT(*) as total_connexions, COUNT(DISTINCT email_utilisateur) as utilisateurs_uniques, SUM(CASE WHEN statut_connexion = 'success' THEN 1 ELSE 0 END) as connexions_reussies, SUM(CASE WHEN statut_connexion = 'failed' THEN 1 ELSE 0 END) as connexions_echouees FROM logs_connexion WHERE 1=1 ${whereClause} `); res.json({ success: true, statistiques: stats.recordset[0] }); } catch (error) { res.status(500).json({ success: false, error: error.message }); } }); // Vérifier clôture app.get('/api/check-cloture', async (req, res) => { try { const { date, campus } = req.query; if (!date) { return res.status(400).json({ error: 'Date requise' }); } const moisAnnee = date.substring(0, 7); try { await pool.request().query(` IF NOT EXISTS (SELECT * FROM sysobjects WHERE name='clotures_saisie' AND xtype='U') CREATE TABLE clotures_saisie ( id INT IDENTITY(1,1) PRIMARY KEY, mois_annee VARCHAR(7) NOT NULL, date_debut DATE NOT NULL, date_fin DATE NOT NULL, date_cloture DATETIME DEFAULT GETDATE(), commentaire NVARCHAR(500), campus VARCHAR(50) ) `); } catch (error) { // Table existe } const result = await pool.request() .input('mois_annee', sql.VarChar, moisAnnee) .input('campus', sql.VarChar, campus || null) .query(` SELECT * FROM clotures_saisie WHERE mois_annee = @mois_annee AND (campus = @campus OR campus IS NULL OR @campus IS NULL) `); res.json({ cloture: result.recordset.length > 0, details: result.recordset[0] || null }); } catch (error) { res.status(500).json({ error: error.message }); } }); // Liste clôtures app.get('/api/clotures', async (req, res) => { try { const result = await pool.request().query(` SELECT * FROM clotures_saisie ORDER BY date_cloture DESC `); res.json({ success: true, clotures: result.recordset }); } catch (error) { res.status(500).json({ success: false, error: error.message }); } }); // Démarrage du serveur async function startServer() { const dbConnected = await connectDatabase(); if (!dbConnected) { console.log('❌ Impossible de démarrer sans base de données'); return; } app.listen(PORT, '0.0.0.0', () => { console.log(`\n${'='.repeat(80)}`); console.log(`🚀 SERVEUR DÉMARRÉ - MODE LOCAL ONLY`); console.log(`${'='.repeat(80)}`); console.log(`URL: http://localhost:${PORT}`); console.log(`Mode: ${systemStatus.operatingMode}`); console.log(`Source: formateurs_local UNIQUEMENT`); console.log(`${'='.repeat(80)}\n`); }); } // Arrêt propre process.on('SIGINT', async () => { console.log('\n⏹️ Arrêt du serveur...'); if (pool) { await pool.close(); console.log('✅ Connexion fermée'); } process.exit(0); }); startServer();