Authentification Collectivité (EPCI)¶
⬇️ Télécharger cette page en Markdown
Vue d'ensemble¶
L'authentification collectivité permet aux utilisateurs de se connecter en tant qu'EPCI (Établissement Public de Coopération Intercommunale) pour visualiser et gérer les incidents de toutes les communes membres simultanément.
Version
Fonctionnalité ajoutée dans v2.1 (Janvier 2026)
Architecture¶
Modèle de données¶
Table users:
mairie_id INT -- Mairie d'origine de l'utilisateur
collectivite_id INT NULL -- ID de la collectivité si utilisateur EPCI
Relation:
- Utilisateur mairie: collectivite_id = NULL → accès uniquement à sa mairie
- Utilisateur collectivité: collectivite_id = 18 → accès aux 15 communes de la CA Riviera Française
Table services_mairie:
scope ENUM('mairie', 'collectivite') -- Niveau du service
mairie_id INT NULL -- Si scope='mairie'
collectivite_id INT NULL -- Si scope='collectivite'
Table mairies:
Fonctionnalités¶
1. Dashboard multi-villes¶
Carte interactive Leaflet:
// Marqueurs violets 🏛️ pour chaque commune
communes.forEach(function(commune) {
const icon = L.divIcon({
html: `<div style="background: #8B5CF6; color: white; ...">
🏛️ ${commune.ville} (${commune.nb_incidents})
</div>`
});
L.marker([lat, lon], {icon: icon})
.bindPopup(`<strong>${commune.ville}</strong><br>
Nouveaux: ${commune.nb_nouveaux}<br>
En cours: ${commune.nb_en_cours}<br>
Résolus: ${commune.nb_resolus}`)
.addTo(communeMarkers);
bounds.extend([lat, lon]);
});
// Zoom automatique sur toutes les communes
if (bounds.isValid()) {
map.fitBounds(bounds, {padding: [50, 50]});
}
Carte "Répartition par commune":
<?php if ($auth->isCollectiviteUser()): ?>
<div class="card">
<div class="card-header"><h3>Répartition par commune</h3></div>
<?php
$cityStats = $db->fetchAll(
"SELECT m.ville, COUNT(i.id) as total,
SUM(CASE WHEN i.statut = 'nouveau' THEN 1 ELSE 0 END) as nouveaux
FROM mairies m
LEFT JOIN incidents i ON m.id = i.mairie_id
WHERE m.id IN ($mairiePlaceholders)
GROUP BY m.id ORDER BY total DESC",
$mairieIds
);
?>
<table>...</table>
</div>
<?php endif; ?>
2. Liste incidents multi-villes¶
Dropdown filtre commune:
<?php if ($auth->isCollectiviteUser()): ?>
<div class="form-group">
<label for="mairie">Commune</label>
<select id="mairie" name="mairie">
<option value="">Toutes les communes</option>
<?php foreach ($auth->getAccessibleMairies() as $mairie): ?>
<option value="<?= $mairie['id'] ?>">
<?= htmlspecialchars($mairie['ville']) ?>
</option>
<?php endforeach; ?>
</select>
</div>
<?php endif; ?>
Badge ville sur chaque incident:
<?php if ($auth->isCollectiviteUser()): ?>
<span style="font-size: 11px; color: #6b7280; background: #f3f4f6;">
<?= htmlspecialchars($incident['mairie_ville']) ?>
</span>
<?php endif; ?>
3. Services intercommunaux¶
Formulaire création service:
<div class="form-group">
<label for="scope">Niveau</label>
<select id="scope" name="scope" required>
<option value="mairie">Service de la mairie</option>
<?php if ($auth->isCollectiviteUser()): ?>
<option value="collectivite">Service intercommunal (EPCI)</option>
<?php endif; ?>
</select>
</div>
Backend INSERT:
$scope = $_POST['scope'] ?? 'mairie';
$collectiviteId = null;
if ($scope === 'collectivite' && $auth->isCollectiviteUser()) {
$collectiviteId = $auth->getCollectiviteId();
$mairieId = null;
}
$db->execute(
"INSERT INTO services_mairie (mairie_id, collectivite_id, scope, nom, couleur)
VALUES (?, ?, ?, ?, ?)",
[$mairieId, $collectiviteId, $scope, $nom, $couleur]
);
Méthodes Auth¶
Nouvelles méthodes (v2.1)¶
// Obtenir l'ID de la collectivité
public function getCollectiviteId(): ?int {
return $_SESSION['collectivite_id'] ?? null;
}
// Vérifier si utilisateur collectivité
public function isCollectiviteUser(): bool {
return !empty($_SESSION['collectivite_id']);
}
// Obtenir tous les IDs de mairies accessibles
public function getMairieIdsForUser(): array {
$collectiviteId = $this->getCollectiviteId();
if (!$collectiviteId) {
return [$this->getMairieId()];
}
$mairies = $this->db->fetchAll(
"SELECT m.id FROM mairies m
JOIN collectivites c ON m.code_siren_epci = c.code_siren_epci
WHERE c.id = ?",
[$collectiviteId]
);
return array_column($mairies, 'id');
}
// Obtenir les objets mairie complets (pour dropdowns)
public function getAccessibleMairies(): array {
$collectiviteId = $this->getCollectiviteId();
if (!$collectiviteId) {
$mairie = $this->db->fetchOne(
"SELECT id, nom, ville FROM mairies WHERE id = ?",
[$this->getMairieId()]
);
return $mairie ? [$mairie] : [];
}
return $this->db->fetchAll(
"SELECT m.id, m.nom, m.ville
FROM mairies m
JOIN collectivites c ON m.code_siren_epci = c.code_siren_epci
WHERE c.id = ?
ORDER BY m.ville ASC",
[$collectiviteId]
);
}
// Vérifier accès à un incident spécifique
public function canAccessIncident(int $incidentId): bool {
$accessibleMairieIds = $this->getMairieIdsForUser();
$accessibleTypeIds = $this->getAccessibleTypeIds();
$placeholders = implode(',', array_fill(0, count($accessibleMairieIds), '?'));
$sql = "SELECT COUNT(*) as count FROM incidents
WHERE id = ? AND mairie_id IN ($placeholders)";
$params = array_merge([$incidentId], $accessibleMairieIds);
if ($accessibleTypeIds !== null && !empty($accessibleTypeIds)) {
$typePlaceholders = implode(',', array_fill(0, count($accessibleTypeIds), '?'));
$sql .= " AND type_id IN ($typePlaceholders)";
$params = array_merge($params, $accessibleTypeIds);
}
$result = $this->db->fetchOne($sql, $params);
return ($result['count'] ?? 0) > 0;
}
Pattern d'implémentation¶
Requête standard (avant)¶
$incidents = $db->fetchAll(
"SELECT * FROM incidents WHERE mairie_id = ?",
[$auth->getMairieId()]
);
Requête multi-villes (après)¶
$mairieIds = $auth->getMairieIdsForUser();
$placeholders = implode(',', array_fill(0, count($mairieIds), '?'));
$incidents = $db->fetchAll(
"SELECT * FROM incidents WHERE mairie_id IN ($placeholders)",
$mairieIds
);
Sécurité
Toujours utiliser prepared statements avec array_fill() pour les clauses IN.
Ne jamais concaténer les IDs directement dans la requête SQL.
Migration base de données¶
Fichier: database/migration_add_collectivite_auth.sql¶
-- Ajouter collectivite_id aux utilisateurs
ALTER TABLE users
ADD COLUMN collectivite_id INT NULL AFTER mairie_id,
ADD KEY idx_collectivite_id (collectivite_id),
ADD CONSTRAINT fk_users_collectivite
FOREIGN KEY (collectivite_id) REFERENCES collectivites(id) ON DELETE SET NULL;
-- Ajouter scope et collectivite_id aux services
ALTER TABLE services_mairie
ADD COLUMN scope ENUM('mairie', 'collectivite') NOT NULL DEFAULT 'mairie',
ADD COLUMN collectivite_id INT NULL AFTER scope,
ADD KEY idx_services_collectivite (collectivite_id),
ADD CONSTRAINT fk_services_collectivite
FOREIGN KEY (collectivite_id) REFERENCES collectivites(id) ON DELETE CASCADE;
-- Ajouter coordonnées GPS aux mairies
ALTER TABLE mairies
ADD COLUMN latitude DECIMAL(10, 8) NULL AFTER ville,
ADD COLUMN longitude DECIMAL(11, 8) NULL AFTER latitude,
ADD INDEX idx_gps (latitude, longitude);
-- Index pour performance requêtes multi-mairies
CREATE INDEX idx_incidents_mairie_type_statut
ON incidents (mairie_id, type_id, statut);
Commande d'exécution:
docker-compose exec -T db mysql -u root -purbafix_root_2024 urbafix \
< database/migration_add_collectivite_auth.sql
Géocodage des communes¶
Script général: populate_mairies_gps.php¶
Étape 1: Calcul coordonnées à partir des incidents existants (AVG) Étape 2: Géocodage via API Nominatim pour communes sans coordonnées
Sortie:
Étape 1: Calcul à partir des incidents...
Mairies avec GPS (depuis incidents): 142
Étape 2: Géocodage des mairies sans GPS...
Mairies à géocoder: 100
✓ Antibes (06600): 43.58084150, 7.12354470
✓ Cannes (06400): 43.55183920, 7.01270630
...
Total mairies avec GPS: 242
Script spécifique: geocode_riviera.php¶
Géocode uniquement les communes d'une collectivité spécifique (ID 18 = CA Riviera Française):
Sortie:
=== GÉOCODAGE COMMUNES CA RIVIERA FRANÇAISE ===
Communes à géocoder: 12
Géocodage de Beausoleil (06240)...
✓ GPS trouvé: 43.74828100, 7.42196770
Géocodage de Breil-sur-Roya (06540)...
✓ GPS trouvé: 43.93753210, 7.51396640
...
=== RÉSULTAT ===
Géocodées: 12
Échecs: 0
=== VÉRIFICATION FINALE ===
✓ Beausoleil: 43.74828100, 7.42196770
✓ Breil-sur-Roya: 43.93753210, 7.51396640
...
Total: 15 / 15 communes avec GPS
Création utilisateur collectivité¶
Via SQL¶
INSERT INTO users (
mairie_id,
collectivite_id,
nom,
prenom,
email,
password,
role,
actif,
voir_tous_incidents
) VALUES (
36825, -- Mairie d'origine (Menton)
18, -- CA Riviera Française
'Test',
'Riviera',
'test.riviera@example.com',
'<hash bcrypt généré avec password_hash()>', -- ne jamais publier un vrai hash
'admin',
1,
1
);
Via interface admin (users.php)¶
Formulaire:
- Nom, prénom, email (champs standards)
- Nouveau: Dropdown "Collectivité (EPCI)"
- Sélection de la collectivité associée à la mairie
Backend INSERT:
$collectiviteId = !empty($_POST['collectivite_id']) ? intval($_POST['collectivite_id']) : null;
$db->execute(
"INSERT INTO users (mairie_id, collectivite_id, password, nom, prenom, email, role)
VALUES (?, ?, ?, ?, ?, ?, ?)",
[$mairieId, $collectiviteId, $hashedPassword, $nom, $prenom, $email, $role]
);
Exemple production: CA Riviera Française¶
Collectivité: - ID: 18 - Code SIREN EPCI: 200030195 - Nom: CA de la Riviera Française - Nombre de communes: 15
Communes membres:
| Commune | Code Postal | Latitude | Longitude |
|---|---|---|---|
| Beausoleil | 06240 | 43.74828100 | 7.42196770 |
| Breil-sur-Roya | 06540 | 43.93753210 | 7.51396640 |
| Castellar | 06500 | 43.80800640 | 7.50035550 |
| Castillon | 06500 | 43.83555190 | 7.44851650 |
| Fontan | 06540 | 43.98216720 | 7.55064280 |
| Gorbio | 06500 | 43.78612740 | 7.44386630 |
| La Brigue | 06430 | 44.06651550 | 7.61732600 |
| La Turbie | 06320 | 43.74553420 | 7.40076800 |
| Menton | 06500 | 43.78151729 | 7.49551457 |
| Moulinet | 06380 | 43.92873170 | 7.40829580 |
| Roquebrune-Cap-Martin | 06190 | 43.76366790 | 7.47553060 |
| Sainte-Agnès | 06500 | 43.79777890 | 7.45939590 |
| Saorge | 06540 | 43.98389870 | 7.55160100 |
| Sospel | 06380 | 43.87848700 | 7.44867090 |
| Tende | 06430 | 44.08906690 | 7.59370710 |
Utilisateur test: - Email: test.riviera@example.com - Mot de passe: défini à la création du compte (non documenté) - Accès: Toutes les 15 communes ci-dessus - URL: https://urbafix.fr/login.php
Tests à effectuer¶
Test 1: Création utilisateur collectivité¶
Test 2: Vérifier accès multi-villes¶
- Login utilisateur collectivité
- Dashboard:
- ✅ Stats agrégées (toutes communes)
- ✅ Carte avec 15 marqueurs violets 🏛️
- ✅ Zoom englobant toutes les communes
- ✅ Carte "Répartition par commune"
- Incidents:
- ✅ Liste affiche incidents de toutes les villes
- ✅ Dropdown filtre commune disponible
- ✅ Badge ville affiché sur chaque incident
- Détail incident:
- ✅ Accès incidents de toutes les villes membres
- ✅ Badge ville affiché dans header
Test 3: Sécurité¶
// Tenter d'accéder à incident hors collectivité
$canAccess = $auth->canAccessIncident(99999);
// Doit retourner false si incident n'appartient pas à la collectivité
- ✅ Queries utilisent prepared statements
- ✅ Filtrage appliqué au niveau SQL (pas juste UI)
- ✅ Pas de fuite de données entre collectivités
Test 4: Services intercommunaux¶
- Créer service
scope='collectivite' - Assigner types d'incidents au service
- Assigner utilisateur d'une autre ville membre
- Vérifier filtrage types fonctionne à travers toutes les villes
Rollback¶
Si besoin de désactiver la fonctionnalité:
-- Retirer collectivite_id de tous les utilisateurs
UPDATE users SET collectivite_id = NULL;
-- Supprimer les contraintes et colonnes
ALTER TABLE users DROP FOREIGN KEY fk_users_collectivite;
ALTER TABLE users DROP COLUMN collectivite_id;
ALTER TABLE services_mairie DROP FOREIGN KEY fk_services_collectivite;
ALTER TABLE services_mairie DROP COLUMN collectivite_id;
ALTER TABLE services_mairie DROP COLUMN scope;
-- Retirer GPS des mairies (optionnel)
ALTER TABLE mairies DROP COLUMN latitude;
ALTER TABLE mairies DROP COLUMN longitude;
Documentation complémentaire¶
- Guide complet:
/srv/urbafix/AUTHENTIFICATION_COLLECTIVITES.md - Import collectivités:
/srv/urbafix/database/README_COLLECTIVITES.md - Guide admin:
/srv/urbafix/GUIDE_ADMIN.md(section Collectivité)
Navigation
Retour à Interface backoffice | Permissions