V 1.003 Accès par MIRADOR sur deux environnements

This commit is contained in:
2026-08-30 15:53:07 +00:00
parent 64cc133b41
commit 00c58fb125
4 changed files with 150 additions and 79 deletions
+74 -37
View File
@@ -1,17 +1,24 @@
<?php <?php
/** /**
* ASPIC — API d'authentification (aspic.php, anciennement login.php — * ASPIC — API d'authentification (aspic.php)
* renommé le 21 août 2026 à la demande de Jean-Marc, purement cosmétique,
* le contenu et le comportement ne changent pas)
* --------------------------------------------------------------------------- * ---------------------------------------------------------------------------
* Vérifie un couple identifiant/mot de passe contre ASP_Users et répond en * ⚠️ Authentification centralisée via MIRADOR (30 août 2026, voir journal de
* JSON. Ne fait aucune gestion de compteur d'essais ni de verrouillage de * développement). L'identifiant n'est plus USR_Code (3 caractères) mais
* compte : ce comportement (2 essais puis sortie, voir journal ASPIC section * MIR_Email. La vérification se fait contre MIR_Users, dans la base MIRADOR
* 8) est géré côté front (identification.php), application strictement * (voir getRouterConnection() dans db.php), qui indique en retour la base de
* interne, pas de mécanisme de bannissement d'IP prévu. * travail propre à cet utilisateur (MIR_ServeurHost / MIR_ServeurPort /
* MIR_BaseNom) ainsi que son code court (MIR_Code, qui reprend le rôle
* jusqu'ici tenu par USR_Code — REA_CodeUser/OPE_CodeUser, jointure avec
* ASP_Users de la base cible pour les préférences/avatar).
*
* ASP_Users.USR_MDP n'est plus consulté ni comparé : le mot de passe est
* désormais vérifié une seule fois, ici, contre MIR_MDPHash (hash bcrypt,
* password_verify()) — la colonne USR_MDP peut être retirée de chaque base
* (ASPIC, MS-DEV, MS-TST...) quand Jean-Marc le souhaite, elle est devenue
* un vestige.
* *
* Requête attendue : POST, corps JSON { "identifiant": "...", "motdepasse": "..." } * Requête attendue : POST, corps JSON { "identifiant": "...", "motdepasse": "..." }
* (identifiant = USR_Code, 3 caractères — pas USR_Nom, voir journal section 2). * (identifiant = adresse mail, MIR_Email).
* *
* Réponses : * Réponses :
* 200 { "success": true, "user": { "USR_Code": "...", "USR_Nom": "..." } } * 200 { "success": true, "user": { "USR_Code": "...", "USR_Nom": "..." } }
@@ -20,13 +27,17 @@
* 405 { "success": false, "error": "Méthode non autorisée" } (autre que POST) * 405 { "success": false, "error": "Méthode non autorisée" } (autre que POST)
* 500 { "success": false, "error": "..." } (connexion base impossible) * 500 { "success": false, "error": "..." } (connexion base impossible)
* *
* ⚠️ Session utilisateur intégrée le 24 août 2026 (voir journal, sections 14 * Session utilisateur (voir journal, sections 14-15, mise à jour le 30 août
* et 15) : en cas d'identification réussie, une session PHP native est * 2026 pour MIRADOR) : en cas d'identification réussie, une session PHP
* ouverte (`session_start()` + `$_SESSION['USR_Code']`), lue ensuite par * native est ouverte avec $_SESSION['USR_Code'] (= MIR_Code) et les
* contacts.php/professions.php/statuts.php/realisations.php pour autoriser * coordonnées de la base de travail cible ($_SESSION['ASPIC_DB_HOST'] /
* leurs appels. `session_start()` doit rester la toute première instruction * ['ASPIC_DB_PORT'] / ['ASPIC_DB_NAME']) — lues ensuite par getDbConnection()
* du fichier, avant tout `header()`/`echo` — sinon PHP refuse d'envoyer les * (db.php) à chaque appel de contacts.php / professions.php / statuts.php /
* cookies de session. * realisations.php / preferences.php, sans qu'aucun de ces fichiers n'ait
* besoin d'être modifié : c'est getDbConnection() seule qui route désormais
* vers la bonne base selon la session en cours. `session_start()` doit
* rester la toute première instruction de ce fichier, avant tout
* `header()`/`echo` — sinon PHP refuse d'envoyer les cookies de session.
*/ */
session_start(); session_start();
@@ -50,28 +61,32 @@ if ($identifiant === '' || $motdepasse === '') {
} }
try { try {
$pdo = getDbConnection(); $pdoRouter = getRouterConnection();
} catch (PDOException $e) { } catch (PDOException $e) {
http_response_code(500); http_response_code(500);
echo json_encode(['success' => false, 'error' => 'Connexion base de données impossible']); echo json_encode(['success' => false, 'error' => 'Connexion à MIRADOR impossible']);
exit; exit;
} }
// USR_Code est un code court (3 caractères) conventionnellement saisi en // Comparaison directe sur l'email : la collation utf8mb4_0900_ai_ci déjà en
// majuscules ; comparaison insensible à la casse sur l'identifiant, comme // place sur MIRADOR (comme sur les autres bases ASPIC) est nativement
// dans le prototype front (voir ASPIC_ecran_identification.html, // insensible à la casse et aux accents, pas besoin d'UPPER() ici.
// trouverUtilisateurDemo). Le mot de passe, lui, reste comparé strictement $stmt = $pdoRouter->prepare(
// (sensible à la casse). 'SELECT MIR_Code, MIR_MDPHash, MIR_ServeurHost, MIR_ServeurPort, MIR_BaseNom
$stmt = $pdo->prepare('SELECT USR_Code, USR_Nom, USR_MDP FROM ASP_Users WHERE UPPER(USR_Code) = UPPER(?)'); FROM MIR_Users WHERE MIR_Email = ? AND MIR_Actif = 1'
);
$stmt->execute([$identifiant]); $stmt->execute([$identifiant]);
$user = $stmt->fetch(); $routage = $stmt->fetch();
// hash_equals() plutôt que === : comparaison en temps constant, pour ne pas // Hash factice comparé même quand l'email est inconnu (password_verify()
// laisser une attaque par mesure de timing déduire des informations sur le // tourne alors quand même) : évite qu'une différence de temps de réponse
// mot de passe stocké. Mot de passe en clair (USR_MDP) assumé consciemment // laisse deviner qu'une adresse n'existe pas dans MIRADOR. Valeur sans
// pour cette application interne (voir journal section 2) — cette précaution // signification, générée une fois pour toutes, jamais utilisée comme vrai
// est un geste peu coûteux, pas une garantie de sécurité forte sur ce point. // mot de passe.
if (!$user || !hash_equals($user['USR_MDP'], $motdepasse)) { const HASH_FACTICE = '$2y$12$wni9cLq0BKttPzU0xAPM9uX/HMzu9UJhlOMj32H6/wLmkjlNfZ1cm';
$hashAVerifier = $routage['MIR_MDPHash'] ?? HASH_FACTICE;
if (!$routage || !password_verify($motdepasse, $hashAVerifier)) {
// Message volontairement générique : ne pas révéler si c'est l'identifiant // Message volontairement générique : ne pas révéler si c'est l'identifiant
// ou le mot de passe qui est en cause. Le compteur d'essais / la sortie au // ou le mot de passe qui est en cause. Le compteur d'essais / la sortie au
// 3e échec sont gérés côté front, pas ici (voir en-tête de ce fichier). // 3e échec sont gérés côté front, pas ici (voir en-tête de ce fichier).
@@ -79,15 +94,37 @@ if (!$user || !hash_equals($user['USR_MDP'], $motdepasse)) {
exit; exit;
} }
// Session ouverte : les endpoints suivants (contacts.php, professions.php, // Coordonnées de la base de travail propre à cet utilisateur : posées en
// statuts.php, realisations.php) exigent désormais $_SESSION['USR_Code'] // session pour que getDbConnection() (db.php) route tous les prochains
// (voir journal, section 15). // appels de CETTE session vers la bonne base, sans qu'il faille les
$_SESSION['USR_Code'] = $user['USR_Code']; // repasser en paramètre à chaque endpoint.
$_SESSION['USR_Code'] = $routage['MIR_Code'];
$_SESSION['ASPIC_DB_HOST'] = $routage['MIR_ServeurHost'];
$_SESSION['ASPIC_DB_PORT'] = (int) $routage['MIR_ServeurPort'];
$_SESSION['ASPIC_DB_NAME'] = $routage['MIR_BaseNom'];
// Nom affiché : toujours porté par ASP_Users de la base cible (MIRADOR ne
// connaît que l'email/code/mot de passe/routage, pas le nom complet). Un
// échec de connexion à cette base cible est remonté explicitement — plutôt
// que de laisser entrer un utilisateur dans une base injoignable, ce qui
// ferait échouer silencieusement tous les écrans suivants.
try {
$pdoCible = getDbConnection();
} catch (PDOException $e) {
session_unset();
http_response_code(500);
echo json_encode(['success' => false, 'error' => 'Connexion à la base de travail impossible']);
exit;
}
$stmtNom = $pdoCible->prepare('SELECT USR_Nom FROM ASP_Users WHERE USR_Code = ?');
$stmtNom->execute([$routage['MIR_Code']]);
$nom = $stmtNom->fetchColumn();
echo json_encode([ echo json_encode([
'success' => true, 'success' => true,
'user' => [ 'user' => [
'USR_Code' => $user['USR_Code'], 'USR_Code' => $routage['MIR_Code'],
'USR_Nom' => $user['USR_Nom'], 'USR_Nom' => $nom !== false ? $nom : null,
], ],
]); ]);
+68 -7
View File
@@ -1,12 +1,39 @@
<?php <?php
function getDbConnection(): PDO /**
{ * ASPIC — db.php
$host = getenv('DB_HOST') ?: 'mysql'; * Connexions PDO de l'application.
$db = getenv('DB_NAME') ?: 'crm'; *
$user = getenv('DB_USER') ?: 'crm_user'; * ⚠️ Mise en place de MIRADOR (30 août 2026, voir journal de développement) :
$pass = getenv('DB_PASS') ?: ''; * deux connexions distinctes désormais.
*
* - getRouterConnection() : connexion fixe vers MIRADOR (base d'authentification
* centralisée, table MIR_Users), sur l'instance de routage dédiée
* (ROUTER_DB_HOST). N'est appelée que par aspic.php, à l'identification.
*
* - getDbConnection() : connexion vers la base de travail réelle. Si la
* session en cours porte des coordonnées de routage (posées par aspic.php
* juste après vérification contre MIRADOR — host/port/base propres à
* l'utilisateur connecté, voir $_SESSION['ASPIC_DB_HOST'] et consorts),
* c'est cette base précise qui est ouverte : deux utilisateurs connectés
* en même temps peuvent donc parfaitement travailler sur deux bases
* différentes (MS-DEV, MS-TST, ou une base de bêta testeur plus tard).
* À défaut de session exploitable (scripts publics sans authentification,
* ex. prospects_core.php — voir journal, section 18), on retombe sur la
* base par défaut du conteneur (DB_HOST/DB_NAME), exactement comme avant
* MIRADOR.
*
* Le compte technique MySQL (DB_USER/DB_PASS) est le même sur toutes les
* instances (mysql/mysql2/mysql3 — droits complets sur toutes les bases que
* chacune héberge, voir journal). Une seule paire d'identifiants suffit donc
* pour ouvrir n'importe laquelle des connexions ci-dessus, MIRADOR compris
* (ROUTER_DB_USER/ROUTER_DB_PASS — valeurs dupliquées à l'identique de
* DB_USER/DB_PASS côté docker-compose, mais des variables distinctes pour
* ne jamais mélanger accidentellement les deux usages).
*/
$dsn = "mysql:host=$host;dbname=$db;charset=utf8mb4"; function ouvrirConnexionPdo(string $host, string $db, string $user, string $pass, int $port = 3306): PDO
{
$dsn = "mysql:host=$host;port=$port;dbname=$db;charset=utf8mb4";
$options = [ $options = [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC, PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
@@ -14,3 +41,37 @@ function getDbConnection(): PDO
return new PDO($dsn, $user, $pass, $options); return new PDO($dsn, $user, $pass, $options);
} }
function getDbConnection(): PDO
{
// Routage dynamique posé par aspic.php à l'identification (MIRADOR) :
// prioritaire dès qu'une session valide le porte.
if (session_status() === PHP_SESSION_ACTIVE && !empty($_SESSION['ASPIC_DB_HOST'])) {
return ouvrirConnexionPdo(
$_SESSION['ASPIC_DB_HOST'],
$_SESSION['ASPIC_DB_NAME'],
getenv('DB_USER') ?: 'crm_user',
getenv('DB_PASS') ?: '',
(int) ($_SESSION['ASPIC_DB_PORT'] ?? 3306)
);
}
// Repli : base par défaut du conteneur, inchangé depuis avant MIRADOR —
// utilisé par les scripts publics sans session (prospection).
$host = getenv('DB_HOST') ?: 'mysql';
$db = getenv('DB_NAME') ?: 'crm';
$user = getenv('DB_USER') ?: 'crm_user';
$pass = getenv('DB_PASS') ?: '';
return ouvrirConnexionPdo($host, $db, $user, $pass);
}
function getRouterConnection(): PDO
{
$host = getenv('ROUTER_DB_HOST') ?: 'mysql2';
$db = getenv('ROUTER_DB_NAME') ?: 'MIRADOR';
$user = getenv('ROUTER_DB_USER') ?: 'crm_user';
$pass = getenv('ROUTER_DB_PASS') ?: '';
return ouvrirConnexionPdo($host, $db, $user, $pass);
}
+7 -34
View File
@@ -383,7 +383,7 @@
<form class="login-panel" id="login-form" novalidate> <form class="login-panel" id="login-form" novalidate>
<div class="panel-header">Utilisateur</div> <div class="panel-header">Utilisateur</div>
<div class="panel-body"> <div class="panel-body">
<input type="text" id="identifiant" name="identifiant" placeholder="Identifiant" autocomplete="off" autofocus required> <input type="email" id="identifiant" name="identifiant" placeholder="Adresse mail" autocomplete="off" autofocus required>
<div class="champ-mdp-wrapper"> <div class="champ-mdp-wrapper">
<input type="password" id="motdepasse" name="motdepasse" placeholder="Mot de passe" autocomplete="off" required> <input type="password" id="motdepasse" name="motdepasse" placeholder="Mot de passe" autocomplete="off" required>
<button type="button" class="toggle-mdp" id="toggle-mdp" title="Afficher le mot de passe" aria-label="Afficher le mot de passe"> <button type="button" class="toggle-mdp" id="toggle-mdp" title="Afficher le mot de passe" aria-label="Afficher le mot de passe">
@@ -405,13 +405,8 @@
/* ========================================================================= /* =========================================================================
ASPIC — Écran "Identification" ASPIC — Écran "Identification"
------------------------------------------------------------------------- -------------------------------------------------------------------------
Appelle aspic.php (voir verifierIdentifiants) pour la vraie vérification Appelle aspic.php (voir verifierIdentifiants) pour la vérification
contre ASP_Users. Filet temporaire (21 août 2026, à retirer avant la contre ASP_Users.
mise en production finale) : si aspic.php n'est pas joignable — fichier
ouvert en local sans serveur PHP à côté — la vérification bascule sur un
compte de démonstration (UTILISATEURS_DEMO, plus bas), pour continuer à
tester le parcours complet (compteur d'essais, redirection vers l'écran
liste des contacts) sans avoir à déployer.
========================================================================= */ ========================================================================= */
const form = document.getElementById('login-form'); const form = document.getElementById('login-form');
const panneauErreur = document.getElementById('panneau-erreur'); const panneauErreur = document.getElementById('panneau-erreur');
@@ -435,32 +430,10 @@
} }
})(); })();
// ---- Utilisateur de démonstration — FILET TEMPORAIRE (21 août 2026) ----
// aspic.php existe désormais (voir journal, section 8) et fait la vraie
// vérification contre ASP_Users. Mais tant qu'il n'est pas déployé quelque
// part joignable (ex. ce fichier ouvert directement en local, sans serveur
// PHP), fetch() échoue — voir verifierIdentifiants() plus bas, qui bascule
// alors automatiquement sur ce compte de démo pour ne pas bloquer les tests
// à l'ouverture simple du fichier. Identifiant de test : JMP / mot de passe :
// aspic2026. À SUPPRIMER explicitement avant la mise en production finale
// (ce serait sinon un mot de passe en dur qui fonctionnerait même une fois
// aspic.php en place) — décision actée avec Jean-Marc le 21 août 2026.
const UTILISATEURS_DEMO = [
{ USR_Code: 'JMP', USR_MDP: 'aspic2026' }
];
function trouverUtilisateurDemo(identifiant, motdepasse) {
return UTILISATEURS_DEMO.find(u =>
u.USR_Code.toUpperCase() === identifiant.toUpperCase() && u.USR_MDP === motdepasse
);
}
// ---- Vérification des identifiants (21 août 2026) ---- // ---- Vérification des identifiants (21 août 2026) ----
// Appel réel à aspic.php. Si le serveur PHP n'est pas joignable (fetch() // Appel réel à aspic.php. Si le serveur PHP n'est pas joignable (fetch()
// rejette : pas de serveur à cette adresse, fichier ouvert en local, etc.) // rejette) ou renvoie une réponse inexploitable, l'identification échoue
// ou renvoie une réponse inexploitable, on bascule sur le compte de // proprement plutôt que de basculer sur un filet de secours.
// démonstration ci-dessus plutôt que de bloquer les tests — voir le
// commentaire de UTILISATEURS_DEMO pour le retrait de ce filet avant prod.
async function verifierIdentifiants(identifiant, motdepasse) { async function verifierIdentifiants(identifiant, motdepasse) {
try { try {
const reponse = await fetch('aspic.php', { const reponse = await fetch('aspic.php', {
@@ -471,8 +444,8 @@
const data = await reponse.json(); const data = await reponse.json();
return !!data.success; return !!data.success;
} catch (erreurReseau) { } catch (erreurReseau) {
console.warn('aspic.php injoignable, bascule sur le compte de démonstration :', erreurReseau); console.error('aspic.php injoignable :', erreurReseau);
return !!trouverUtilisateurDemo(identifiant, motdepasse); return false;
} }
} }
+1 -1
View File
@@ -20,7 +20,7 @@
* d'identification, avant toute connexion. * d'identification, avant toute connexion.
*/ */
const ASPIC_VERSION = 'V1.002'; const ASPIC_VERSION = 'V1.003';
if (basename($_SERVER['SCRIPT_NAME']) === 'version.php') { if (basename($_SERVER['SCRIPT_NAME']) === 'version.php') {
header('Content-Type: application/json; charset=utf-8'); header('Content-Type: application/json; charset=utf-8');