1768 lines
61 KiB
JavaScript
1768 lines
61 KiB
JavaScript
const express = require('express');
|
||
const cors = require('cors');
|
||
const sql = require('mssql');
|
||
const axios = require('axios');
|
||
require('dotenv').config();
|
||
|
||
const app = express();
|
||
const PORT = 3002;
|
||
|
||
// 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
|
||
}
|
||
};
|
||
|
||
// Configuration Microsoft OAuth
|
||
const CLIENT_ID = 'cd99bbea-dcd4-4a76-a0b0-7aeb49931943';
|
||
const TENANT_ID = '9840a2a0-6ae1-4688-b03d-d2ec291be0f9';
|
||
const REDIRECT_URI = 'http://localhost:5174';
|
||
const CLIENT_SECRET = 'F5G8Q~qWNzuMdghyIwTX20cAVjqAK4sz~1uEUaLB';
|
||
const GROUP_ID = 'c1ea877c-6bca-4f47-bfad-f223640813a0';
|
||
|
||
// Middleware
|
||
app.use(cors({
|
||
origin: ['http://localhost:5174', 'http://localhost:5173', 'http://localhost:3000'],
|
||
credentials: true,
|
||
methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'],
|
||
allowedHeaders: ['Content-Type', 'Authorization']
|
||
}));
|
||
|
||
app.use(express.json({ limit: '10mb' }));
|
||
app.use(express.urlencoded({ extended: true, limit: '10mb' }));
|
||
|
||
// Log de toutes les requêtes
|
||
app.use((req, res, next) => {
|
||
console.log(`${new Date().toISOString()} - ${req.method} ${req.path}`);
|
||
if (req.method === 'POST' && req.path !== '/api/exchange-token') {
|
||
console.log('POST Body:', req.body);
|
||
}
|
||
next();
|
||
});
|
||
|
||
// Variable pour stocker la connexion et l'état du système
|
||
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 (serveur RH)');
|
||
|
||
// 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 (RH)');
|
||
} catch (error) {
|
||
systemStatus.canAccessFormateurView = false;
|
||
console.log('❌ Accès à la vue Formateurs: ERREUR (RH) -', error.message);
|
||
}
|
||
}
|
||
|
||
// 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 (RH)');
|
||
} catch (error) {
|
||
systemStatus.hasFormateurLocal = false;
|
||
console.log('❌ Table formateurs_local: non accessible (RH)');
|
||
}
|
||
|
||
// 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 RH:');
|
||
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 RH: ${systemStatus.operatingMode}`);
|
||
|
||
} catch (error) {
|
||
console.error('Erreur lors du diagnostic RH:', error.message);
|
||
systemStatus.operatingMode = 'legacy_hash';
|
||
}
|
||
}
|
||
|
||
// À ajouter dans votre serveur RH (server.js)
|
||
app.get('/api/debug-campus', async (req, res) => {
|
||
try {
|
||
// Vérifier les campus distincts dans la vue
|
||
const campusResult = await pool.request().query(`
|
||
SELECT DISTINCT Campus, COUNT(*) as nb_formateurs
|
||
FROM [dbo].[Formateurs]
|
||
GROUP BY Campus
|
||
ORDER BY Campus
|
||
`);
|
||
|
||
// Échantillon des formateurs pour voir la structure
|
||
const sampleResult = await pool.request().query(`
|
||
SELECT TOP 10
|
||
userPrincipalName,
|
||
displayName,
|
||
Campus,
|
||
surname,
|
||
givenname
|
||
FROM [dbo].[Formateurs]
|
||
ORDER BY Campus, displayName
|
||
`);
|
||
|
||
res.json({
|
||
success: true,
|
||
campus_distincts: campusResult.recordset,
|
||
echantillon_formateurs: sampleResult.recordset,
|
||
message: 'Diagnostic des campus dans la vue Formateurs'
|
||
});
|
||
|
||
} catch (error) {
|
||
res.status(500).json({
|
||
success: false,
|
||
error: error.message
|
||
});
|
||
}
|
||
});
|
||
|
||
// Route de diagnostic qui fonctionne même sans accès à la vue distante
|
||
app.get('/api/debug-campus-local', async (req, res) => {
|
||
try {
|
||
let results = {
|
||
campusFromDeclarations: [],
|
||
campusFromFormateurs: [],
|
||
sampleFormateurs: []
|
||
};
|
||
|
||
// Campus trouvés dans les déclarations
|
||
try {
|
||
const declCampus = await pool.request().query(`
|
||
SELECT DISTINCT
|
||
ISNULL(formateur_email_fk, 'hash_' + CAST(formateur_numero AS VARCHAR)) as formateur_ref,
|
||
COUNT(*) as nb_declarations
|
||
FROM declarations
|
||
GROUP BY formateur_email_fk, formateur_numero
|
||
ORDER BY COUNT(*) DESC
|
||
`);
|
||
results.campusFromDeclarations = declCampus.recordset;
|
||
} catch (error) {
|
||
results.campusFromDeclarations = `Erreur: ${error.message}`;
|
||
}
|
||
|
||
// Essayer formateurs_local si accessible
|
||
try {
|
||
const formatLocal = await pool.request().query(`
|
||
SELECT TOP 10
|
||
userPrincipalName,
|
||
displayName,
|
||
Campus,
|
||
surname,
|
||
givenname
|
||
FROM formateurs_local
|
||
ORDER BY Campus, displayName
|
||
`);
|
||
results.campusFromFormateurs = formatLocal.recordset.map(f => f.Campus).filter(Boolean);
|
||
results.sampleFormateurs = formatLocal.recordset;
|
||
} catch (error) {
|
||
results.campusFromFormateurs = `Erreur formateurs_local: ${error.message}`;
|
||
}
|
||
|
||
res.json({
|
||
success: true,
|
||
results,
|
||
message: 'Diagnostic local des campus'
|
||
});
|
||
|
||
} catch (error) {
|
||
res.status(500).json({
|
||
success: false,
|
||
error: error.message
|
||
});
|
||
}
|
||
});
|
||
|
||
|
||
// 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;
|
||
}
|
||
return Math.abs(hash) % 10000 + 1000;
|
||
}
|
||
|
||
// Fonction pour formatter l'heure SQL
|
||
function formatSqlTime(timeValue) {
|
||
if (!timeValue) return null;
|
||
|
||
if (typeof timeValue === 'string') {
|
||
if (timeValue.match(/^\d{2}:\d{2}:\d{2}\.\d+$/)) {
|
||
return timeValue.substring(0, 5);
|
||
}
|
||
if (timeValue.match(/^\d{2}:\d{2}:\d{2}$/)) {
|
||
return timeValue.substring(0, 5);
|
||
}
|
||
if (timeValue.match(/^\d{2}:\d{2}$/)) {
|
||
return timeValue;
|
||
}
|
||
}
|
||
|
||
if (timeValue instanceof Date) {
|
||
const hours = timeValue.getHours().toString().padStart(2, '0');
|
||
const minutes = timeValue.getMinutes().toString().padStart(2, '0');
|
||
return `${hours}:${minutes}`;
|
||
}
|
||
|
||
return null;
|
||
}
|
||
|
||
// Fonction utilitaire pour récupérer le token
|
||
function getAccessToken(req) {
|
||
const headers = req.headers;
|
||
let accessToken = null;
|
||
|
||
console.log('\n=== DEBUG TOKEN EXTRACTION (RH) ===');
|
||
console.log('Headers reçus:', JSON.stringify(headers, null, 2));
|
||
|
||
// 1. Vérifier Authorization header
|
||
if (headers['authorization']) {
|
||
accessToken = headers['authorization'].replace(/^Bearer\s+/i, '').trim();
|
||
console.log('✅ Token trouvé dans Authorization header');
|
||
}
|
||
|
||
// 2. Vérifier x-access-token
|
||
if (!accessToken && headers['x-access-token']) {
|
||
accessToken = headers['x-access-token'].trim();
|
||
console.log('✅ Token trouvé dans x-access-token header');
|
||
}
|
||
|
||
// 3. Vérifier query param
|
||
if (!accessToken && req.query && req.query.token) {
|
||
accessToken = req.query.token.trim();
|
||
console.log('✅ Token trouvé dans query param ?token=');
|
||
}
|
||
|
||
// 4. Vérifier body
|
||
if (!accessToken && req.body && req.body.accessToken) {
|
||
accessToken = req.body.accessToken.trim();
|
||
console.log('✅ Token trouvé dans body');
|
||
}
|
||
|
||
if (accessToken) {
|
||
console.log(`🎫 Token extrait: Présent (${accessToken.substring(0, 25)}...)`);
|
||
} else {
|
||
console.warn('⚠️ Aucun token trouvé dans la requête');
|
||
}
|
||
|
||
console.log('=== FIN DEBUG TOKEN (RH) ===\n');
|
||
return accessToken;
|
||
}
|
||
|
||
async function getApplicationToken() {
|
||
try {
|
||
const response = await axios.post(
|
||
`https://login.microsoftonline.com/${TENANT_ID}/oauth2/v2.0/token`,
|
||
new URLSearchParams({
|
||
grant_type: 'client_credentials',
|
||
client_id: CLIENT_ID,
|
||
client_secret: CLIENT_SECRET,
|
||
scope: 'https://graph.microsoft.com/.default'
|
||
}),
|
||
{ headers: { 'Content-Type': 'application/x-www-form-urlencoded' } }
|
||
);
|
||
|
||
return response.data.access_token;
|
||
} catch (error) {
|
||
console.error('Erreur obtention token application:', error.response?.data || error.message);
|
||
throw new Error('Impossible d\'obtenir un token Microsoft');
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Fonction générique pour appeler Graph API
|
||
*/
|
||
async function callGraph(url, accessToken, method = 'GET', data = null) {
|
||
try {
|
||
const config = {
|
||
method,
|
||
url,
|
||
headers: {
|
||
'Authorization': `Bearer ${accessToken}`,
|
||
'Content-Type': 'application/json'
|
||
}
|
||
};
|
||
if (data && method === 'POST') config.data = data;
|
||
|
||
const response = await axios(config);
|
||
return response.data;
|
||
} catch (error) {
|
||
console.error('Erreur Graph API:', error.response?.data || error.message);
|
||
return null;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Vérifier si utilisateur appartient à un groupe
|
||
*/
|
||
async function isUserInGroup(userId, groupId, accessToken) {
|
||
try {
|
||
const url = `https://graph.microsoft.com/v1.0/users/${userId}/checkMemberGroups`;
|
||
const data = { groupIds: [groupId] };
|
||
|
||
const result = await callGraph(url, accessToken, 'POST', data);
|
||
|
||
if (result && result.value && result.value.includes(groupId)) {
|
||
const userInfo = await callGraph(
|
||
`https://graph.microsoft.com/v1.0/users/${userId}?$select=mail,userPrincipalName,department`,
|
||
accessToken
|
||
);
|
||
|
||
const userEmail = userInfo?.mail || userInfo?.userPrincipalName;
|
||
const userDepartment = userInfo?.department || '';
|
||
|
||
if (userDepartment.toLowerCase().includes('administratif') ||
|
||
userDepartment.toLowerCase().includes('administration') ||
|
||
userDepartment.toLowerCase().includes('informatique') ||
|
||
userDepartment.toLowerCase().includes('ressources humaines')) {
|
||
console.log(`✅ Utilisateur RH autorisé: ${userEmail} (Service: ${userDepartment})`);
|
||
return true;
|
||
}
|
||
|
||
console.log(`❌ Utilisateur pas dans le service administratif: ${userEmail} (Service: ${userDepartment})`);
|
||
}
|
||
|
||
const userInfo = await callGraph(
|
||
`https://graph.microsoft.com/v1.0/users/${userId}?$select=mail,userPrincipalName`,
|
||
accessToken
|
||
);
|
||
|
||
const userEmail = userInfo?.mail || userInfo?.userPrincipalName;
|
||
const authorizedUsers = ['adminensup@ensup.eu', 'klambert@ensup.eu'];
|
||
|
||
if (authorizedUsers.includes(userEmail)) {
|
||
console.log(`✅ Utilisateur autorisé spécifiquement: ${userEmail}`);
|
||
return true;
|
||
}
|
||
|
||
console.log(`❌ Utilisateur non autorisé: ${userEmail}`);
|
||
return false;
|
||
|
||
} catch (error) {
|
||
console.error('Erreur vérification groupe:', error);
|
||
return false;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Obtenir les membres d'un groupe avec filtrage par service
|
||
*/
|
||
async function getGroupMembers(groupId, accessToken, service = null, limit = null) {
|
||
const url = `https://graph.microsoft.com/v1.0/groups/${groupId}/members?$select=id,displayName,givenName,surname,mail,department,jobTitle`;
|
||
|
||
const result = await callGraph(url, accessToken);
|
||
|
||
if (!result || !result.value) {
|
||
return [];
|
||
}
|
||
|
||
let members = result.value;
|
||
|
||
if (service) {
|
||
members = members.filter(member =>
|
||
member.department && member.department.toLowerCase().includes(service.toLowerCase())
|
||
);
|
||
}
|
||
|
||
if (limit) {
|
||
members = members.slice(0, limit);
|
||
}
|
||
|
||
return members;
|
||
}
|
||
|
||
// ==================== ROUTES DE DIAGNOSTIC ====================
|
||
|
||
// 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,
|
||
serverType: 'RH'
|
||
});
|
||
|
||
} 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 RH: ${systemStatus.operatingMode}`
|
||
});
|
||
|
||
} catch (error) {
|
||
res.status(500).json({
|
||
success: false,
|
||
error: error.message,
|
||
message: 'Erreur lors de la migration'
|
||
});
|
||
}
|
||
});
|
||
|
||
// ==================== ROUTES EXISTANTES ====================
|
||
|
||
// Route de test
|
||
app.get('/api/test', (req, res) => {
|
||
res.json({
|
||
message: 'Le serveur RH fonctionne !',
|
||
timestamp: new Date().toISOString(),
|
||
systemStatus
|
||
});
|
||
});
|
||
|
||
// Route pour tester la base de données
|
||
app.get('/api/db-test', async (req, res) => {
|
||
try {
|
||
if (!pool) {
|
||
return res.status(500).json({ error: 'Base non connectée' });
|
||
}
|
||
|
||
const declarationsResult = await pool.request().query('SELECT COUNT(*) as total FROM declarations');
|
||
const rhResult = await pool.request().query('SELECT COUNT(*) as total FROM rh');
|
||
|
||
let formateurCount = 0;
|
||
try {
|
||
if (systemStatus.canAccessFormateurView) {
|
||
const formateurResult = await pool.request().query('SELECT COUNT(*) as total FROM [dbo].[Formateurs]');
|
||
formateurCount = formateurResult.recordset[0].total;
|
||
} else if (systemStatus.hasFormateurLocal) {
|
||
const formateurResult = await pool.request().query('SELECT COUNT(*) as total FROM formateurs_local');
|
||
formateurCount = formateurResult.recordset[0].total;
|
||
}
|
||
} catch (error) {
|
||
console.log('Impossible de compter les formateurs:', error.message);
|
||
}
|
||
|
||
res.json({
|
||
message: 'Base RH OK',
|
||
declarations: declarationsResult.recordset[0].total,
|
||
utilisateurs_rh: rhResult.recordset[0].total,
|
||
formateurs: formateurCount,
|
||
mode: systemStatus.operatingMode
|
||
});
|
||
} catch (error) {
|
||
res.status(500).json({ error: error.message });
|
||
}
|
||
});
|
||
|
||
// Route pour mettre à jour le statut d'une déclaration
|
||
app.put('/api/declarations/:id/status', async (req, res) => {
|
||
try {
|
||
const { id } = req.params;
|
||
const { status } = req.body;
|
||
|
||
await pool.request()
|
||
.input('id', sql.Int, id)
|
||
.input('status', sql.VarChar, status)
|
||
.query('UPDATE declarations SET status = @status WHERE id = @id');
|
||
|
||
res.json({ success: true, message: 'Statut mis à jour' });
|
||
} catch (error) {
|
||
res.status(500).json({ success: false, error: error.message });
|
||
}
|
||
});
|
||
|
||
// Route pour récupérer les déclarations (ADAPTÉE AU NOUVEAU SYSTÈME)
|
||
app.get('/api/get_declarations', async (req, res) => {
|
||
try {
|
||
console.log(`Récupération des déclarations (mode RH: ${systemStatus.operatingMode})...`);
|
||
|
||
let result;
|
||
|
||
switch (systemStatus.operatingMode) {
|
||
case 'new_with_view':
|
||
// Avec vue Formateurs
|
||
result = await pool.request().query(`
|
||
SELECT
|
||
d.id,
|
||
d.utilisateur_id,
|
||
d.formateur_email_fk as formateur_email,
|
||
f.displayName as formateur_nom_complet,
|
||
f.surname as nom,
|
||
f.givenname as prenom,
|
||
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':
|
||
// Avec table formateurs_local
|
||
result = await pool.request().query(`
|
||
SELECT
|
||
d.id,
|
||
d.utilisateur_id,
|
||
d.formateur_email_fk as formateur_email,
|
||
f.displayName as formateur_nom_complet,
|
||
f.surname as nom,
|
||
f.givenname as prenom,
|
||
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':
|
||
// Sans jointure formateur
|
||
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;
|
||
|
||
case 'legacy_hash':
|
||
default:
|
||
// Ancien système avec hash
|
||
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.description,
|
||
d.formateur_numero,
|
||
d.heure_debut,
|
||
d.heure_fin,
|
||
'pending' as status,
|
||
td.libelle as activityType
|
||
FROM declarations d
|
||
INNER JOIN types_demandes td ON d.type_demande_id = td.id
|
||
ORDER BY d.date DESC
|
||
`);
|
||
break;
|
||
}
|
||
|
||
console.log(`${result.recordset.length} déclarations récupérées`);
|
||
|
||
// Traitement selon le mode
|
||
let processedResults = [];
|
||
|
||
if (systemStatus.operatingMode.startsWith('new_')) {
|
||
// Nouveau système - données déjà enrichies par les jointures
|
||
processedResults = result.recordset.map(row => ({
|
||
id: row.id,
|
||
utilisateur_id: row.utilisateur_id,
|
||
formateur_email: row.formateur_email,
|
||
type_demande_id: row.type_demande_id,
|
||
date: row.date,
|
||
duree: row.duree,
|
||
description: row.description,
|
||
heure_debut: formatSqlTime(row.heure_debut),
|
||
heure_fin: formatSqlTime(row.heure_fin),
|
||
status: row.status || 'pending',
|
||
activityType: row.activityType,
|
||
// Informations formateur (peuvent être null si pas de jointure)
|
||
nom: row.nom || (row.formateur_email ? row.formateur_email.split('@')[0] : 'Inconnu'),
|
||
prenom: row.prenom || '',
|
||
campus: row.Campus || 'Non défini',
|
||
formateur_nom_complet: row.formateur_nom_complet || row.formateur_email || 'Utilisateur inconnu'
|
||
}));
|
||
} else {
|
||
// Ancien système - mapping manuel
|
||
const knownMappings = {
|
||
122: { nom: 'Admin', prenom: 'Ensup', campus: 'SQY' },
|
||
999: { nom: 'Inconnu', prenom: 'Formateur', campus: 'Non défini' }
|
||
};
|
||
|
||
const emailMappings = {
|
||
'oimer@ensup.eu': { nom: 'Oimer', prenom: 'Utilisateur', campus: 'Cergy' },
|
||
'admin@ensup.eu': { nom: 'Admin', prenom: 'Ensup', campus: 'SQY' },
|
||
'adminensup@ensup.eu': { nom: 'Admin', prenom: 'Ensup', campus: 'SQY' },
|
||
'klambert@ensup.eu': { nom: 'Lambert', prenom: 'Kevin', campus: 'SQY' }
|
||
};
|
||
|
||
processedResults = result.recordset.map(row => {
|
||
const formateurNumero = row.formateur_numero;
|
||
let formateurInfo = {
|
||
nom: `Formateur ${formateurNumero}`,
|
||
prenom: '',
|
||
campus: 'Non défini'
|
||
};
|
||
|
||
// 1. Vérifier les mappings directs
|
||
if (knownMappings[formateurNumero]) {
|
||
formateurInfo = knownMappings[formateurNumero];
|
||
} else {
|
||
// 2. Vérifier si c'est un hash d'email connu
|
||
for (const [email, info] of Object.entries(emailMappings)) {
|
||
const hash = generateHashFromEmail(email);
|
||
if (hash === formateurNumero) {
|
||
formateurInfo = info;
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
|
||
return {
|
||
id: row.id,
|
||
utilisateur_id: row.utilisateur_id,
|
||
type_demande_id: row.type_demande_id,
|
||
date: row.date,
|
||
duree: row.duree,
|
||
description: row.description,
|
||
formateur_numero: row.formateur_numero,
|
||
heure_debut: formatSqlTime(row.heure_debut),
|
||
heure_fin: formatSqlTime(row.heure_fin),
|
||
status: row.status,
|
||
activityType: row.activityType,
|
||
nom: formateurInfo.nom,
|
||
prenom: formateurInfo.prenom,
|
||
campus: formateurInfo.campus
|
||
};
|
||
});
|
||
}
|
||
|
||
console.log('Déclarations traitées avec succès (RH)');
|
||
if (processedResults.length > 0) {
|
||
console.log('Exemple:', processedResults[0]);
|
||
}
|
||
|
||
res.json(processedResults);
|
||
|
||
} catch (error) {
|
||
console.error('Erreur get_declarations (RH):', error);
|
||
res.status(500).json({
|
||
error: error.message,
|
||
details: 'Erreur lors de la récupération des déclarations (serveur RH)'
|
||
});
|
||
}
|
||
});
|
||
|
||
// Route de debug pour voir quel hash correspond à votre email
|
||
app.get('/api/debug-hash', (req, res) => {
|
||
const { email } = req.query;
|
||
if (!email) {
|
||
return res.json({
|
||
error: 'Email requis',
|
||
example: 'http://localhost:3002/api/debug-hash?email=oimer@ensup.eu'
|
||
});
|
||
}
|
||
|
||
const hash = generateHashFromEmail(email);
|
||
res.json({
|
||
email: email,
|
||
hash: hash,
|
||
message: `L'email ${email} génère le numéro ${hash}`,
|
||
serverType: 'RH'
|
||
});
|
||
});
|
||
|
||
// Nouvelle route pour les formateurs avec déclarations (ADAPTÉE)
|
||
app.get('/api/formateurs-avec-declarations', async (req, res) => {
|
||
try {
|
||
let result;
|
||
|
||
switch (systemStatus.operatingMode) {
|
||
case 'new_with_view':
|
||
result = await pool.request().query(`
|
||
SELECT DISTINCT
|
||
f.givenname,
|
||
f.surname,
|
||
f.displayName,
|
||
f.Campus,
|
||
f.userPrincipalName,
|
||
f.Jobtitle,
|
||
f.Contrat,
|
||
COUNT(d.id) as nb_declarations
|
||
FROM [dbo].[Formateurs] f
|
||
LEFT JOIN declarations d ON f.userPrincipalName = d.formateur_email_fk
|
||
WHERE (f.Contrat = 'CDD' OR f.Contrat LIKE '%CDD%')
|
||
AND d.id IS NOT NULL
|
||
GROUP BY f.givenname, f.surname, f.displayName, f.Campus, f.userPrincipalName, f.Jobtitle, f.Contrat
|
||
ORDER BY f.surname, f.givenname
|
||
`);
|
||
break;
|
||
|
||
case 'new_with_local':
|
||
result = await pool.request().query(`
|
||
SELECT DISTINCT
|
||
f.givenname,
|
||
f.surname,
|
||
f.displayName,
|
||
f.Campus,
|
||
f.userPrincipalName,
|
||
f.Jobtitle,
|
||
f.Contrat,
|
||
COUNT(d.id) as nb_declarations
|
||
FROM formateurs_local f
|
||
LEFT JOIN declarations d ON f.userPrincipalName = d.formateur_email_fk
|
||
WHERE (f.Contrat = 'CDD' OR f.Contrat LIKE '%CDD%')
|
||
AND d.id IS NOT NULL
|
||
GROUP BY f.givenname, f.surname, f.displayName, f.Campus, f.userPrincipalName, f.Jobtitle, f.Contrat
|
||
ORDER BY f.surname, f.givenname
|
||
`);
|
||
break;
|
||
}
|
||
|
||
const formateurs = result.recordset.map(f => ({
|
||
userPrincipalName: f.userPrincipalName,
|
||
displayName: f.displayName,
|
||
nom: f.surname || '',
|
||
prenom: f.givenname || '',
|
||
campus: f.Campus || 'Non défini',
|
||
poste: f.Jobtitle || '',
|
||
contrat: f.Contrat || '',
|
||
nbDeclarations: f.nb_declarations,
|
||
displayText: `${f.surname || ''} ${f.givenname || ''} (${f.Campus || 'Non défini'}) - CDD`.trim()
|
||
}));
|
||
|
||
res.json({
|
||
success: true,
|
||
count: formateurs.length,
|
||
formateurs: formateurs,
|
||
mode: systemStatus.operatingMode,
|
||
filtre: 'CDD_avec_declarations'
|
||
});
|
||
|
||
} catch (error) {
|
||
res.status(500).json({
|
||
success: false,
|
||
error: error.message
|
||
});
|
||
}
|
||
});
|
||
|
||
// Route de compatibilité pour l'ancienne méthode (ADAPTÉE)
|
||
app.get('/api/formateurs-vue', async (req, res) => {
|
||
try {
|
||
console.log(`🔍 Récupération formateurs CDD seulement (mode: ${systemStatus.operatingMode})...`);
|
||
|
||
let formateurs = [];
|
||
let result; // Déclarer result ici !
|
||
|
||
if (systemStatus.canAccessFormateurView) {
|
||
console.log('Utilisation de la vue Formateurs...');
|
||
result = await pool.request().query(`
|
||
SELECT
|
||
userPrincipalName,
|
||
displayName,
|
||
surname,
|
||
givenname,
|
||
Campus,
|
||
Contrat
|
||
FROM [dbo].[Formateurs]
|
||
WHERE Contrat = 'CDD' OR Contrat LIKE '%CDD%'
|
||
ORDER BY surname, givenname
|
||
`);
|
||
} else if (systemStatus.hasFormateurLocal) {
|
||
console.log('Utilisation de formateurs_local...');
|
||
result = await pool.request().query(`
|
||
SELECT
|
||
userPrincipalName,
|
||
displayName,
|
||
surname,
|
||
givenname,
|
||
Campus,
|
||
Contrat
|
||
FROM formateurs_local
|
||
WHERE Contrat = 'CDD' OR Contrat LIKE '%CDD%'
|
||
ORDER BY surname, givenname
|
||
`);
|
||
} else {
|
||
console.log('Aucune source de formateurs disponible');
|
||
return res.json({
|
||
success: true,
|
||
count: 0,
|
||
formateurs: [],
|
||
mode: systemStatus.operatingMode,
|
||
message: 'Aucune source de formateurs disponible'
|
||
});
|
||
}
|
||
|
||
console.log(`Résultat requête: ${result.recordset.length} formateurs`);
|
||
|
||
if (result.recordset.length === 0) {
|
||
console.log('Aucun formateur CDD trouvé, test sans filtre...');
|
||
|
||
// Test sans le filtre CDD pour voir s'il y a des formateurs
|
||
let testResult;
|
||
if (systemStatus.hasFormateurLocal) {
|
||
testResult = await pool.request().query(`
|
||
SELECT TOP 5
|
||
userPrincipalName,
|
||
displayName,
|
||
surname,
|
||
givenname,
|
||
Campus,
|
||
Contrat
|
||
FROM formateurs_local
|
||
ORDER BY surname, givenname
|
||
`);
|
||
console.log('Test sans filtre:', testResult.recordset);
|
||
console.log('Types de contrats:', [...new Set(testResult.recordset.map(f => f.Contrat))]);
|
||
}
|
||
}
|
||
|
||
formateurs = result.recordset.map(f => ({
|
||
userPrincipalName: f.userPrincipalName,
|
||
displayName: f.displayName,
|
||
nom: f.surname || '',
|
||
prenom: f.givenname || '',
|
||
campus: f.Campus || 'Non défini',
|
||
contrat: f.Contrat || '',
|
||
displayText: `${f.surname || ''} ${f.givenname || ''} (${f.Campus || 'Non défini'})`.trim()
|
||
}));
|
||
|
||
console.log(`✅ ${formateurs.length} formateurs CDD traités`);
|
||
|
||
res.json({
|
||
success: true,
|
||
count: formateurs.length,
|
||
formateurs: formateurs,
|
||
mode: systemStatus.operatingMode,
|
||
filtre: 'CDD_uniquement'
|
||
});
|
||
|
||
} catch (error) {
|
||
console.error('Erreur récupération formateurs CDD:', error);
|
||
res.status(500).json({
|
||
success: false,
|
||
error: error.message
|
||
});
|
||
}
|
||
});
|
||
// ==================== ROUTES MICROSOFT GRAPH (INCHANGÉES) ====================
|
||
|
||
app.post('/api/auth', async (req, res) => {
|
||
const { userPrincipalName } = req.body;
|
||
const accessToken = getAccessToken(req);
|
||
|
||
if (!userPrincipalName || !accessToken) {
|
||
return res.json({
|
||
authorized: false,
|
||
message: 'Email ou token manquant'
|
||
});
|
||
}
|
||
|
||
try {
|
||
const authResult = await authenticateUserWithGraph(userPrincipalName, accessToken);
|
||
res.json(authResult);
|
||
} catch (error) {
|
||
console.error('Erreur authentification:', error);
|
||
res.json({
|
||
authorized: false,
|
||
message: 'Erreur serveur: ' + error.message
|
||
});
|
||
}
|
||
});
|
||
|
||
/**
|
||
* Authentifier un utilisateur avec Microsoft Graph
|
||
*/
|
||
async function authenticateUserWithGraph(userPrincipalName, accessToken) {
|
||
try {
|
||
const existingUser = await pool.request()
|
||
.input('email', sql.VarChar, userPrincipalName)
|
||
.query('SELECT id,nom, prenom, email FROM rh WHERE email = @email');
|
||
|
||
if (existingUser.recordset.length > 0) {
|
||
const user = existingUser.recordset[0];
|
||
return {
|
||
authorized: true,
|
||
role: user.role || 'Collaborateur',
|
||
groups: [user.role || 'Collaborateur'],
|
||
localUserId: parseInt(user.id),
|
||
user: user
|
||
};
|
||
}
|
||
|
||
const userGraph = await callGraph(
|
||
`https://graph.microsoft.com/v1.0/users/${userPrincipalName}?$select=id,displayName,givenName,surname,mail,department,jobTitle`,
|
||
accessToken
|
||
);
|
||
|
||
if (!userGraph) {
|
||
throw new Error('Utilisateur introuvable dans Entra ou token invalide');
|
||
}
|
||
|
||
const isInTargetGroup = await isUserInGroup(userGraph.id, GROUP_ID, accessToken);
|
||
|
||
if (!isInTargetGroup) {
|
||
throw new Error('Utilisateur non autorisé : il n\'appartient pas au groupe requis');
|
||
}
|
||
|
||
const prenom = userGraph.givenName || '';
|
||
const nom = userGraph.surname || '';
|
||
const email = userGraph.mail || userPrincipalName;
|
||
|
||
const insertResult = await pool.request()
|
||
.input('nom', sql.VarChar, nom)
|
||
.input('prenom', sql.VarChar, prenom)
|
||
.input('email', sql.VarChar, email)
|
||
.query(`INSERT INTO rh (nom, prenom, email)
|
||
OUTPUT INSERTED.id
|
||
VALUES (@nom, @prenom, @email)`);
|
||
|
||
const newUserId = insertResult.recordset[0].id;
|
||
|
||
return {
|
||
authorized: true,
|
||
role: 'Collaborateur',
|
||
groups: ['Collaborateur'],
|
||
localUserId: parseInt(newUserId),
|
||
user: {
|
||
id: newUserId,
|
||
nom: nom,
|
||
prenom: prenom,
|
||
email: email,
|
||
role: 'Collaborateur'
|
||
}
|
||
};
|
||
|
||
} catch (error) {
|
||
console.error('Erreur authentification Microsoft Graph:', error);
|
||
throw error;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Route pour extraire 3 personnes du service administratif
|
||
*/
|
||
app.get('/api/admin-users', async (req, res) => {
|
||
console.log('=== Route /api/admin-users appelée (RH) ===');
|
||
|
||
const accessToken = getAccessToken(req);
|
||
|
||
console.log('Token reçu:', accessToken ? `Présent (${accessToken.substring(0, 20)}...)` : 'Absent');
|
||
|
||
try {
|
||
let users = [];
|
||
|
||
if (accessToken) {
|
||
try {
|
||
const adminUsers = await getGroupMembers(GROUP_ID, accessToken, 'administratif', 2);
|
||
|
||
users = adminUsers.map(user => ({
|
||
nom: user.surname || '',
|
||
prenom: user.givenName || '',
|
||
email: user.mail || ''
|
||
}));
|
||
|
||
console.log(`${users.length} utilisateurs AD récupérés`);
|
||
} catch (adError) {
|
||
console.error('Erreur récupération AD:', adError.message);
|
||
}
|
||
}
|
||
|
||
users.push({
|
||
nom: 'Lambert',
|
||
prenom: 'Kevin',
|
||
email: 'kevin.lambert@test.com'
|
||
});
|
||
|
||
const response = {
|
||
success: true,
|
||
count: users.length,
|
||
users: users
|
||
};
|
||
|
||
if (!accessToken) {
|
||
response.warning = 'Mode test - Token manquant, seules les données de test sont affichées';
|
||
}
|
||
|
||
res.json(response);
|
||
|
||
} catch (error) {
|
||
console.error('Erreur générale:', error);
|
||
|
||
res.json({
|
||
success: true,
|
||
count: 1,
|
||
users: [{
|
||
nom: 'Lambert',
|
||
prenom: 'Kevin',
|
||
email: 'kevin.lambert@test.com'
|
||
}],
|
||
error: error.message,
|
||
warning: 'Données de secours uniquement'
|
||
});
|
||
}
|
||
});
|
||
|
||
/**
|
||
* Route pour obtenir tous les membres du groupe
|
||
*/
|
||
app.get('/api/group-members', async (req, res) => {
|
||
console.log('=== Route /api/group-members appelée (RH) ===');
|
||
|
||
const accessToken = getAccessToken(req);
|
||
const service = req.query.service;
|
||
const limit = req.query.limit ? parseInt(req.query.limit) : null;
|
||
|
||
console.log('Paramètres reçus:', { service, limit });
|
||
console.log('Token reçu:', accessToken ? `Présent (${accessToken.substring(0, 20)}...)` : 'Absent');
|
||
|
||
try {
|
||
let members = [];
|
||
|
||
if (accessToken) {
|
||
try {
|
||
const adMembers = await getGroupMembers(GROUP_ID, accessToken, service, limit);
|
||
members = adMembers.map(m => ({
|
||
entraUserId: m.id,
|
||
prenom: m.givenName || '',
|
||
nom: m.surname || '',
|
||
email: m.mail || '',
|
||
service: m.department || '',
|
||
poste: m.jobTitle || '',
|
||
nomComplet: m.displayName || ''
|
||
}));
|
||
|
||
console.log(`✅ ${members.length} membres AD récupérés`);
|
||
} catch (adError) {
|
||
console.error('Erreur récupération AD:', adError.message);
|
||
}
|
||
}
|
||
|
||
if (members.length === 0) {
|
||
members = [
|
||
{ entraUserId: 'test-user-1', prenom: 'Kevin', nom: 'Lambert', email: 'kevin.lambert@test.com', service: service || 'Administratif', poste: 'Administrateur Test', nomComplet: 'Kevin Lambert' },
|
||
{ entraUserId: 'test-user-2', prenom: 'Marie', nom: 'Dubois', email: 'marie.dubois@test.com', service: service || 'Administratif', poste: 'Assistante RH Test', nomComplet: 'Marie Dubois' },
|
||
{ entraUserId: 'test-user-3', prenom: 'Jean', nom: 'Martin', email: 'jean.martin@test.com', service: service || 'Pédagogique', poste: 'Formateur Test', nomComplet: 'Jean Martin' }
|
||
];
|
||
|
||
if (limit && limit < members.length) members = members.slice(0, limit);
|
||
if (service) members = members.filter(m => m.service.toLowerCase().includes(service.toLowerCase()));
|
||
}
|
||
|
||
const response = {
|
||
success: true,
|
||
count: members.length,
|
||
members: members
|
||
};
|
||
|
||
if (!accessToken) {
|
||
response.warning = 'Mode test - Token manquant, seules les données de test sont affichées';
|
||
}
|
||
|
||
res.json(response);
|
||
|
||
} catch (error) {
|
||
console.error('Erreur générale:', error);
|
||
|
||
res.json({
|
||
success: true,
|
||
count: 1,
|
||
members: [{
|
||
entraUserId: 'test-user-fallback',
|
||
prenom: 'Kevin',
|
||
nom: 'Lambert',
|
||
email: 'kevin.lambert@test.com',
|
||
service: 'Administratif',
|
||
poste: 'Administrateur Test',
|
||
nomComplet: 'Kevin Lambert'
|
||
}],
|
||
error: error.message,
|
||
warning: 'Données de secours uniquement'
|
||
});
|
||
}
|
||
});
|
||
|
||
// Route de test à ajouter temporairement
|
||
app.get('/api/test-permissions', async (req, res) => {
|
||
try {
|
||
console.log('Test des permissions sur HP-TO-O365...');
|
||
|
||
// Test direct de la vue
|
||
const result = await pool.request().query(`
|
||
SELECT TOP 3
|
||
userPrincipalName,
|
||
displayName,
|
||
Campus
|
||
FROM [HP-TO-O365].[dbo].[V_Formateurs_Augmentees]
|
||
`);
|
||
|
||
res.json({
|
||
success: true,
|
||
message: 'Permissions OK !',
|
||
data: result.recordset,
|
||
count: result.recordset.length
|
||
});
|
||
|
||
} catch (error) {
|
||
console.error('Erreur test permissions:', error.message);
|
||
res.status(500).json({
|
||
success: false,
|
||
error: error.message,
|
||
message: 'Permissions insuffisantes'
|
||
});
|
||
}
|
||
});
|
||
|
||
// ==================== ROUTES TABLE RH ====================
|
||
|
||
app.post('/api/exchange-token', async (req, res) => {
|
||
try {
|
||
console.log('=== DÉBUT EXCHANGE TOKEN (RH) ===');
|
||
console.log('Body reçu:', req.body);
|
||
|
||
const { code, code_verifier } = req.body;
|
||
|
||
if (!code) {
|
||
return res.json({ success: false, message: 'Code manquant' });
|
||
}
|
||
|
||
const params = new URLSearchParams();
|
||
params.append('client_id', CLIENT_ID);
|
||
params.append('code', code);
|
||
params.append('redirect_uri', REDIRECT_URI);
|
||
params.append('grant_type', 'authorization_code');
|
||
params.append('scope', 'https://graph.microsoft.com/User.Read https://graph.microsoft.com/User.Read');
|
||
|
||
if (code_verifier) {
|
||
params.append('code_verifier', code_verifier);
|
||
} else {
|
||
return res.json({ success: false, message: 'Code verifier manquant (PKCE requis)' });
|
||
}
|
||
|
||
console.log('Échange du code avec Microsoft (mode PKCE)...');
|
||
|
||
const response = await axios.post(
|
||
`https://login.microsoftonline.com/${TENANT_ID}/oauth2/v2.0/token`,
|
||
params.toString(),
|
||
{ headers: { 'Content-Type': 'application/x-www-form-urlencoded' } }
|
||
);
|
||
|
||
const { access_token } = response.data;
|
||
console.log('✅ Token obtenu avec succès');
|
||
|
||
const userInfo = await callGraph(
|
||
'https://graph.microsoft.com/v1.0/me?$select=id,displayName,givenName,surname,mail,userPrincipalName',
|
||
access_token
|
||
);
|
||
|
||
if (!userInfo) {
|
||
throw new Error('Impossible de récupérer les informations utilisateur');
|
||
}
|
||
|
||
console.log('Utilisateur récupéré:', userInfo.userPrincipalName);
|
||
|
||
const isAuthorized = await isUserInGroup(userInfo.id, GROUP_ID, access_token);
|
||
if (!isAuthorized) {
|
||
return res.json({ success: false, message: 'Utilisateur non autorisé - pas dans le groupe requis' });
|
||
}
|
||
|
||
console.log('✅ Utilisateur autorisé');
|
||
|
||
const email = userInfo.mail || userInfo.userPrincipalName;
|
||
const prenom = userInfo.givenName || '';
|
||
const nom = userInfo.surname || '';
|
||
|
||
if (!pool) {
|
||
throw new Error('Base de données non connectée');
|
||
}
|
||
|
||
let userResult = await pool.request()
|
||
.input('email', sql.VarChar, email)
|
||
.query('SELECT id FROM rh WHERE email = @email');
|
||
|
||
let userId;
|
||
if (userResult.recordset.length > 0) {
|
||
userId = userResult.recordset[0].id;
|
||
console.log('Utilisateur existant trouvé, ID:', userId);
|
||
} else {
|
||
const insertResult = await pool.request()
|
||
.input('nom', sql.VarChar, nom)
|
||
.input('prenom', sql.VarChar, prenom)
|
||
.input('email', sql.VarChar, email)
|
||
.query('INSERT INTO rh (nom, prenom, email) OUTPUT INSERTED.id VALUES (@nom, @prenom, @email)');
|
||
userId = insertResult.recordset[0].id;
|
||
console.log('Nouvel utilisateur créé, ID:', userId);
|
||
}
|
||
|
||
const userData = { id: userId, nom, prenom, email, role: 'Collaborateur' };
|
||
|
||
console.log('✅ Succès complet, retour des données');
|
||
res.json({
|
||
success: true,
|
||
accessToken: access_token,
|
||
user: userData
|
||
});
|
||
|
||
} catch (error) {
|
||
console.error('🚨 ERREUR DANS EXCHANGE TOKEN:');
|
||
console.error('Message:', error.message);
|
||
|
||
if (error.response) {
|
||
console.error('Erreur Microsoft:', error.response.status, error.response.data);
|
||
}
|
||
|
||
res.status(500).json({
|
||
success: false,
|
||
message: 'Erreur serveur: ' + error.message
|
||
});
|
||
}
|
||
});
|
||
|
||
app.post('/api/check-user-groups', async (req, res) => {
|
||
try {
|
||
const { userPrincipalName } = req.body;
|
||
const accessToken = getAccessToken(req);
|
||
|
||
if (!userPrincipalName || !accessToken) {
|
||
return res.json({
|
||
authorized: false,
|
||
message: "Email ou token manquant"
|
||
});
|
||
}
|
||
|
||
const existingUser = await pool.request()
|
||
.input('email', sql.VarChar, userPrincipalName)
|
||
.query('SELECT id, nom, prenom, email FROM rh WHERE email = @email');
|
||
|
||
if (existingUser.recordset.length > 0) {
|
||
const user = existingUser.recordset[0];
|
||
return res.json({
|
||
authorized: true,
|
||
role: 'Collaborateur',
|
||
groups: ['Collaborateur'],
|
||
localUserId: parseInt(user.id),
|
||
user: {
|
||
id: user.id,
|
||
prenom: user.prenom,
|
||
nom: user.nom,
|
||
email: user.email,
|
||
role: 'Collaborateur'
|
||
}
|
||
});
|
||
}
|
||
|
||
const userGraph = await callGraph(
|
||
`https://graph.microsoft.com/v1.0/users/${userPrincipalName}?$select=id,displayName,givenName,surname,mail,department,jobTitle`,
|
||
accessToken
|
||
);
|
||
|
||
if (!userGraph) {
|
||
return res.json({
|
||
authorized: false,
|
||
message: "Utilisateur introuvable dans Entra ou token invalide"
|
||
});
|
||
}
|
||
|
||
const isInTargetGroup = await isUserInGroup(userGraph.id, GROUP_ID, accessToken);
|
||
|
||
if (!isInTargetGroup) {
|
||
return res.json({
|
||
authorized: false,
|
||
message: "Utilisateur non autorisé : il n'appartient pas au groupe requis"
|
||
});
|
||
}
|
||
|
||
const prenom = userGraph.givenName || '';
|
||
const nom = userGraph.surname || '';
|
||
const email = userGraph.mail || userPrincipalName;
|
||
|
||
const insertResult = await pool.request()
|
||
.input('nom', sql.VarChar, nom)
|
||
.input('prenom', sql.VarChar, prenom)
|
||
.input('email', sql.VarChar, email)
|
||
.query(`
|
||
INSERT INTO rh (nom, prenom, email)
|
||
OUTPUT INSERTED.id
|
||
VALUES (@nom, @prenom, @email)
|
||
`);
|
||
|
||
const newUserId = insertResult.recordset[0].id;
|
||
|
||
res.json({
|
||
authorized: true,
|
||
role: 'Collaborateur',
|
||
groups: ['Collaborateur'],
|
||
localUserId: parseInt(newUserId),
|
||
user: {
|
||
id: newUserId,
|
||
nom: nom,
|
||
prenom: prenom,
|
||
email: email,
|
||
role: 'Collaborateur'
|
||
}
|
||
});
|
||
|
||
} catch (error) {
|
||
console.error('Erreur check-user-groups:', error);
|
||
res.json({
|
||
authorized: false,
|
||
message: 'Erreur serveur: ' + error.message
|
||
});
|
||
}
|
||
});
|
||
|
||
app.post('/api/initial-sync', async (req, res) => {
|
||
try {
|
||
console.log('🔄 Démarrage de la synchronisation initiale (RH)...');
|
||
|
||
const accessToken = await getApplicationToken();
|
||
|
||
const group = await callGraph(
|
||
`https://graph.microsoft.com/v1.0/groups/${GROUP_ID}?$select=id,displayName,description,mail,createdDateTime`,
|
||
accessToken
|
||
);
|
||
|
||
if (!group) {
|
||
return res.json({
|
||
success: false,
|
||
message: "Impossible de récupérer le groupe Ensup-Groupe"
|
||
});
|
||
}
|
||
|
||
const membersResponse = await callGraph(
|
||
`https://graph.microsoft.com/v1.0/groups/${GROUP_ID}/members?$select=id,givenName,surname,mail,department,jobTitle`,
|
||
accessToken
|
||
);
|
||
|
||
const members = membersResponse?.value || [];
|
||
let usersInserted = 0;
|
||
|
||
for (const member of members) {
|
||
const prenom = member.givenName || '';
|
||
const nom = member.surname || '';
|
||
const email = member.mail || '';
|
||
|
||
if (!email) continue;
|
||
|
||
try {
|
||
const existingUser = await pool.request()
|
||
.input('email', sql.VarChar, email)
|
||
.query('SELECT id FROM rh WHERE email = @email');
|
||
|
||
if (existingUser.recordset.length === 0) {
|
||
await pool.request()
|
||
.input('nom', sql.VarChar, nom)
|
||
.input('prenom', sql.VarChar, prenom)
|
||
.input('email', sql.VarChar, email)
|
||
.query('INSERT INTO rh (nom, prenom, email) VALUES (@nom, @prenom, @email)');
|
||
} else {
|
||
await pool.request()
|
||
.input('nom', sql.VarChar, nom)
|
||
.input('prenom', sql.VarChar, prenom)
|
||
.input('email', sql.VarChar, email)
|
||
.query('UPDATE rh SET nom = @nom, prenom = @prenom WHERE email = @email');
|
||
}
|
||
|
||
usersInserted++;
|
||
} catch (dbError) {
|
||
console.error(`Erreur insertion utilisateur ${email}:`, dbError.message);
|
||
}
|
||
}
|
||
|
||
res.json({
|
||
success: true,
|
||
message: "Synchronisation terminée",
|
||
groupe_sync: group.displayName,
|
||
users_sync: usersInserted
|
||
});
|
||
|
||
} catch (error) {
|
||
console.error('Erreur synchronisation:', error);
|
||
res.json({
|
||
success: false,
|
||
message: 'Erreur lors de la synchronisation: ' + error.message
|
||
});
|
||
}
|
||
});
|
||
|
||
app.post('/api/login-hybrid', async (req, res) => {
|
||
try {
|
||
const { email, mot_de_passe, entraUserId, userPrincipalName } = req.body;
|
||
const accessToken = getAccessToken(req);
|
||
|
||
if (accessToken && entraUserId) {
|
||
const userResult = await pool.request()
|
||
.input('email', sql.VarChar, email)
|
||
.query('SELECT * FROM rh WHERE email = @email');
|
||
|
||
if (userResult.recordset.length === 0) {
|
||
return res.json({
|
||
success: false,
|
||
message: "Utilisateur non autorisé (pas dans l'annuaire RH)"
|
||
});
|
||
}
|
||
|
||
const user = userResult.recordset[0];
|
||
|
||
const userGroups = [];
|
||
try {
|
||
const memberOfResponse = await callGraph(
|
||
`https://graph.microsoft.com/v1.0/users/${userPrincipalName}/memberOf?$select=id`,
|
||
accessToken
|
||
);
|
||
|
||
if (memberOfResponse?.value) {
|
||
memberOfResponse.value.forEach(g => {
|
||
if (g.id) userGroups.push(g.id);
|
||
});
|
||
}
|
||
} catch (graphError) {
|
||
console.error('Erreur récupération groupes:', graphError.message);
|
||
}
|
||
|
||
const authorized = userGroups.includes(GROUP_ID);
|
||
|
||
if (authorized) {
|
||
return res.json({
|
||
success: true,
|
||
message: "Connexion réussie via Azure AD",
|
||
user: {
|
||
id: user.id,
|
||
prenom: user.prenom,
|
||
nom: user.nom,
|
||
email: user.email,
|
||
role: 'Collaborateur'
|
||
}
|
||
});
|
||
} else {
|
||
return res.json({
|
||
success: false,
|
||
message: "Utilisateur non autorisé - pas dans le groupe requis"
|
||
});
|
||
}
|
||
}
|
||
|
||
if (email && mot_de_passe) {
|
||
const userResult = await pool.request()
|
||
.input('email', sql.VarChar, email)
|
||
.input('password', sql.VarChar, mot_de_passe)
|
||
.query(`
|
||
SELECT
|
||
u.ID,
|
||
u.Prenom,
|
||
u.Nom,
|
||
u.Email,
|
||
u.Role,
|
||
u.ServiceId,
|
||
s.Nom AS ServiceNom
|
||
FROM Users u
|
||
LEFT JOIN Services s ON u.ServiceId = s.Id
|
||
WHERE u.Email = @email AND u.MDP = @password
|
||
`);
|
||
|
||
if (userResult.recordset.length === 1) {
|
||
const user = userResult.recordset[0];
|
||
return res.json({
|
||
success: true,
|
||
message: "Connexion réussie (mode local)",
|
||
user: {
|
||
id: user.ID,
|
||
prenom: user.Prenom,
|
||
nom: user.Nom,
|
||
email: user.Email,
|
||
role: user.Role,
|
||
service: user.ServiceNom || 'Non défini'
|
||
}
|
||
});
|
||
} else {
|
||
return res.json({
|
||
success: false,
|
||
message: "Identifiants incorrects (mode local)"
|
||
});
|
||
}
|
||
}
|
||
|
||
res.json({
|
||
success: false,
|
||
message: "Aucune méthode de connexion fournie"
|
||
});
|
||
|
||
} catch (error) {
|
||
console.error('Erreur login-hybrid:', error);
|
||
res.json({
|
||
success: false,
|
||
message: 'Erreur serveur: ' + error.message
|
||
});
|
||
}
|
||
});
|
||
|
||
/**
|
||
* Route pour tester la table RH
|
||
*/
|
||
app.get('/api/rh-test', async (req, res) => {
|
||
try {
|
||
if (!pool) {
|
||
return res.status(500).json({ error: 'Base non connectée' });
|
||
}
|
||
|
||
const result = await pool.request().query('SELECT COUNT(*) as total FROM rh');
|
||
const sample = await pool.request().query('SELECT TOP 3 * FROM rh');
|
||
|
||
res.json({
|
||
message: 'Table RH OK',
|
||
total_utilisateurs: result.recordset[0].total,
|
||
echantillon: sample.recordset,
|
||
systemStatus: systemStatus
|
||
});
|
||
} catch (error) {
|
||
res.status(500).json({ error: error.message });
|
||
}
|
||
});
|
||
|
||
/**
|
||
* Route pour lister tous les utilisateurs de la table RH
|
||
*/
|
||
app.get('/api/rh-users', async (req, res) => {
|
||
try {
|
||
const limit = req.query.limit ? parseInt(req.query.limit) : 50;
|
||
const offset = req.query.offset ? parseInt(req.query.offset) : 0;
|
||
|
||
const result = await pool.request()
|
||
.input('limit', sql.Int, limit)
|
||
.input('offset', sql.Int, offset)
|
||
.query(`
|
||
SELECT id, nom, prenom, email
|
||
FROM rh
|
||
ORDER BY nom, prenom
|
||
OFFSET @offset ROWS
|
||
FETCH NEXT @limit ROWS ONLY
|
||
`);
|
||
|
||
const total = await pool.request().query('SELECT COUNT(*) as total FROM rh');
|
||
|
||
res.json({
|
||
success: true,
|
||
users: result.recordset,
|
||
total: total.recordset[0].total,
|
||
limit: limit,
|
||
offset: offset
|
||
});
|
||
} 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, () => {
|
||
console.log(`🚀 Serveur RH démarré sur http://localhost:${PORT}`);
|
||
console.log(`📊 Mode de fonctionnement RH: ${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/db-test');
|
||
console.log('- GET /api/get_declarations');
|
||
console.log('- PUT /api/declarations/:id/status');
|
||
console.log('- POST /api/exchange-token (Échange code Microsoft)');
|
||
console.log('- POST /api/auth (Microsoft Graph - legacy)');
|
||
console.log('- GET /api/admin-users (3 utilisateurs administratifs)');
|
||
console.log('- GET /api/group-members (tous les membres du groupe)');
|
||
console.log('- GET /api/formateurs-avec-declarations');
|
||
console.log('- GET /api/formateurs-vue');
|
||
console.log('- GET /api/rh-test (test table RH)');
|
||
console.log('- GET /api/rh-users (liste utilisateurs RH)');
|
||
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;
|
||
}
|
||
|
||
if (!CLIENT_SECRET) {
|
||
console.warn('⚠️ Variable d\'environnement manquante: CLIENT_SECRET');
|
||
console.warn(' Ajoutez CLIENT_SECRET dans votre fichier .env');
|
||
} else {
|
||
console.log('✅ Configuration Microsoft OAuth OK');
|
||
console.log(` Client ID: ${CLIENT_ID}`);
|
||
console.log(` Tenant ID: ${TENANT_ID}`);
|
||
console.log(` Redirect URI: ${REDIRECT_URI}`);
|
||
}
|
||
});
|
||
}
|
||
|
||
// Arrêt propre
|
||
process.on('SIGINT', async () => {
|
||
console.log('Arrêt du serveur RH...');
|
||
if (pool) {
|
||
await pool.close();
|
||
}
|
||
process.exit(0);
|
||
});
|
||
|
||
// Démarrer
|
||
startServer(); |