267 lines
12 KiB
JavaScript
267 lines
12 KiB
JavaScript
/**
|
||
* 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('<span class="dsi-state dsi-state-alert">Jeton d’installation</span>');
|
||
if (compte.mot_de_passe_requis) {
|
||
etats.push(compte.mot_de_passe_defini
|
||
? '<span class="dsi-state dsi-state-on">Mot de passe défini</span>'
|
||
: '<span class="dsi-state dsi-state-wait">Mot de passe à définir</span>');
|
||
}
|
||
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
|
||
? '<button type="button" class="button button-secondary compte-reset-password">Effacer le mot de passe</button>'
|
||
: '',
|
||
'<button type="button" class="button button-secondary compte-journal">Journal</button>',
|
||
].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
|
||
? `<br><small>${escapeHtml(entree.cible_table)}${entree.cible_id ? ` #${Number(entree.cible_id)}` : ''}</small>`
|
||
: '',
|
||
entree.contexte
|
||
? `<br><small class="dsi-journal-context">${escapeHtml(JSON.stringify(entree.contexte))}</small>`
|
||
: '',
|
||
].join('');
|
||
|
||
const classeResultat = entree.resultat === 'SUCCES' ? '' : ` dsi-journal-${entree.resultat.toLowerCase()}`;
|
||
|
||
return `
|
||
<tr class="dsi-journal-row${classeResultat}">
|
||
<td>${formatDateTime(entree.date_evenement)}</td>
|
||
<td>${escapeHtml(LIBELLES_CATEGORIE[entree.categorie] || entree.categorie)}</td>
|
||
<td>${escapeHtml(entree.acteur || 'Anonyme')}<br><small>${escapeHtml(LIBELLES_ROLE[entree.role] || entree.role || '—')}</small></td>
|
||
<td><code>${escapeHtml(entree.action)}</code></td>
|
||
<td>${detail}</td>
|
||
<td>${escapeHtml(entree.adresse_ip || '—')}</td>
|
||
</tr>`;
|
||
}
|
||
|
||
/** 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]) => `
|
||
<article class="summary-card"><span>${escapeHtml(libelle)}</span><strong>${Number(valeur || 0)}</strong></article>
|
||
`).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('')
|
||
: '<tr><td colspan="6">Aucune entrée pour ces filtres.</td></tr>';
|
||
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 };
|
||
})();
|