Aller au contenu

Gestion des Agents (Backoffice)

Interface d'administration permettant aux administrateurs et gestionnaires de créer et gérer les comptes agents pour l'application mobile.

Vue d'ensemble

Caractéristique Détails
URL /agents.php
Permissions Admin, Gestionnaire
Base de données Table agents
API Endpoints /api/admin/agents/*

Permissions

Matrice de permissions

Action Super Admin Admin Collectivité Admin Mairie Gestionnaire Lecteur
Voir agents Tous Sa collectivité Sa mairie Sa mairie ❌
Créer agent ✅ ✅ ✅ Uniquement rôle "agent" ❌
Créer responsable/admin ✅ ✅ ✅ ❌ ❌
Modifier agent ✅ ✅ ✅ Uniquement agents ❌
Réinitialiser mot de passe ✅ ✅ ✅ Uniquement agents ❌
Désactiver agent ✅ ✅ ✅ Uniquement agents ❌

Règles de sécurité

Restrictions pour gestionnaires

Les gestionnaires ne peuvent :

  • Créer que des agents (pas admin/responsable)
  • Modifier/désactiver uniquement les agents (pas admin/responsable)
  • Gérer uniquement les agents de leur mairie

Interface utilisateur

Liste des agents

Fonctionnalités :

  • Affichage paginé avec compteur total
  • Filtres :
    • Rôle (agent, responsable, admin)
    • Statut (actif, inactif)
    • Recherche textuelle (nom, prénom, email)
  • Informations affichées :
    • Nom complet avec badge de rôle
    • Email
    • Mairie et collectivité
    • Dernière connexion
    • Créé par

Actions rapides :

  • ✏️ Modifier
  • 🔑 Réinitialiser mot de passe
  • ✅/❌ Activer/Désactiver

Création d'agent

Formulaire :

Champs obligatoires:
  - Email: validation format email, unicité vérifiée
  - Nom: texte libre
  - Rôle: agent | responsable | admin

Champs optionnels:
  - Prénom: texte libre
  - Mot de passe: min 6 caractères
  - Forcer changement: checkbox (par défaut: coché)

Génération automatique du mot de passe :

Mot de passe automatique

Si le champ mot de passe est laissé vide :

  1. Un mot de passe aléatoire de 16 caractères est généré
  2. Il est affiché une seule fois dans une modal
  3. Le flag force_password_change est automatiquement activé
  4. L'agent devra changer son mot de passe à la première connexion

Workflow de création :

Séquence de création d'un agent : formulaire sans mot de passe, génération et hash côté serveur, puis affichage unique du mot de passe généré à communiquer de façon sécurisée

Modification d'agent

Champs modifiables :

  • Email (avec vérification d'unicité)
  • Nom / Prénom
  • Rôle (selon permissions)
  • Statut actif/inactif
  • Force password change

Mot de passe

Le mot de passe ne peut pas être modifié via l'édition. Utiliser "Réinitialiser mot de passe" à la place.

Réinitialisation de mot de passe

Processus :

  1. Clic sur "🔑 Réinitialiser mot de passe"
  2. Confirmation obligatoire
  3. Génération d'un nouveau mot de passe (16 caractères)
  4. Invalidation de tous les tokens JWT (déconnexion immédiate de l'agent)
  5. Affichage du mot de passe dans une modal

Sécurité

Lors de la réinitialisation :

  • Tous les tokens JWT actifs sont invalidés
  • L'agent est immédiatement déconnecté de l'app mobile
  • Le mot de passe est affiché une seule fois
  • Communication sécurisée obligatoire avec l'agent

Activation/Désactivation

Désactivation :

  • Le compte agent passe à actif = false
  • Tous les tokens JWT sont invalidés
  • L'agent ne peut plus se connecter à l'application mobile
  • Les données restent en base de données

Réactivation :

  • Le compte repasse à actif = true
  • L'agent peut se reconnecter avec ses identifiants

API Backoffice

Tous les endpoints sont sécurisés par authentification session PHP.

GET /api/admin/agents/list.php

Liste les agents selon les permissions de l'utilisateur connecté.

Query Parameters :

Paramètre Type Description
role string Filtrer par rôle (agent, responsable, admin)
actif int Filtrer par statut (0, 1)
search string Recherche dans nom, prénom, email
{
  "success": true,
  "data": [
    {
      "id": 1,
      "mairie_id": 1,
      "mairie_nom": "Mairie de Menton",
      "mairie_ville": "Menton",
      "collectivite_id": null,
      "collectivite_nom": null,
      "email": "agent@mairie.test",
      "nom": "Dupont",
      "prenom": "Jean",
      "role": "agent",
      "actif": true,
      "force_password_change": true,
      "last_login": "2026-01-18 10:00:00",
      "created_at": "2026-01-15 09:00:00",
      "updated_at": "2026-01-18 10:00:00",
      "created_by": "Admin Menton"
    }
  ],
  "total": 1
}

POST /api/admin/agents/create.php

Crée un nouveau compte agent.

{
  "email": "agent@mairie.test",
  "nom": "Dupont",
  "prenom": "Jean",
  "role": "agent",
  "password": "optionnel",
  "force_password_change": true,
  "mairie_id": 1,
  "collectivite_id": null
}
{
  "success": true,
  "message": "Agent créé avec succès",
  "agent_id": 1,
  "email": "agent@mairie.test",
  "generated_password": "a1b2c3d4e5f6g7h8",
  "warning": "Mot de passe généré automatiquement. Communiquez-le de manière sécurisée à l'agent."
}
{
  "success": false,
  "message": "Cet email est déjà utilisé"
}
{
  "success": false,
  "message": "Un responsable ne peut créer que des agents (pas admin/responsable)"
}

PUT /api/admin/agents/update.php

Modifie un agent existant.

{
  "id": 1,
  "email": "agent@mairie.test",
  "nom": "Dupont",
  "prenom": "Jean",
  "role": "responsable",
  "actif": true,
  "force_password_change": false
}
{
  "success": true,
  "message": "Agent modifié avec succès"
}

POST /api/admin/agents/pairing_generate.php

Génère (ou régénère) un token de pairing QR pour un agent. Le token expire après 15 minutes et est à usage unique.

{
  "agent_id": 1
}
{
  "success": true,
  "data": {
    "token": "a1b2c3d4e5f6...",
    "expires_at": "2026-09-06 15:30:00"
  }
}

POST /api/admin/agents/pairing_revoke.php

Révoque l'appareil apparié d'un agent (perte/vol) sans désactiver son compte. L'agent devra scanner un nouveau QR code pour se reconnecter.

{
  "agent_id": 1
}
{
  "success": true,
  "message": "Appareil révoqué avec succès"
}

POST /api/admin/agents/disable.php

Active ou désactive un agent.

{
  "id": 1,
  "actif": false
}
{
  "success": true,
  "message": "Agent désactivé avec succès"
}

Effet de bord

La désactivation invalide tous les tokens JWT de l'agent.

Suivi de position en direct (agents_carte.php)

Carte backoffice affichant la position temps réel des agents (polling), distincte de /agents.php.

  • API : GET /api/admin/agents/positions_live.php, GET /api/admin/agents/position_history.php, POST /api/admin/agent_tracking_toggle.php
  • Rétention : scripts/purge_agent_positions.php, points source='tracking' > 60 jours (voir backend/CLAUDE.md)
  • Bannière session expirée : le polling détecte une session PHP expirée (401) et arrête le suivi en affichant "Votre session a expiré — le suivi en direct est arrêté. Reconnectez-vous pour continuer." (avant fix : la carte se figeait silencieusement sans indication)
  • Distinction erreur / absence de données : les 4 points d'appel de la carte ne traitent plus success:false (erreur API) comme équivalent à "aucune position disponible" — un message d'erreur explicite est affiché dans le premier cas
  • Périmètre de la collectivité connectée : au chargement, la carte trace en pointillés (#1e3750) le contour de la commune ou de l'EPCI de l'utilisateur connecté et cadre la vue dessus, à la place de la vue France entière par défaut. Même découpage de scope que positions_live.php : superadmin (pas de contour, vue globale), admin rattaché à une collectivité (contour EPCI), tout le reste — admin mono-mairie, gestionnaire (contour commune). Source géométrique : API publique geo.api.gouv.fr (/communes/{code_insee}?geometry=contour, /epcis/{siren}?geometry=contour), la même que le "liseré communal" de l'app citoyens — aucune donnée géométrique stockée côté UrbaFix. Le cadrage périodique sur les positions des agents (toutes les 30s) reste inchangé et prend le dessus dès qu'au moins un agent a une position active.

Modèle de données

Table agents

CREATE TABLE `agents` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `mairie_id` int(11) NOT NULL,
  `collectivite_id` int(11) DEFAULT NULL,
  `email` varchar(255) NOT NULL,
  `password_hash` varchar(255) NOT NULL,
  `nom` varchar(255) NOT NULL,
  `prenom` varchar(255) NOT NULL,
  `role` enum('agent','responsable','admin') DEFAULT 'agent',
  `actif` tinyint(1) DEFAULT 1,
  `force_password_change` tinyint(1) DEFAULT 0,
  `last_login` timestamp NULL DEFAULT NULL,
  `created_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
  `updated_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
  `created_by` int(11) DEFAULT NULL COMMENT 'ID du user qui a créé cet agent',
  PRIMARY KEY (`id`),
  UNIQUE KEY `email` (`email`),
  KEY `mairie_id` (`mairie_id`),
  KEY `collectivite_id` (`collectivite_id`),
  CONSTRAINT `fk_agents_mairie` FOREIGN KEY (`mairie_id`) REFERENCES `mairies` (`id`) ON DELETE CASCADE,
  CONSTRAINT `fk_agents_collectivite` FOREIGN KEY (`collectivite_id`) REFERENCES `collectivites` (`id`) ON DELETE SET NULL,
  CONSTRAINT `fk_agents_created_by` FOREIGN KEY (`created_by`) REFERENCES `users` (`id`) ON DELETE SET NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

Relations

Modèle de données des agents : AGENTS au centre, rattaché à une mairie, une collectivité et un créateur, avec ses tokens, son journal d'activité et ses affectations d'incidents

Affectation multiple (incident_agents)

Un incident peut être assigné à plusieurs agents, avec un référent unique (is_referent). Gérée depuis incident_detail.php (backoffice). incidents.assigned_agent_id reste en base pour compatibilité mais n'est plus la source de vérité — utiliser incident_agents.

Sécurité

Authentification

  • Session PHP pour l'accès backoffice
  • Vérification du rôle (admin ou gestionnaire)
  • Vérification des permissions spécifiques à chaque action

Mots de passe

  • Hashage : bcrypt via password_hash()
  • Longueur minimale : 6 caractères
  • Génération automatique : 16 caractères aléatoires sécurisés (random_bytes())
  • Force changement : obligatoire pour mots de passe générés

Validation

// Email
if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
    http_response_code(400);
    echo json_encode(['success' => false, 'message' => 'Email invalide']);
    exit;
}

// Unicité email
$existing = $db->fetchOne('SELECT id FROM agents WHERE email = ?', [$email]);
if ($existing) {
    http_response_code(409);
    echo json_encode(['success' => false, 'message' => 'Cet email est déjà utilisé']);
    exit;
}

// Hashage
$passwordHash = password_hash($password, PASSWORD_BCRYPT);

Journalisation

Toutes les actions sont loggées :

error_log("Agent créé: ID=$agentId, email=$email, par user_id={$user['id']}");
error_log("Agent modifié: ID=$agentId par user_id={$user['id']}");
error_log("Mot de passe réinitialisé pour agent ID=$agentId par user_id={$user['id']}");
error_log("Agent désactivé: ID=$agentId par user_id={$user['id']}");

Invalidation des tokens

Lors de réinitialisation ou désactivation :

// Invalider tous les refresh tokens
$db->execute(
    'DELETE FROM agent_refresh_tokens WHERE agent_id = ?',
    [$agentId]
);

Migration

Fichier SQL

Emplacement : /database/migration_agents.sql

Tables créées :

  • agents - Comptes agents
  • agent_refresh_tokens - Tokens JWT
  • agent_activity_log - Journal d'activité

Modification :

  • Ajout de assigned_agent_id dans la table incidents

Application de la migration

docker-compose exec -T db mysql \
  -u urbafix_user \
  -purbafix_pass_2024 \
  urbafix < database/migration_agents.sql

Bonnes pratiques

Communication des mots de passe

Sécurité des mots de passe

Les mots de passe générés sont affichés une seule fois. Utilisez un canal sécurisé :

  • ✅ Email chiffré
  • ✅ Message privé sécurisé
  • ✅ Remise en main propre
  • ❌ Email non chiffré
  • ❌ SMS
  • ❌ Messagerie instantanée non chiffrée

Gestion des rôles

Rôle Utilisation recommandée
Agent Agents terrain utilisant l'application mobile
Responsable Chefs d'équipe, superviseurs
Admin Directeurs de service, DST

Désactivation vs Suppression

Pas de suppression

L'interface ne propose que la désactivation (pas de suppression).

Avantages :

  • Historique préservé
  • Incidents assignés conservés
  • Possibilité de réactivation
  • Traçabilité complète

Workflow complet

Workflow complet : l'administrateur crée l'agent, le mot de passe généré est communiqué de façon sécurisée, puis l'agent se connecte et force le changement de mot de passe pour devenir actif

Tests

Scénario 1 : Création d'agent

  1. Se connecter au backoffice en tant qu'admin
  2. Naviguer vers /agents.php
  3. Cliquer sur "Ajouter un Agent"
  4. Remplir :
    • Email : test.agent@mairie.test
    • Nom : Test
    • Prénom : Agent
    • Rôle : agent
    • Laisser le mot de passe vide
  5. Enregistrer
  6. Vérifier : modal avec mot de passe généré
  7. Copier le mot de passe
  8. Vérifier : agent apparaît dans la liste avec badge "Actif"

Scénario 2 : Modification

  1. Cliquer sur "Modifier" pour un agent
  2. Changer le prénom
  3. Enregistrer
  4. Vérifier : modification appliquée dans la liste

Scénario 3 : Réinitialisation MDP

  1. Cliquer sur "Réinitialiser mot de passe"
  2. Confirmer
  3. Vérifier : modal avec nouveau mot de passe
  4. Copier le mot de passe
  5. Vérifier : tokens JWT invalidés (agent déconnecté de l'app mobile)

Scénario 4 : Désactivation

  1. Cliquer sur "Désactiver"
  2. Confirmer
  3. Vérifier : badge passe à "Inactif"
  4. Vérifier : agent ne peut plus se connecter à l'app mobile
  5. Cliquer sur "Activer"
  6. Vérifier : badge repasse à "Actif"