/** * Espace DSI (`PTA.modules.dsi`), écran dsi.php — V7.11. * * Deux blocs indépendants dans une seule page : * - les COMPTES D'ACCÈS : création, régénération de jeton, activation, * suppression, et génération en lot pour les identités qui n'ont pas encore * de compte ; * - le JOURNAL D'ACTIVITÉ : recherche filtrée et paginée, statistiques de tête, * purge des entrées anciennes. * * Le premier bloc est rendu par `comptes-acces.js`, partagé avec l'onglet * Administration : ce fichier ne lui apporte que ce qui est propre à la DSI — * les rôles privilégiés et leur champ « Nom du titulaire », les badges de mot de * passe (V7.12), l'alerte du jeton d'installation et le bouton qui filtre le * journal sur un compte. Le second bloc, le journal, n'existe QUE sur cet écran * et vit donc entièrement ici. * * RÈGLE CENTRALE DE L'ÉCRAN : un jeton n'existe en clair que dans la réponse qui * vient de le produire. Il est donc affiché dans un panneau dédié, qui reste * ouvert tant que la DSI ne l'a pas refermé, et n'est jamais rechargé depuis le * serveur — la base n'en garde qu'une empreinte. Toute la mise en garde de * l'interface découle de là. */ (() => { 'use strict'; const PTA = window.PTA; // core.js ou comptes-acces.js absent : la page reste lisible, simplement non // interactive. if (!PTA?.shared?.ecranComptesAcces) return; const { apiJson, postJson, showMessageIn, formatDateTime, escapeHtml } = PTA.utils; const el = (id) => document.getElementById(id); const message = (id, texte, type = 'info') => showMessageIn(id, texte, type); /** Page courante du journal, seul état conservé par le module. */ let pageJournal = 1; const LIBELLES_ROLE = { DSI: 'DSI', SERVICE_ENFANCE: 'Service Enfance', RESPONSABLE_STRUCTURE: 'Responsable de structure', AGENT: 'Agent', }; const LIBELLES_CATEGORIE = { CONNEXION: 'Connexion', ECRITURE: 'Écriture', ACCES_REFUSE: 'Refus d’accès', ADMINISTRATION: 'Administration', SYSTEME: 'Système', }; // ----------------------------------------------------------------- comptes /** * Badges d'état propres à la DSI, ajoutés à « Actif / Désactivé ». * * Second facteur (V7.12) : seuls les comptes Service Enfance et DSI en * portent un. « À définir » n'est pas une anomalie — c'est l'état normal * d'un compte neuf ou fraîchement réinitialisé. */ function etatsCompte(compte) { const etats = []; if (compte.jeton_installation) etats.push('Jeton d’installation'); if (compte.mot_de_passe_requis) { etats.push(compte.mot_de_passe_defini ? 'Mot de passe défini' : 'Mot de passe à définir'); } return etats; } /** * Boutons propres à la DSI en fin de ligne : effacement du mot de passe (la * DSI peut l'effacer, jamais en poser un) et accès au journal du compte. */ function actionsCompte(compte) { return [ compte.mot_de_passe_requis && compte.mot_de_passe_defini ? '' : '', '', ].join(''); } /** * Traite les deux boutons que la fabrique ne connaît pas. * @returns {boolean} Vrai s'il faut recharger la liste des comptes derrière. */ async function actionCompte(evenement, { bouton, compteId, titulaire }) { if (bouton.classList.contains('compte-reset-password')) { if (!window.confirm(`Effacer le mot de passe de ${titulaire} ? Son jeton reste valable, et il en choisira un nouveau à sa prochaine connexion.`)) return false; const donnees = await postJson('api/dsi_compte_mot_de_passe.php', { compte_id: compteId }); message('dsi-accounts-message', donnees.message, 'success'); return true; } if (bouton.classList.contains('compte-journal')) { // Filtre le journal sur ce compte, sans quitter la page ni toucher // à la liste des comptes. el('dsi-journal-search').value = ''; pageJournal = 1; await chargerJournal({ id_compte: compteId }); el('dsi-journal-body')?.scrollIntoView({ behavior: 'smooth', block: 'start' }); } return false; } const ecranComptes = PTA.shared.ecranComptesAcces({ prefixe: 'dsi', endpoints: { liste: 'api/dsi_comptes.php', creer: 'api/dsi_compte_creer.php', jeton: 'api/dsi_compte_jeton.php', statut: 'api/dsi_compte_statut.php', manquants: 'api/dsi_comptes_manquants.php', }, // Seul l'espace DSI crée des comptes sans identité en base : eux seuls // ont besoin du champ libre « Nom du titulaire », et de ces deux aides. avecLibelle: true, aides: { SERVICE_ENFANCE: 'Compte d’administration métier : accès à tous les lieux, à tous les agents et à la validation des plannings. Il n’est rattaché à aucun agent en base — indiquez simplement le nom de son titulaire. Son titulaire choisira un mot de passe à sa première connexion, en plus du jeton.', DSI: 'Compte d’administration technique : comptes d’accès et journal uniquement, aucun accès aux plannings. Prévoyez-en au moins deux, pour ne pas dépendre d’un seul jeton. Son titulaire choisira un mot de passe à sa première connexion, en plus du jeton.', }, rappelJetons: ' Les comptes Service Enfance et DSI choisiront en plus un mot de passe à leur première connexion.', etatsSupplementaires: etatsCompte, actionsSupplementaires: actionsCompte, actionPersonnalisee: actionCompte, // Le jeton d'installation est publié en clair dans la migration SQL : // l'alerte reste affichée tant qu'il n'a pas été régénéré. apresChargement: (donnees) => { const alerte = el('dsi-installation-warning'); if (alerte) alerte.hidden = !donnees.jeton_installation_actif; }, }); // ----------------------------------------------------------------- journal /** Une ligne du journal. */ function ligneJournal(entree) { const detail = [ entree.description ? escapeHtml(entree.description) : '', entree.cible_table ? `
${escapeHtml(entree.cible_table)}${entree.cible_id ? ` #${Number(entree.cible_id)}` : ''}` : '', entree.contexte ? `
${escapeHtml(JSON.stringify(entree.contexte))}` : '', ].join(''); const classeResultat = entree.resultat === 'SUCCES' ? '' : ` dsi-journal-${entree.resultat.toLowerCase()}`; return ` ${formatDateTime(entree.date_evenement)} ${escapeHtml(LIBELLES_CATEGORIE[entree.categorie] || entree.categorie)} ${escapeHtml(entree.acteur || 'Anonyme')}
${escapeHtml(LIBELLES_ROLE[entree.role] || entree.role || '—')} ${escapeHtml(entree.action)} ${detail} ${escapeHtml(entree.adresse_ip || '—')} `; } /** Bandeau de chiffres au-dessus du journal. */ function afficherStatistiques(stats) { const cible = el('dsi-journal-stats'); if (!cible || !stats) return; const cartes = [ ['Entrées au total', stats.total], ['Connexions (24 h)', stats.connexions_24h], ['Refus (24 h)', stats.refus_24h], ['Écritures (7 j)', stats.sept_jours?.ECRITURE || 0], ]; cible.innerHTML = cartes.map(([libelle, valeur]) => `
${escapeHtml(libelle)}${Number(valeur || 0)}
`).join(''); } /** * Recharge le journal. * * @param {object} supplements Filtres imposés par le code (ex. id_compte * depuis le bouton « Journal » d'un compte), qui * s'ajoutent à ceux saisis dans l'écran. */ async function chargerJournal(supplements = {}) { const parametres = new URLSearchParams(); const champs = { categorie: el('dsi-journal-category')?.value || '', resultat: el('dsi-journal-result')?.value || '', role: el('dsi-journal-role')?.value || '', du: el('dsi-journal-from')?.value || '', au: el('dsi-journal-to')?.value || '', q: el('dsi-journal-search')?.value.trim() || '', }; Object.entries({ ...champs, ...supplements }).forEach(([cle, valeur]) => { if (valeur !== '' && valeur !== null && valeur !== undefined) parametres.set(cle, String(valeur)); }); parametres.set('page', String(pageJournal)); message('dsi-journal-message', 'Chargement du journal…'); try { const donnees = await apiJson(`api/dsi_journal.php?${parametres.toString()}`); const entrees = donnees.entrees || []; el('dsi-journal-body').innerHTML = entrees.length ? entrees.map(ligneJournal).join('') : 'Aucune entrée pour ces filtres.'; afficherStatistiques(donnees.statistiques); // Le serveur peut ramener la page demandée à la dernière page réelle. pageJournal = Number(donnees.page || 1); el('dsi-journal-page-label').textContent = `Page ${pageJournal} sur ${Number(donnees.pages || 1)} — ${Number(donnees.total || 0)} entrée(s)`; el('dsi-journal-previous').disabled = pageJournal <= 1; el('dsi-journal-next').disabled = pageJournal >= Number(donnees.pages || 1); message('dsi-journal-message', ''); } catch (erreur) { message('dsi-journal-message', erreur.message, 'error'); } } /** Purge des entrées anciennes, confirmée puis journalisée côté serveur. */ async function purgerJournal(evenement) { evenement.preventDefault(); const jours = Number(el('dsi-purge-days').value); if (!window.confirm(`Supprimer définitivement les entrées de plus de ${jours} jours ?`)) return; try { const donnees = await postJson('api/dsi_journal_purge.php', { jours }); message('dsi-journal-message', donnees.message, 'success'); pageJournal = 1; await chargerJournal(); } catch (erreur) { message('dsi-journal-message', erreur.message, 'error'); } } function init() { const params = window.PTA_PAGE_CONFIG?.params || {}; // Comptes, jetons et formulaire de création : branchés et chargés par la // fabrique partagée. ecranComptes.attacher(); ['dsi-journal-category', 'dsi-journal-result', 'dsi-journal-role', 'dsi-journal-from', 'dsi-journal-to', 'dsi-journal-search'] .forEach((identifiant) => el(identifiant)?.addEventListener('change', () => { pageJournal = 1; chargerJournal(); })); el('dsi-journal-refresh')?.addEventListener('click', () => chargerJournal()); el('dsi-journal-previous')?.addEventListener('click', () => { pageJournal = Math.max(1, pageJournal - 1); chargerJournal(); }); el('dsi-journal-next')?.addEventListener('click', () => { pageJournal += 1; chargerJournal(); }); el('dsi-purge-form')?.addEventListener('submit', purgerJournal); // Lien profond « journal de ce compte » venu d'une autre page. chargerJournal(params.compteId ? { id_compte: params.compteId } : {}); } PTA.modules.dsi = { init, chargerComptes: ecranComptes.chargerComptes, chargerJournal }; })();