Files
GTF/GTF/project/backend/config/server.js
2025-09-23 14:51:00 +02:00

856 lines
33 KiB
JavaScript
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
const express = require('express');
const cors = require('cors');
const sql = require('mssql');
require('dotenv').config();
const app = express();
const PORT = 3001;
// 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:5173', 'http://localhost:3000', 'http://127.0.0.1:5173'],
credentials: true
}));
app.use(express.json());
// Log de toutes les requêtes
app.use((req, res, next) => {
console.log(`${new Date().toISOString()} - ${req.method} ${req.path} depuis ${req.get('origin') || 'inconnu'}`);
if (req.query.user_email || req.query.email) {
console.log(` - Filtrage pour utilisateur: ${req.query.user_email || req.query.email}`);
}
next();
});
// Variable pour stocker la connexion et l'état de la migration
let pool = null;
let systemStatus = {
hasFormateurEmailColumn: false,
hasFormateurView: false,
canAccessFormateurView: false,
hasFormateurLocal: false,
operatingMode: 'unknown'
};
// Fonction pour se connecter à la base
async function connectDatabase() {
try {
pool = await sql.connect(dbConfig);
console.log('Base de données connectée');
// Diagnostic automatique de la structure et permissions
await checkSystemStatus();
return true;
} catch (error) {
console.error('Erreur de connexion :', error.message);
return false;
}
}
// Fonction pour vérifier l'état complet du système
async function checkSystemStatus() {
try {
// 1. Vérifier si la colonne formateur_email_fk existe
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;
// 2. Vérifier si la vue Formateurs existe
const viewCheck = await pool.request().query(`
SELECT COUNT(*) as count
FROM INFORMATION_SCHEMA.VIEWS
WHERE TABLE_NAME = 'Formateurs'
`);
systemStatus.hasFormateurView = viewCheck.recordset[0].count > 0;
// 3. Tester l'accès à la vue Formateurs si elle existe
if (systemStatus.hasFormateurView) {
try {
await pool.request().query(`SELECT TOP 1 userPrincipalName FROM [dbo].[Formateurs]`);
systemStatus.canAccessFormateurView = true;
console.log('✅ Accès à la vue Formateurs: OK');
} catch (error) {
systemStatus.canAccessFormateurView = false;
console.log('❌ Accès à la vue Formateurs: ERREUR -', error.message);
if (error.message.includes('HP-TO-O365')) {
console.log('💡 Problème de permissions sur la base HP-TO-O365');
}
}
}
// 4. Vérifier si la table formateurs_local existe et est accessible
try {
await pool.request().query(`SELECT TOP 1 * FROM formateurs_local`);
systemStatus.hasFormateurLocal = true;
console.log('✅ Table formateurs_local: OK');
} catch (error) {
systemStatus.hasFormateurLocal = false;
console.log('❌ Table formateurs_local: non accessible');
}
// 5. Déterminer le mode de fonctionnement optimal
if (systemStatus.hasFormateurEmailColumn && systemStatus.canAccessFormateurView) {
systemStatus.operatingMode = 'new_with_view';
} else if (systemStatus.hasFormateurEmailColumn && systemStatus.hasFormateurLocal) {
systemStatus.operatingMode = 'new_with_local';
} else if (systemStatus.hasFormateurEmailColumn) {
systemStatus.operatingMode = 'new_email_only';
} else {
systemStatus.operatingMode = 'legacy_hash';
}
console.log('📊 État du système:');
console.log(` - Colonne formateur_email_fk: ${systemStatus.hasFormateurEmailColumn ? '✅' : '❌'}`);
console.log(` - Vue Formateurs: ${systemStatus.hasFormateurView ? '✅' : '❌'}`);
console.log(` - Accès vue Formateurs: ${systemStatus.canAccessFormateurView ? '✅' : '❌'}`);
console.log(` - Table formateurs_local: ${systemStatus.hasFormateurLocal ? '✅' : '❌'}`);
console.log(` - Mode de fonctionnement: ${systemStatus.operatingMode}`);
} catch (error) {
console.error('Erreur lors du diagnostic:', error.message);
systemStatus.operatingMode = 'legacy_hash';
}
}
// Route de diagnostic complet
app.get('/api/diagnostic', async (req, res) => {
try {
await checkSystemStatus();
let recommendations = [];
switch (systemStatus.operatingMode) {
case 'new_with_view':
recommendations.push('✅ Système optimal - toutes les fonctionnalités disponibles');
break;
case 'new_with_local':
recommendations.push('⚠️ Fonctionne avec la table locale - pas d\'accès à la vue distante');
recommendations.push('💡 Vérifier les permissions sur HP-TO-O365 pour utiliser la vue');
break;
case 'new_email_only':
recommendations.push('⚠️ Mode dégradé - sauvegarde par email mais pas de détails formateurs');
recommendations.push('💡 Restaurer l\'accès à la vue Formateurs ou table formateurs_local');
break;
case 'legacy_hash':
recommendations.push('🔄 Mode compatibilité - utilise l\'ancien système de hash');
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 = [];
// Étape 1: Ajouter la colonne si nécessaire
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');
}
}
// Étape 2: Créer un index
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}`);
}
}
// Vérifier à nouveau l'état
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 utilisateur fonctionne !',
timestamp: new Date().toISOString(),
systemStatus
});
});
// Fonction pour générer un hash reproductible depuis un email (mode legacy)
function generateHashFromEmail(email) {
let hash = 0;
for (let i = 0; i < email.length; i++) {
const char = email.charCodeAt(i);
hash = ((hash << 5) - hash) + char;
hash = hash & hash; // Convert to 32-bit integer
}
return Math.abs(hash) % 10000 + 1000;
}
// Route pour les formateurs (adaptée au mode de fonctionnement)
app.get('/api/formateurs', async (req, res) => {
try {
const { email, search } = req.query;
switch (systemStatus.operatingMode) {
case 'new_with_view':
// Utiliser la vue Formateurs
let query = `
SELECT
userPrincipalName as email,
displayName as nom_complet,
givenname as prenom,
surname as nom,
Campus,
departement,
Jobtitle
FROM [dbo].[Formateurs]
`;
const request = pool.request();
if (email) {
query += ` WHERE userPrincipalName = @email`;
request.input('email', sql.NVarChar, email);
} else if (search) {
query += ` WHERE displayName LIKE @search OR userPrincipalName LIKE @search`;
request.input('search', sql.NVarChar, `%${search}%`);
}
query += ` ORDER BY displayName`;
const result = await request.query(query);
res.json({
success: true,
data: result.recordset,
source: 'vue_formateurs'
});
break;
case 'new_with_local':
// Utiliser la table formateurs_local
let localQuery = `
SELECT
userPrincipalName as email,
displayName as nom_complet,
givenname as prenom,
surname as nom,
Campus,
departement,
Jobtitle
FROM formateurs_local
`;
const localRequest = pool.request();
if (email) {
localQuery += ` WHERE userPrincipalName = @email`;
localRequest.input('email', sql.NVarChar, email);
} else if (search) {
localQuery += ` WHERE displayName LIKE @search OR userPrincipalName LIKE @search`;
localRequest.input('search', sql.NVarChar, `%${search}%`);
}
localQuery += ` ORDER BY displayName`;
const localResult = await localRequest.query(localQuery);
res.json({
success: true,
data: localResult.recordset,
source: 'table_locale'
});
break;
case 'new_email_only':
case 'legacy_hash':
default:
// Mode dégradé - réponse basique
if (email) {
res.json({
success: true,
data: [{
email: email,
nom_complet: email.split('@')[0] || 'Utilisateur',
prenom: 'Utilisateur',
nom: email.split('@')[0] || 'Inconnu',
source: 'fallback'
}]
});
} else {
res.json({
success: true,
data: [],
source: 'fallback'
});
}
break;
}
} catch (error) {
console.error('Erreur recherche formateurs:', error);
// Fallback en cas d'erreur
if (email) {
res.json({
success: true,
data: [{
email: email,
nom_complet: email.split('@')[0] || 'Utilisateur',
prenom: 'Utilisateur',
nom: email.split('@')[0] || 'Inconnu',
source: 'error_fallback'
}]
});
} else {
res.status(500).json({
success: false,
error: error.message
});
}
}
});
// Conversion simple en string "HH:mm:ss"
const toSQLTime = (timeStr) => {
if (!timeStr) return null;
const [hours, minutes] = timeStr.split(':').map(Number);
const d = new Date(Date.UTC(2000, 0, 1, hours, minutes, 0));
return d;
};
// Route pour sauvegarder une déclaration (ADAPTÉE AU MODE)
app.post('/api/save_declaration', async (req, res) => {
try {
const { date, activityType, hours, description, user, startTime, endTime } = req.body;
console.log('Données reçues:', { date, activityType, hours, description, user, startTime, endTime });
const heureDebutSQL = toSQLTime(startTime);
const heureFinSQL = toSQLTime(endTime);
// Validation
if (!date || !activityType || !hours || !startTime || !endTime) {
return res.status(400).json({
success: false,
error: 'Données manquantes'
});
}
if (!user || !user.email) {
return res.status(400).json({
success: false,
error: 'Email utilisateur requis'
});
}
const userEmail = user.email;
// Récupération du type de demande
const typeResult = await pool.request()
.input('activityType', sql.VarChar, activityType)
.query('SELECT id FROM types_demandes WHERE libelle = @activityType');
if (typeResult.recordset.length === 0) {
return res.status(400).json({
success: false,
error: `Type d'activité invalide: ${activityType}`
});
}
const typeDemandeId = typeResult.recordset[0].id;
// Logique selon le mode de fonctionnement
if (systemStatus.operatingMode.startsWith('new_')) {
// NOUVEAU SYSTÈME - par email
console.log(`💾 Sauvegarde avec nouveau système (email) - Mode: ${systemStatus.operatingMode}`);
// Vérifier si une déclaration existe déjà
const existingResult = await pool.request()
.input('formateurEmail', sql.NVarChar, userEmail)
.input('date', sql.Date, date)
.query('SELECT id FROM declarations WHERE formateur_email_fk = @formateurEmail AND date = @date');
const utilisateurId = 1; // À adapter selon votre logique
if (existingResult.recordset.length > 0) {
// UPDATE
await pool.request()
.input('formateurEmail', sql.NVarChar, userEmail)
.input('typeDemandeId', sql.Int, typeDemandeId)
.input('hours', sql.Float, hours)
.input('description', sql.NVarChar, description || null)
.input('date', sql.Date, date)
.input('heure_debut', sql.Time, heureDebutSQL)
.input('heure_fin', sql.Time, heureFinSQL)
.query(`
UPDATE declarations
SET type_demande_id = @typeDemandeId,
duree = @hours,
description = @description,
heure_debut = @heure_debut,
heure_fin = @heure_fin
WHERE formateur_email_fk = @formateurEmail AND date = @date
`);
console.log('✅ Déclaration mise à jour');
} else {
// INSERT
await pool.request()
.input('utilisateurId', sql.Int, utilisateurId)
.input('formateurEmail', sql.NVarChar, userEmail)
.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.Time, heureDebutSQL)
.input('heure_fin', sql.Time, heureFinSQL)
.query(`
INSERT INTO declarations (
utilisateur_id, formateur_email_fk, type_demande_id,
date, duree, description, heure_debut, heure_fin
)
VALUES (
@utilisateurId, @formateurEmail, @typeDemandeId,
@date, @hours, @description, @heure_debut, @heure_fin
)
`);
console.log('✅ Nouvelle déclaration créée');
}
res.json({
success: true,
message: `Déclaration sauvegardée avec succès (${systemStatus.operatingMode})`,
formateurEmail: userEmail
});
} else {
// ANCIEN SYSTÈME - par hash
console.log('💾 Sauvegarde avec ancien système (hash)');
const formateurNumero = generateHashFromEmail(userEmail);
// Vérifier si une déclaration existe déjà
const existingResult = await pool.request()
.input('formateurNumero', sql.Int, formateurNumero)
.input('date', sql.Date, date)
.query('SELECT id FROM declarations WHERE formateur_numero = @formateurNumero AND date = @date');
if (existingResult.recordset.length > 0) {
// UPDATE
await pool.request()
.input('utilisateurId', sql.Int, formateurNumero)
.input('typeDemandeId', sql.Int, typeDemandeId)
.input('hours', sql.Float, hours)
.input('description', sql.NVarChar, description || null)
.input('formateurNumero', sql.Int, formateurNumero)
.input('date', sql.Date, date)
.input('heure_debut', sql.Time, heureDebutSQL)
.input('heure_fin', sql.Time, heureFinSQL)
.query(`
UPDATE declarations
SET utilisateur_id = @utilisateurId,
type_demande_id = @typeDemandeId,
duree = @hours,
description = @description,
heure_debut = @heure_debut,
heure_fin = @heure_fin
WHERE formateur_numero = @formateurNumero AND date = @date
`);
console.log('✅ Déclaration mise à jour (legacy)');
} else {
// INSERT
await pool.request()
.input('utilisateurId', sql.Int, formateurNumero)
.input('formateurNumero', sql.Int, formateurNumero)
.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.Time, heureDebutSQL)
.input('heure_fin', sql.Time, heureFinSQL)
.query(`
INSERT INTO declarations (
utilisateur_id, formateur_numero, type_demande_id,
date, duree, description, heure_debut, heure_fin
)
VALUES (
@utilisateurId, @formateurNumero, @typeDemandeId,
@date, @hours, @description, @heure_debut, @heure_fin
)
`);
console.log('✅ Nouvelle déclaration créée (legacy)');
}
res.json({
success: true,
message: 'Déclaration sauvegardée avec succès (legacy)',
formateurNumero: formateurNumero
});
}
} catch (error) {
console.error('Erreur lors de la sauvegarde:', error);
res.status(500).json({
success: false,
error: error.message
});
}
});
// Route pour récupérer par email (ADAPTÉE AU MODE)
app.get('/api/get_declarations_by_email', async (req, res) => {
try {
const { email } = req.query;
console.log('DEBUG - Email reçu:', email);
if (!email) {
return res.status(400).json({ error: 'Email requis' });
}
switch (systemStatus.operatingMode) {
case 'new_with_view':
// Avec vue Formateurs
const resultWithView = await pool.request()
.input('email', sql.NVarChar, email)
.query(`
SELECT
d.id,
d.utilisateur_id,
d.formateur_email_fk as formateur_email,
f.displayName as formateur_nom,
f.Campus,
f.departement,
td.id as type_demande_id,
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 [dbo].[Formateurs] f ON d.formateur_email_fk = f.userPrincipalName
WHERE d.formateur_email_fk = @email
ORDER BY d.date DESC
`);
res.json(resultWithView.recordset);
break;
case 'new_with_local':
// Avec table formateurs_local
const resultWithLocal = await pool.request()
.input('email', sql.NVarChar, email)
.query(`
SELECT
d.id,
d.utilisateur_id,
d.formateur_email_fk as formateur_email,
f.displayName as formateur_nom,
f.Campus,
f.departement,
td.id as type_demande_id,
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
`);
res.json(resultWithLocal.recordset);
break;
case 'new_email_only':
// Sans jointure formateur
const resultEmailOnly = await pool.request()
.input('email', sql.NVarChar, email)
.query(`
SELECT
d.id,
d.utilisateur_id,
d.formateur_email_fk as formateur_email,
td.id as type_demande_id,
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
WHERE d.formateur_email_fk = @email
ORDER BY d.date DESC
`);
res.json(resultEmailOnly.recordset);
break;
case 'legacy_hash':
default:
// Ancien système avec hash
const formateurNumero = generateHashFromEmail(email);
console.log(`Email ${email} -> Numéro ${formateurNumero}`);
const resultLegacy = await pool.request()
.input('formateur_numero', sql.Int, formateurNumero)
.query(`
SELECT
d.id,
d.formateur_numero as utilisateur_id,
td.id as type_demande_id,
d.date,
d.duree,
d.heure_debut,
d.heure_fin,
d.description,
td.libelle as activityType,
d.status
FROM declarations d
INNER JOIN types_demandes td ON d.type_demande_id = td.id
WHERE d.formateur_numero = @formateur_numero
ORDER BY d.date DESC
`);
res.json(resultLegacy.recordset);
break;
}
console.log(`✅ Déclarations récupérées pour ${email} (mode: ${systemStatus.operatingMode})`);
} catch (error) {
console.error('Erreur lors de la récupération par email:', error.message);
res.status(500).json({ error: error.message });
}
});
// Route pour récupérer toutes les déclarations
app.get('/api/get_declarations', async (req, res) => {
try {
const { user_email, admin } = req.query;
// Utiliser la même logique que pour get_declarations_by_email
if (user_email && !admin) {
// Rediriger vers get_declarations_by_email
req.query = { email: user_email };
return app._router.handle(req, res);
}
// Mode admin - récupérer toutes les déclarations
let result;
switch (systemStatus.operatingMode) {
case 'new_with_view':
result = await pool.request().query(`
SELECT
d.id,
d.utilisateur_id,
d.formateur_email_fk as formateur_email,
f.displayName as formateur_nom,
f.Campus,
f.departement,
td.id as type_demande_id,
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 [dbo].[Formateurs] f ON d.formateur_email_fk = f.userPrincipalName
ORDER BY d.date DESC
`);
break;
case 'new_with_local':
result = await pool.request().query(`
SELECT
d.id,
d.utilisateur_id,
d.formateur_email_fk as formateur_email,
f.displayName as formateur_nom,
f.Campus,
f.departement,
td.id as type_demande_id,
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
`);
break;
case 'new_email_only':
result = await pool.request().query(`
SELECT
d.id,
d.utilisateur_id,
d.formateur_email_fk as formateur_email,
td.id as type_demande_id,
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
ORDER BY d.date DESC
`);
break;
default:
result = await pool.request().query(`
SELECT
d.id,
d.formateur_numero as utilisateur_id,
td.id as type_demande_id,
d.date,
d.duree,
d.heure_debut,
d.heure_fin,
d.description,
d.formateur_numero,
td.libelle as activityType,
d.status
FROM declarations d
INNER JOIN types_demandes td ON d.type_demande_id = td.id
ORDER BY d.date DESC
`);
break;
}
res.json(result.recordset);
} catch (error) {
console.error('Erreur lors de la récupération:', error);
res.status(500).json({ error: error.message });
}
});
// Route de création de formateur (pour compatibilité)
app.post('/api/create_formateur', (req, res) => {
res.json({
success: true,
message: 'Route de compatibilité - utilisez /api/formateurs'
});
});
// 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, () => {
console.log(`🚀 Serveur UTILISATEUR démarré sur http://localhost:${PORT}`);
console.log(`📊 Mode de fonctionnement: ${systemStatus.operatingMode}`);
console.log('');
console.log('Routes disponibles :');
console.log('- GET /api/diagnostic (vérifier l\'état)');
console.log('- POST /api/migrate (appliquer la migration)');
console.log('- GET /api/test');
console.log('- GET /api/formateurs?email=test@example.com');
console.log('- POST /api/save_declaration');
console.log('- GET /api/get_declarations');
console.log('- GET /api/get_declarations_by_email?email=test@example.com');
console.log('');
switch (systemStatus.operatingMode) {
case 'new_with_view':
console.log('✅ Système optimal - utilise la vue Formateurs');
break;
case 'new_with_local':
console.log('⚠️ Mode dégradé - utilise la table formateurs_local');
console.log('💡 Conseil: Vérifier les permissions sur HP-TO-O365');
break;
case 'new_email_only':
console.log('⚠️ Mode minimal - sauvegarde par email sans détails formateurs');
break;
case 'legacy_hash':
console.log('🔄 Mode compatibilité - utilise l\'ancien système de hash');
console.log('💡 Conseil: Appliquer la migration avec POST /api/migrate');
break;
}
});
}
// Arrêt propre
process.on('SIGINT', async () => {
console.log('Arrêt du serveur utilisateur...');
if (pool) {
await pool.close();
}
process.exit(0);
});
// Démarrer
startServer()