access = new Access($pdo); $this->comptes = new CompteModel($pdo); $this->journal = new JournalModel($pdo); } /** * GET api/dsi_comptes.php — comptes d'accès et identités sans compte. * * Paramètres GET : role, actif, q. */ public function comptes(): void { $this->access->requireDsi(); $comptes = $this->comptes->lister([ 'role' => (string) ($_GET['role'] ?? ''), 'actif' => (string) ($_GET['actif'] ?? ''), 'q' => (string) ($_GET['q'] ?? ''), ]); JsonResponse::send(200, [ 'comptes' => $comptes, 'manquants' => $this->comptes->manquants(), // Tant qu'un compte porte encore le jeton public de la migration, // l'interface doit le crier : c'est une porte ouverte documentée. 'jeton_installation_actif' => $this->comptes->installationEnCours(), 'roles' => $this->rolesDisponibles(), ]); } /** * POST api/dsi_compte_creer.php — crée un compte et renvoie son jeton. * * Charge utile : role, agent_id, structure_id, libelle, commentaire, * csrf_token. */ public function creerCompte(): void { $this->access->requireDsi(); Request::requireMethod('POST'); $payload = Request::json(); Csrf::assertPayload($payload); $role = strtoupper(trim((string) ($payload['role'] ?? ''))); $agentId = filter_var($payload['agent_id'] ?? null, FILTER_VALIDATE_INT) ?: null; $structureId = filter_var($payload['structure_id'] ?? null, FILTER_VALIDATE_INT) ?: null; $libelle = trim((string) ($payload['libelle'] ?? '')); $commentaire = trim((string) ($payload['commentaire'] ?? '')); try { $resultat = $this->comptes->creer( $role, $agentId !== null ? (int) $agentId : null, $structureId !== null ? (int) $structureId : null, $libelle, $commentaire !== '' ? $commentaire : null ); } catch (DomainException $e) { JsonResponse::send(422, ['error' => $e->getMessage()]); return; } $compte = $resultat['compte']; Journal::administration('COMPTE_CREE', [ 'cible_table' => 'compte_acces', 'cible_id' => (int) $compte['id_compte'], 'description' => sprintf( 'Création du compte d’accès « %s » (%s).', (string) $compte['identite_libelle'], (string) $compte['role'] ), 'contexte' => ['role' => $compte['role'], 'prefixe' => $compte['jeton_prefixe']], ]); JsonResponse::send(201, [ 'message' => sprintf('Compte créé pour %s. Transmettez le jeton : il ne sera plus affiché.', (string) $compte['identite_libelle']), 'compte' => $compte, 'jeton' => $resultat['jeton'], ]); } /** * POST api/dsi_compte_jeton.php — régénère le jeton d'un compte. * * L'ancien jeton cesse de fonctionner immédiatement : c'est la procédure à * suivre pour un jeton perdu comme pour un jeton compromis. * * Charge utile : compte_id, csrf_token. */ public function regenererJeton(): void { $this->access->requireDsi(); Request::requireMethod('POST'); $payload = Request::json(); Csrf::assertPayload($payload); $compteId = (int) (filter_var($payload['compte_id'] ?? null, FILTER_VALIDATE_INT) ?: 0); try { $jeton = $this->comptes->regenererJeton($compteId); } catch (DomainException $e) { JsonResponse::send(404, ['error' => $e->getMessage()]); return; } $compte = $this->comptes->trouver($compteId) ?? []; Journal::administration('JETON_REGENERE', [ 'cible_table' => 'compte_acces', 'cible_id' => $compteId, 'description' => sprintf( 'Régénération du jeton de « %s » : l’ancien jeton est révoqué.', (string) ($compte['identite_libelle'] ?? 'compte inconnu') ), 'contexte' => ['prefixe' => $compte['jeton_prefixe'] ?? null], ]); JsonResponse::send(200, [ 'message' => 'Nouveau jeton généré. L’ancien ne fonctionne plus.', 'compte' => $compte, 'jeton' => $jeton, ]); } /** * POST api/dsi_compte_mot_de_passe.php — efface le mot de passe d'un compte * privilégié. * * La DSI ne REMPLACE pas le mot de passe, elle le RETIRE : le titulaire en * choisira un nouveau à sa prochaine connexion, avec son jeton. C'est ce qui * garantit que personne d'autre que lui ne l'a jamais connu — y compris la * personne qui vient de le réinitialiser. * * Charge utile : compte_id, csrf_token. */ public function reinitialiserMotDePasse(): void { $this->access->requireDsi(); Request::requireMethod('POST'); $payload = Request::json(); Csrf::assertPayload($payload); $compteId = (int) (filter_var($payload['compte_id'] ?? null, FILTER_VALIDATE_INT) ?: 0); try { $compte = $this->comptes->reinitialiserMotDePasse($compteId); } catch (DomainException $e) { JsonResponse::send(422, ['error' => $e->getMessage()]); return; } Journal::administration('MOT_DE_PASSE_REINITIALISE', [ 'cible_table' => 'compte_acces', 'cible_id' => $compteId, 'description' => sprintf( 'Mot de passe de « %s » effacé : le titulaire en définira un nouveau à sa prochaine connexion.', (string) $compte['identite_libelle'] ), 'contexte' => ['role' => $compte['role']], ]); JsonResponse::send(200, [ 'message' => sprintf( 'Mot de passe effacé pour %s. Son jeton reste valable : il en choisira un nouveau à sa prochaine connexion.', (string) $compte['identite_libelle'] ), ]); } /** * POST api/dsi_compte_statut.php — active, désactive ou supprime un compte. * * La suppression efface le compte mais PAS ses traces : le journal conserve * le libellé figé de l'acteur (clé étrangère en ON DELETE SET NULL). * * Charge utile : compte_id, action (ACTIVER | DESACTIVER | SUPPRIMER), * csrf_token. */ public function changerStatutCompte(): void { $this->access->requireDsi(); Request::requireMethod('POST'); $payload = Request::json(); Csrf::assertPayload($payload); $compteId = (int) (filter_var($payload['compte_id'] ?? null, FILTER_VALIDATE_INT) ?: 0); $action = strtoupper(trim((string) ($payload['action'] ?? ''))); if (!in_array($action, ['ACTIVER', 'DESACTIVER', 'SUPPRIMER'], true)) { JsonResponse::send(422, ['error' => 'Action inconnue.']); } // La fiche est lue AVANT l'opération : après une suppression, il n'y // aurait plus de libellé à écrire dans le journal. $compte = $this->comptes->trouver($compteId); if ($compte === null) { JsonResponse::send(404, ['error' => 'Compte introuvable.']); return; } // Se retirer soi-même son propre accès en pleine session est une erreur // de manipulation, jamais une intention : on la refuse. if ($compteId === $this->access->accountId() && $action !== 'ACTIVER') { JsonResponse::send(422, ['error' => 'Vous ne pouvez pas désactiver ni supprimer le compte avec lequel vous êtes connecté.']); } try { if ($action === 'SUPPRIMER') { $this->comptes->supprimer($compteId); } else { $this->comptes->definirActif($compteId, $action === 'ACTIVER'); } } catch (DomainException $e) { JsonResponse::send(422, ['error' => $e->getMessage()]); return; } $libelles = [ 'ACTIVER' => 'Réactivation', 'DESACTIVER' => 'Désactivation', 'SUPPRIMER' => 'Suppression', ]; Journal::administration('COMPTE_' . $action, [ 'cible_table' => 'compte_acces', 'cible_id' => $compteId, 'description' => sprintf( '%s du compte d’accès « %s » (%s).', $libelles[$action], (string) $compte['identite_libelle'], (string) $compte['role'] ), 'contexte' => ['role' => $compte['role'], 'prefixe' => $compte['jeton_prefixe']], ]); JsonResponse::send(200, [ 'message' => sprintf('%s effectuée pour %s.', $libelles[$action], (string) $compte['identite_libelle']), ]); } /** * POST api/dsi_comptes_manquants.php — équipe d'un coup toutes les identités * actives sans compte. * * C'est l'outil de MISE EN SERVICE : à l'installation, aucun agent ni * responsable n'a de jeton, donc personne ne peut entrer. La réponse porte * tous les jetons produits, en clair — c'est la seule occasion de les lire, * et elle est faite pour être imprimée ou copiée avant de refermer l'écran. * * Charge utile : csrf_token. */ public function genererJetonsManquants(): void { $this->access->requireDsi(); Request::requireMethod('POST'); $payload = Request::json(); Csrf::assertPayload($payload); try { $produits = $this->comptes->genererManquants(); } catch (DomainException $e) { JsonResponse::send(422, ['error' => $e->getMessage()]); return; } Journal::administration('JETONS_GENERES_EN_LOT', [ 'cible_table' => 'compte_acces', 'cible_id' => null, 'description' => sprintf('Génération de %d jeton(s) pour les identités sans compte.', count($produits)), 'contexte' => ['nombre' => count($produits)], ]); JsonResponse::send(200, [ 'message' => $produits === [] ? 'Toutes les identités actives disposent déjà d’un compte.' : sprintf('%d jeton(s) généré(s). Distribuez-les maintenant : ils ne seront plus affichés.', count($produits)), 'jetons' => $produits, ]); } /** * GET api/dsi_journal.php — recherche paginée dans le journal. * * Paramètres GET : categorie, resultat, role, id_compte, q, du, au, page, * taille. */ public function journal(): void { $this->access->requireDsi(); $resultat = $this->journal->rechercher([ 'categorie' => (string) ($_GET['categorie'] ?? ''), 'resultat' => (string) ($_GET['resultat'] ?? ''), 'role' => (string) ($_GET['role'] ?? ''), 'id_compte' => (int) ($_GET['id_compte'] ?? 0), 'q' => (string) ($_GET['q'] ?? ''), 'du' => (string) ($_GET['du'] ?? ''), 'au' => (string) ($_GET['au'] ?? ''), 'page' => (int) ($_GET['page'] ?? 1), 'taille' => (int) ($_GET['taille'] ?? JournalModel::TAILLE_PAGE), ]); JsonResponse::send(200, array_merge($resultat, [ 'statistiques' => $this->journal->statistiques(), ])); } /** * POST api/dsi_journal_purge.php — supprime les entrées les plus anciennes. * * La purge est elle-même journalisée, et l'est APRÈS coup : sa propre trace * ne peut donc pas être emportée par la purge qu'elle décrit. * * Charge utile : jours (minimum 30), csrf_token. */ public function purgerJournal(): void { $this->access->requireDsi(); Request::requireMethod('POST'); $payload = Request::json(); Csrf::assertPayload($payload); $jours = (int) (filter_var($payload['jours'] ?? null, FILTER_VALIDATE_INT) ?: 0); if ($jours < 30) { JsonResponse::send(422, ['error' => 'La purge ne peut pas descendre sous 30 jours de conservation.']); } $supprimees = $this->journal->purger($jours); Journal::administration('JOURNAL_PURGE', [ 'cible_table' => 'journal_activite', 'cible_id' => null, 'description' => sprintf('Purge du journal : %d entrée(s) de plus de %d jours supprimée(s).', $supprimees, $jours), 'contexte' => ['jours' => $jours, 'supprimees' => $supprimees], ]); JsonResponse::send(200, [ 'message' => sprintf('%d entrée(s) supprimée(s).', $supprimees), 'supprimees' => $supprimees, ]); } /** * Rôles proposés à la création d'un compte, avec leur libellé. * * @return array */ private function rolesDisponibles(): array { return [ ['code' => Access::ROLE_AGENT, 'libelle' => 'Agent', 'identite' => 'AGENT'], ['code' => Access::ROLE_RESPONSABLE, 'libelle' => 'Responsable de structure', 'identite' => 'STRUCTURE'], ['code' => Access::ROLE_SERVICE, 'libelle' => 'Service Enfance (administrateur)', 'identite' => 'LIBELLE'], ['code' => Access::ROLE_DSI, 'libelle' => 'DSI', 'identite' => 'LIBELLE'], ]; } }