Files
pta/assets/js/dsi.js
2026-08-27 11:38:59 +02:00

267 lines
12 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
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.
/**
* 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 };
})();