Jelajahi Sumber

feat: Implement 2FA cancellation and validation processes

- Added cms.cancel-2fa-pending.php to handle cancellation of pending 2FA requests.
- Added cms.validate-2fa.php to validate TOTP codes for activating 2FA.
- Updated cms.user.php to include UI elements for managing 2FA, including modals for activation and cancellation.
- Enhanced user profile management with additional checks for 2FA status and improved user feedback.
stany.ferer 1 Minggu lalu
induk
melakukan
fee11aadf3

+ 20 - 18
access.inc.php

@@ -1,20 +1,22 @@
 <?php
-    
-    define("WHITE_ACCESS", array(
-            "user",
-            "unknow",
-            "login",
-            "login-salarie",
-            "login-control",
-            "spash-screen",
-            "maintenance",
-            "authenticator",
-            "logout",
-            "test",
-    ));
 
-    define("OFF_LINE", array(
-        "authenticator",
-        "login",
-        "cron",
-    ));
+define("WHITE_ACCESS", array(
+    "user",
+    "unknow",
+    "login",
+    "login-salarie",
+    "login-control",
+    "spash-screen",
+    "maintenance",
+    "authenticator",
+    "logout",
+    "test",
+    "validate-2fa",
+    "cancel-2fa-pending",
+));
+
+define("OFF_LINE", array(
+    "authenticator",
+    "login",
+    "cron",
+));

+ 1 - 1
core/class/db.class.php

@@ -1,4 +1,5 @@
 <?php
+
 /**
  * Classe `db`
  *
@@ -219,4 +220,3 @@ class db
         return self::$error;
     }
 }
-

+ 226 - 79
core/class/user.class.php

@@ -19,20 +19,20 @@ class user
     {
         // Récupération des données de l'excel au format Json
         db::query("SELECT "
-                . "" . DB_T_USER . ".id AS id, "
-                . "" . DB_T_USER . ".email ,"
-                . "" . DB_T_USER . ".prenom, "
-                . "" . DB_T_USER . ".nom, "
-                . "" . DB_T_USER . ".cree, "
-                . "" . DB_T_USER . ".last_connect, "
-                . "" . DB_T_USER . ".googleAuthenticator, "
-                . "" . DB_T_USER . ".actif, "
-                . "" . DB_T_USER . ".deleted, "
-                . "" . DB_T_USER . ".id_type, "
-                . "" . DB_T_TYPE_USER . ".type "
-                . "FROM " . DB_T_USER . " "
-                . "INNER JOIN " . DB_T_TYPE_USER . " ON " . DB_T_USER . ".id_type = " . DB_T_TYPE_USER . ".id "
-                . "WHERE " . DB_T_USER . ".id = :id");
+            . "" . DB_T_USER . ".id AS id, "
+            . "" . DB_T_USER . ".email ,"
+            . "" . DB_T_USER . ".prenom, "
+            . "" . DB_T_USER . ".nom, "
+            . "" . DB_T_USER . ".cree, "
+            . "" . DB_T_USER . ".last_connect, "
+            . "" . DB_T_USER . ".googleAuthenticator, "
+            . "" . DB_T_USER . ".actif, "
+            . "" . DB_T_USER . ".deleted, "
+            . "" . DB_T_USER . ".id_type, "
+            . "" . DB_T_TYPE_USER . ".type "
+            . "FROM " . DB_T_USER . " "
+            . "INNER JOIN " . DB_T_TYPE_USER . " ON " . DB_T_USER . ".id_type = " . DB_T_TYPE_USER . ".id "
+            . "WHERE " . DB_T_USER . ".id = :id");
         db::bind(':id', $_id);
         $return = db::single();
         $return["tags"] = self::getTags($_id);
@@ -44,22 +44,23 @@ class user
      *
      * @return array Liste des utilisateurs avec leurs informations et tags associés.
      */
-    public static function getUsers() {
+    public static function getUsers()
+    {
         // Récupération des données de l'excel au format Json
         db::query("SELECT "
-                . "" . DB_T_USER . ".id, "
-                . "" . DB_T_USER . ".email, "
-                . "" . DB_T_USER . ".prenom, "
-                . "" . DB_T_USER . ".nom, "
-                . "" . DB_T_USER . ".cree, "
-                . "" . DB_T_USER . ".last_connect, "
-                . "" . DB_T_USER . ".googleAuthenticator, "
-                . "" . DB_T_USER . ".actif, "
-                . "" . DB_T_USER . ".id_type, "
-                . "" . DB_T_TYPE_USER . ".type "
-                . "FROM " . DB_T_USER . " "
-                . "INNER JOIN " . DB_T_TYPE_USER . " ON " . DB_T_USER . ".id_type = " . DB_T_TYPE_USER . ".id "
-                . "WHERE " . DB_T_USER . ".deleted = 0");
+            . "" . DB_T_USER . ".id, "
+            . "" . DB_T_USER . ".email, "
+            . "" . DB_T_USER . ".prenom, "
+            . "" . DB_T_USER . ".nom, "
+            . "" . DB_T_USER . ".cree, "
+            . "" . DB_T_USER . ".last_connect, "
+            . "" . DB_T_USER . ".googleAuthenticator, "
+            . "" . DB_T_USER . ".actif, "
+            . "" . DB_T_USER . ".id_type, "
+            . "" . DB_T_TYPE_USER . ".type "
+            . "FROM " . DB_T_USER . " "
+            . "INNER JOIN " . DB_T_TYPE_USER . " ON " . DB_T_USER . ".id_type = " . DB_T_TYPE_USER . ".id "
+            . "WHERE " . DB_T_USER . ".deleted = 0");
         $return =  db::resultset();
 
         foreach ($return as $key => $users) {
@@ -76,11 +77,12 @@ class user
      * @param int $_id Identifiant de l'utilisateur.
      * @return string Nom complet de l'utilisateur.
      */
-    public static function getNameById(int $_id) {
+    public static function getNameById(int $_id)
+    {
         db::query("SELECT "
-                . "CONCAT (" . DB_T_USER . ".prenom, ' ', " . DB_T_USER . ".nom) AS 'name' "
-                . "FROM " . DB_T_USER . " "
-                . "WHERE " . DB_T_USER . ".id = :id");
+            . "CONCAT (" . DB_T_USER . ".prenom, ' ', " . DB_T_USER . ".nom) AS 'name' "
+            . "FROM " . DB_T_USER . " "
+            . "WHERE " . DB_T_USER . ".id = :id");
         db::bind(':id', $_id);
         return db::single()["name"];
     }
@@ -91,11 +93,12 @@ class user
      * @param int $_id Identifiant de l'utilisateur.
      * @return string Secret Google Authenticator.
      */
-    public static function getMyGoogleAuthenticator(int $_id){
+    public static function getMyGoogleAuthenticator(int $_id)
+    {
         db::query("SELECT "
-                . "" . DB_T_USER . ".googleAuthenticatorSecret "
-                . "FROM " . DB_T_USER . " "
-                . "WHERE " . DB_T_USER . ".id = :id");
+            . "" . DB_T_USER . ".googleAuthenticatorSecret "
+            . "FROM " . DB_T_USER . " "
+            . "WHERE " . DB_T_USER . ".id = :id");
         db::bind(':id', $_id);
         return db::single()["googleAuthenticatorSecret"];
     }
@@ -106,16 +109,31 @@ class user
      * @param string $_email Email de l'utilisateur.
      * @return int 1 si activé, 0 sinon.
      */
-    public static function checkGoogleAuthenticator(string $_email){
+    public static function checkGoogleAuthenticator(string $_email)
+    {
         db::query("SELECT "
-                . "" . DB_T_USER . ".googleAuthenticator "
-                . "FROM " . DB_T_USER . " "
-                . "WHERE " . DB_T_USER . ".email = :email");
+            . "" . DB_T_USER . ".googleAuthenticator "
+            . "FROM " . DB_T_USER . " "
+            . "WHERE " . DB_T_USER . ".email = :email");
         db::bind(':email', $_email);
         $return = db::single();
         return (isset($return["googleAuthenticator"])) ? $return["googleAuthenticator"] : 0;
     }
 
+    /**
+     * Vérifie le statut du 2FA d'un utilisateur par son ID.
+     *
+     * @param int $_id Identifiant de l'utilisateur.
+     * @return int 1 si activé, 0 sinon.
+     */
+    public static function check2FAStatus(int $_id): int
+    {
+        db::query("SELECT googleAuthenticator FROM " . DB_T_USER . " WHERE id = :id");
+        db::bind(':id', $_id);
+        $result = db::single();
+        return isset($result["googleAuthenticator"]) ? (int)$result["googleAuthenticator"] : 0;
+    }
+
     /**
      * Insère un jeton JWT pour un utilisateur.
      *
@@ -123,7 +141,8 @@ class user
      * @param string $_jwt Jeton JWT à insérer.
      * @return bool TRUE si l'insertion est réussie, FALSE sinon.
      */
-    private static function insertJWT($_id_user, $_jwt){
+    private static function insertJWT($_id_user, $_jwt)
+    {
         self::deleteJWTbyUSer($_id_user);
 
         db::query("INSERT INTO " . DB_T_JWT . " (id_user, md5, jwt) VALUES (:id_user, :md5, :jwt)");
@@ -144,7 +163,8 @@ class user
      * @param int $_id_user Identifiant de l'utilisateur.
      * @return bool TRUE si la suppression est réussie, FALSE sinon.
      */
-    public static function deleteJWTbyUSer($_id_user){
+    public static function deleteJWTbyUSer($_id_user)
+    {
         db::query("DELETE FROM " . DB_T_JWT . " WHERE id_user = :id_user");
         db::bind(':id_user', $_id_user);
         try {
@@ -162,7 +182,8 @@ class user
      * @param string $_jwt Nouveau jeton JWT.
      * @return bool TRUE si la mise à jour est réussie, FALSE sinon.
      */
-    public static function updateJWTbyMd5($_md5, $_jwt){
+    public static function updateJWTbyMd5($_md5, $_jwt)
+    {
         db::query("UPDATE " . DB_T_JWT . " SET jwt = :jwt, md5 = :newmd5 WHERE md5 = :md5");
         db::bind(':md5', $_md5);
         db::bind(':jwt', $_jwt);
@@ -181,12 +202,13 @@ class user
      * @param string $_jwt Jeton JWT.
      * @return array|bool Informations de l'utilisateur ou FALSE si non trouvé.
      */
-    public static function getInfosByJWT($_jwt){
+    public static function getInfosByJWT($_jwt)
+    {
         db::query("SELECT id_user, creer FROM " . DB_T_JWT . " WHERE md5 = :md5");
         db::bind(':md5', md5($_jwt));
         $row = db::single();
 
-        if(isset($row["id_user"])){
+        if (isset($row["id_user"])) {
             return $row;
         } else {
             return FALSE;
@@ -246,7 +268,8 @@ class user
      * @param array $_input Données d'entrée pour la connexion.
      * @return bool TRUE si la connexion est réussie, FALSE sinon.
      */
-    public static function connect(array $_input) {
+    public static function connect(array $_input)
+    {
 
         $connect = jwt::authenticate($_input);
 
@@ -294,7 +317,8 @@ class user
      *
      * @param int $_id Identifiant de l'utilisateur.
      */
-    private static function updateLastConnect(int $_id){
+    private static function updateLastConnect(int $_id)
+    {
         db::query("UPDATE " . DB_T_USER . " SET `last_connect` = CURRENT_TIMESTAMP() WHERE id = :id");
         db::bind(':id', $_id);
         db::execute();
@@ -305,10 +329,11 @@ class user
      *
      * @param array $_input Données de l'utilisateur à ajouter.
      */
-    public static function add_user(array $_input){
+    public static function add_user(array $_input)
+    {
         db::query("INSERT INTO " . DB_T_USER . " "
-                . "(email, password, googleAuthenticator, googleAuthenticatorSecret, prenom, nom, id_type, actif) "
-                . "VALUES (:email, :password, :googleAuthenticator, :googleAuthenticatorSecret, :prenom, :nom, :id_type, :actif)");
+            . "(email, password, googleAuthenticator, googleAuthenticatorSecret, prenom, nom, id_type, actif) "
+            . "VALUES (:email, :password, :googleAuthenticator, :googleAuthenticatorSecret, :prenom, :nom, :id_type, :actif)");
         db::bind(':email', $_input["email"]);
         db::bind(':password', md5($_input["password"]));
         db::bind(':prenom', $_input["prenom"]);
@@ -321,7 +346,7 @@ class user
         try {
             db::execute();
 
-            $tags = tags::textToId($_input["tags"], 1); 
+            $tags = tags::textToId($_input["tags"], 1);
             self::addTags(db::lastInsertId(), $tags);
             alert::recSuccess("La création a bien été prise en compte");
         } catch (Exception $ex) {
@@ -336,8 +361,9 @@ class user
      *
      * @return int Identifiant du dernier utilisateur.
      */
-    public static function lastUser(){
-        db::query("SELECT MAX(id) AS id FROM ". DB_T_USER);
+    public static function lastUser()
+    {
+        db::query("SELECT MAX(id) AS id FROM " . DB_T_USER);
         return db::single()["id"];
     }
 
@@ -346,9 +372,10 @@ class user
      *
      * @param array $_input Données mises à jour de l'utilisateur.
      */
-    public static function maj_user(array $_input){
+    public static function maj_user(array $_input)
+    {
 
-        if($_input["password"] != ""){
+        if ($_input["password"] != "") {
             db::query("UPDATE " . DB_T_USER . " SET password = :password WHERE id = :id");
             db::bind(':password', md5($_input["password"]));
             db::bind(':id', $_input["id"]);
@@ -356,12 +383,12 @@ class user
                 db::execute();
             } catch (Exception $ex) {
                 alert::recError("Erreur lors de la modification du mot de passe");
-                header("Location: /user-" . $_input["id"] .".html");
+                header("Location: /user-" . $_input["id"] . ".html");
                 exit();
             }
         }
 
-        if(self::getMyGoogleAuthenticator($_input["id"]) == NULL){
+        if (self::getMyGoogleAuthenticator($_input["id"]) == NULL) {
             db::query("UPDATE " . DB_T_USER . " SET googleAuthenticatorSecret = :googleAuthenticatorSecret WHERE id = :id");
             db::bind(':googleAuthenticatorSecret', googleAuthenticator::createSecret());
             db::bind(':id', $_input["id"]);
@@ -369,14 +396,30 @@ class user
                 db::execute();
             } catch (Exception $ex) {
                 alert::recError("Erreur lors de la création du token de Google Authenticator");
-                header("Location: /user-" . $_input["id"] .".html");
+                header("Location: /user-" . $_input["id"] . ".html");
                 exit();
             }
         }
 
-        $tags = tags::textToId($_input["tags"], 1); 
+        $tags = tags::textToId($_input["tags"], 1);
         self::addTags($_input["id"], $tags);
 
+        // Vérifier si on active le 2FA (passage de 0 à 1)
+        $current2FA = self::check2FAStatus($_input["id"]);
+        $new2FA = isset($_input["googleAuthenticator"]) ? (int)$_input["googleAuthenticator"] : 0;
+
+        // Si on veut activer le 2FA et qu'il n'est pas déjà actif, on le met en pending
+        if ($new2FA == 1 && $current2FA == 0) {
+            self::set2FAPending($_input["id"]);
+            $googleAuthValue = 0; // Reste à 0 jusqu'à validation
+        } else {
+            $googleAuthValue = $new2FA;
+            // Si on désactive le 2FA, on supprime aussi le pending
+            if ($new2FA == 0) {
+                self::cancel2FAPending($_input["id"]);
+            }
+        }
+
         db::query("UPDATE " . DB_T_USER . " SET 
                     email = :email, 
                     prenom = :prenom, 
@@ -388,11 +431,11 @@ class user
         db::bind(':email', $_input["email"]);
         db::bind(':prenom', $_input["prenom"]);
         db::bind(':nom', $_input["nom"]);
-        db::bind(':googleAuthenticator', $_input["googleAuthenticator"]);
+        db::bind(':googleAuthenticator', $googleAuthValue);
         db::bind(':id_type', $_input["id_type"]);
         db::bind(':actif', $_input["actif"]);
         db::bind(':id', $_input["id"]);
-        
+
         try {
             db::execute();
             alert::recSuccess("La modification a bien été prise en compte");
@@ -409,7 +452,8 @@ class user
      * @param float $_idUser Identifiant de l'utilisateur.
      * @return string|null Liste des tags sous forme de chaîne ou NULL si aucun tag.
      */
-    static public function getTags(float $_idUser){
+    static public function getTags(float $_idUser)
+    {
         db::query("SELECT "
             . "" . DB_T_TAGS . ".label "
             . "FROM " . DB_T_USER_TAGS . " "
@@ -419,18 +463,18 @@ class user
         db::bind(':id', $_idUser);
         $tmp = db::resultset();
 
-        if(isset($tmp[0])){
+        if (isset($tmp[0])) {
             $return = NULL;
             foreach ($tmp as $value) {
-                $return .= $value["label"].",";
+                $return .= $value["label"] . ",";
             }
-    
+
             $return = substr($return, 0, -1);
             return $return;
         } else {
             return NULL;
         }
-    }  
+    }
 
     /**
      * Récupère les identifiants des tags associés à un utilisateur.
@@ -438,7 +482,8 @@ class user
      * @param float $_idUser Identifiant de l'utilisateur.
      * @return array|null Liste des identifiants des tags ou NULL si aucun tag.
      */
-    static public function getIdTags(float $_idUser){
+    static public function getIdTags(float $_idUser)
+    {
         db::query("SELECT "
             . "" . DB_T_USER_TAGS . ".id_tags "
             . "FROM " . DB_T_USER_TAGS . " "
@@ -446,7 +491,7 @@ class user
             . "ORDER BY " . DB_T_USER_TAGS . ".creer");
         db::bind(':id', $_idUser);
         $tmp = db::resultset();
-        if(isset($tmp[0])){
+        if (isset($tmp[0])) {
             $return = [];
             foreach ($tmp as $value) {
                 $return[] = $value["id_tags"];
@@ -470,12 +515,12 @@ class user
         db::bind(':id_user', $_idUser);
         db::execute();
 
-        if($_tags != NULL){ 
+        if ($_tags != NULL) {
             $tags = explode(",", $_tags);
 
             $sqlMaj = "";
             foreach ($tags as $tag) {
-                $sqlMaj .= " (:id_user, ".$tag."),";
+                $sqlMaj .= " (:id_user, " . $tag . "),";
             }
 
             $sqlMaj = substr($sqlMaj, 0, -1);
@@ -490,20 +535,21 @@ class user
             }
         }
     }
-    
+
     /**
      * Marque un utilisateur comme supprimé.
      *
      * @param int $_id Identifiant de l'utilisateur.
      */
-    public static function deleteUser(int $_id){
+    public static function deleteUser(int $_id)
+    {
         db::query("UPDATE " . DB_T_USER . " SET deleted = 1 WHERE id = :id");
         db::bind(':id', $_id);
         try {
             db::execute();
         } catch (Exception $ex) {
             alert::recError("Erreur lors de la suppression");
-            header("Location: /user-" . $_id .".html");
+            header("Location: /user-" . $_id . ".html");
             exit();
         }
     }
@@ -513,14 +559,15 @@ class user
      *
      * @param int $_id Identifiant de l'utilisateur.
      */
-    public static function restoreUser(int $_id){
+    public static function restoreUser(int $_id)
+    {
         db::query("UPDATE " . DB_T_USER . " SET deleted = 0 WHERE id = :id");
         db::bind(':id', $_id);
         try {
             db::execute();
         } catch (Exception $ex) {
             alert::recError("Erreur lors de la restauration");
-            header("Location: /user-" . $_id .".html");
+            header("Location: /user-" . $_id . ".html");
             exit();
         }
     }
@@ -530,7 +577,8 @@ class user
      *
      * @return bool TRUE si la double authentification est activée, FALSE sinon.
      */
-    static public function checkSecur(){
+    static public function checkSecur()
+    {
         db::query("SELECT googleAuthenticator FROM " . DB_T_USER . " WHERE id = :id");
         db::bind(':id', session::getId());
         return db::single()["googleAuthenticator"] == 1 ? TRUE : FALSE;
@@ -539,10 +587,11 @@ class user
     /**
      * Affiche un message de sécurité si la double authentification n'est pas activée.
      */
-    static public function printIsSecur(){
-        if(ALERT_AUTHENTICATOR == TRUE){
+    static public function printIsSecur()
+    {
+        if (ALERT_AUTHENTICATOR == TRUE) {
             $_SESSION["CALLOUT"] ??= 0;
-            if(self::checkSecur() == FALSE AND $_SESSION["CALLOUT"] < NB_ALERT_AUTHENTICATOR){
+            if (self::checkSecur() == FALSE and $_SESSION["CALLOUT"] < NB_ALERT_AUTHENTICATOR) {
                 $callout = [
                     "type" => "danger",
                     "size" => "tiny",
@@ -554,4 +603,102 @@ class user
         }
     }
 
+    /**
+     * Vérifie si le 2FA est en attente de validation pour un utilisateur.
+     *
+     * @param int $_id Identifiant de l'utilisateur.
+     * @return bool TRUE si le 2FA est en attente de validation, FALSE sinon.
+     */
+    public static function is2FAPending(int $_id): bool
+    {
+        try {
+            db::query("SELECT googleAuthenticatorPending FROM " . DB_T_USER . " WHERE id = :id");
+            db::bind(':id', $_id);
+            $result = db::single();
+            return isset($result["googleAuthenticatorPending"]) && $result["googleAuthenticatorPending"] == 1;
+        } catch (Exception $ex) {
+            // La colonne n'existe pas encore, retourner false
+            return false;
+        }
+    }
+
+    /**
+     * Définit le 2FA en mode pending (en attente de validation du premier code).
+     *
+     * @param int $_id Identifiant de l'utilisateur.
+     * @return bool TRUE si la mise à jour est réussie, FALSE sinon.
+     */
+    public static function set2FAPending(int $_id): bool
+    {
+        db::query("UPDATE " . DB_T_USER . " SET googleAuthenticatorPending = 1, googleAuthenticator = 0 WHERE id = :id");
+        db::bind(':id', $_id);
+        try {
+            db::execute();
+            return true;
+        } catch (Exception $ex) {
+            return false;
+        }
+    }
+
+    /**
+     * Valide le code TOTP et active définitivement le 2FA.
+     *
+     * @param int $_id Identifiant de l'utilisateur.
+     * @param string $_code Code TOTP à vérifier.
+     * @return array Résultat de la validation avec status et message.
+     */
+    public static function validate2FAActivation(int $_id, string $_code): array
+    {
+        // Récupérer le secret de l'utilisateur
+        $secret = self::getMyGoogleAuthenticator($_id);
+
+        if (empty($secret)) {
+            return [
+                "status" => "error",
+                "message" => "Aucun secret Google Authenticator configuré."
+            ];
+        }
+
+        // Vérifier le code TOTP
+        if (googleAuthenticator::verifyCode($secret, $_code, 1)) {
+            // Code valide, activer le 2FA et supprimer le pending
+            db::query("UPDATE " . DB_T_USER . " SET googleAuthenticator = 1, googleAuthenticatorPending = 0 WHERE id = :id");
+            db::bind(':id', $_id);
+            try {
+                db::execute();
+                return [
+                    "status" => "success",
+                    "message" => "La double authentification a été activée avec succès."
+                ];
+            } catch (Exception $ex) {
+                return [
+                    "status" => "error",
+                    "message" => "Erreur lors de l'activation de la double authentification."
+                ];
+            }
+        } else {
+            return [
+                "status" => "error",
+                "message" => "Code invalide. Veuillez réessayer."
+            ];
+        }
+    }
+
+    /**
+     * Annule le mode pending du 2FA.
+     *
+     * @param int $_id Identifiant de l'utilisateur.
+     * @return bool TRUE si l'annulation est réussie, FALSE sinon.
+     */
+    public static function cancel2FAPending(int $_id): bool
+    {
+        db::query("UPDATE " . DB_T_USER . " SET googleAuthenticatorPending = 0 WHERE id = :id");
+        db::bind(':id', $_id);
+        try {
+            db::execute();
+            return true;
+        } catch (Exception $ex) {
+            return false;
+        }
+    }
 }

+ 45 - 0
core/submit/cms.cancel-2fa-pending.php

@@ -0,0 +1,45 @@
+<?php
+
+/**
+ * Annulation du mode pending pour la double authentification
+ */
+
+if (core::ifPost("from") and core::getPost("from") == "cancel-2fa-pending") {
+
+    $userId = (int) core::getPost("id");
+
+    // Vérifier que l'utilisateur annule son propre 2FA pending
+    if ($userId !== session::getId()) {
+        alert::recError("Vous ne pouvez annuler que votre propre demande d'activation.");
+        header("Location: /user.html");
+        exit();
+    }
+
+    // Vérifier que le 2FA est bien en attente de validation
+    if (!user::is2FAPending($userId)) {
+        alert::recError("Aucune demande d'activation en attente.");
+        header("Location: /user.html");
+        exit();
+    }
+
+    // Annuler le pending
+    if (user::cancel2FAPending($userId)) {
+        historique::recRef("/user.html");
+        historique::add(array(
+            "idType" => historique::getIdRef("ACTION"),
+            "idUser" => session::getId(),
+            "idPage" => historique::getIdRef("/user.html"),
+            "log" => "Annulation de l'activation de la double authentification"
+        ));
+
+        alert::recSuccess("L'activation de la double authentification a été annulée.");
+    } else {
+        alert::recError("Erreur lors de l'annulation.");
+    }
+
+    header("Location: /user.html");
+    exit();
+} else {
+    header('HTTP/1.0 401 Unauthorized');
+    exit();
+}

+ 51 - 0
core/submit/cms.validate-2fa.php

@@ -0,0 +1,51 @@
+<?php
+
+/**
+ * Validation du code TOTP pour activer la double authentification
+ */
+
+if (core::ifPost("from") and core::getPost("from") == "validate-2fa") {
+
+    $userId = (int) core::getPost("id");
+    $totpCode = core::getPost("totp_code");
+
+    // Vérifier que l'utilisateur valide son propre 2FA
+    if ($userId !== session::getId()) {
+        alert::recError("Vous ne pouvez valider que votre propre double authentification.");
+        header("Location: /user.html");
+        exit();
+    }
+
+    // Vérifier que le 2FA est bien en attente de validation
+    if (!user::is2FAPending($userId)) {
+        alert::recError("La double authentification n'est pas en attente de validation.");
+        header("Location: /user.html");
+        exit();
+    }
+
+    // Valider le code TOTP
+    $result = user::validate2FAActivation($userId, $totpCode);
+
+    if ($result["status"] === "success") {
+        // Mettre à jour la session
+        $_SESSION["user"]["googleAuthenticator"] = 1;
+
+        historique::recRef("/user.html");
+        historique::add(array(
+            "idType" => historique::getIdRef("ACTION"),
+            "idUser" => session::getId(),
+            "idPage" => historique::getIdRef("/user.html"),
+            "log" => "Activation de la double authentification (Google Authenticator)"
+        ));
+
+        alert::recSuccess($result["message"]);
+    } else {
+        alert::recError($result["message"]);
+    }
+
+    header("Location: /user.html");
+    exit();
+} else {
+    header('HTTP/1.0 401 Unauthorized');
+    exit();
+}

+ 237 - 140
core/views/pages/cms.user.php

@@ -1,12 +1,12 @@
 <?php
-if(core::ifGet("add") AND access::ifAccesss("add-user")) {
+if (core::ifGet("add") and access::ifAccesss("add-user")) {
     $id_form = '<input type="hidden" name="id" value="add">';
     $submit = "Ajouter un profil";
     $titre = "Ajouter un profil";
     $protect = 0;
 } else {
-    
-    if( core::ifGet("id") == FALSE OR  (core::ifGet("id") AND session::getId() == core::getGet("id"))){
+
+    if (core::ifGet("id") == FALSE or  (core::ifGet("id") and session::getId() == core::getGet("id"))) {
         $user = user::getUser(session::getId());
         $submit = "Modifier votre profil";
         $titre = "Votre fiche de profil";
@@ -15,26 +15,28 @@ if(core::ifGet("add") AND access::ifAccesss("add-user")) {
             debug::log($user, "Données brutes user");
         }
     } else {
-        if(access::ifAccesss("add-user")){
+        if (access::ifAccesss("add-user")) {
             $user = user::getUser(core::getGet("id"));
-            if(is_array($user)){
+            if (is_array($user)) {
                 $submit = "Modifier ce profil";
                 $titre = "Fiche de " . $user["prenom"] . " " . $user["nom"];
-                if($user["deleted"] == 1){
+                if ($user["deleted"] == 1) {
                     $titre .= " (Supprimée)";
                     $protect = 2;
                 } else {
                     $protect = 0;
                 }
             } else {
-                get::page("unknow"); exit();
+                get::page("unknow");
+                exit();
             }
         } else {
-            get::page("unknow"); exit();
+            get::page("unknow");
+            exit();
         }
-    } 
+    }
 
-    $id_form = '<input type="hidden" name="id" value="' . $user["id"] . '">'; 
+    $id_form = '<input type="hidden" name="id" value="' . $user["id"] . '">';
 }
 
 ?>
@@ -43,7 +45,7 @@ if(core::ifGet("add") AND access::ifAccesss("add-user")) {
     <div class="col-11">
         <h2 class="bd-title" id="content">
             <span><?= $titre ?></span>
-        </h2>        
+        </h2>
         <?php if (isset($user["id"]) && session::getId() != $user["id"] && $protect != 2): ?>
             <div class="fix-container-button-nav">
                 <a href="/submit.php?from=parametres-user-delete&id=<?= $user['id']; ?>" onclick="return confirm('Voulez-vous supprimer le compte de <?= $user["prenom"] . " " . $user["nom"]; ?> ?')">
@@ -59,200 +61,295 @@ if(core::ifGet("add") AND access::ifAccesss("add-user")) {
         <?php endif; ?>
     </div>
 </header>
-<?php   
-    if(core::getGet("id")){
-        echo core::filAriane(array(
-            "current" => $titre, 
-            "arbo" => array( 
-                "Administration" => NULL,
-                "Utilisateurs" => "/parametres-users.html",
-                $titre => "#")
-        ));
-    }
-        ?>
-        <?php if (isset($user["last_connect"])): ?>
-        <?php callout::print([
-            "type" => "info",
-            "size" => "tiny",
-            "style" => "margin:-5px 0;",
-            "p" => "Dernière connexion le " . core::convertDate($user["last_connect"]),
-        ]); ?>
-    <?php endif; ?>
+<?php
+if (core::getGet("id")) {
+    echo core::filAriane(array(
+        "current" => $titre,
+        "arbo" => array(
+            "Administration" => NULL,
+            "Utilisateurs" => "/parametres-users.html",
+            $titre => "#"
+        )
+    ));
+}
+?>
+<?php if (isset($user["last_connect"])): ?>
+    <?php callout::print([
+        "type" => "info",
+        "size" => "tiny",
+        "style" => "margin:-5px 0;",
+        "p" => "Dernière connexion le " . core::convertDate($user["last_connect"]),
+    ]); ?>
+<?php endif; ?>
 <br />
-<?php if($protect != 2): ?>
-    
-<form id="form-user" method="post" action="/submit.php" oninput='password2.setCustomValidity(password2.value != password.value ? "Les mots de passe ne sont pas identiques" : "")' onsubmit="return(false);">
+<?php if ($protect != 2): ?>
+
+    <form id="form-user" method="post" action="/submit.php" oninput='password2.setCustomValidity(password2.value != password.value ? "Les mots de passe ne sont pas identiques" : "")' onsubmit="return(false);">
+
+        <input type="hidden" name="from" value="user">
 
-    <input type="hidden" name="from" value="user">
-    
     <?php
-        echo $id_form;
-        endif;
+    echo $id_form;
+endif;
     ?>
-    
+
     <div class="form-group">
         <label>Type de compte</label>
         <?php
-            $id_type = [
-                2 => "Contrôleur QRCode (émargement)",
-                3 => "Assistance sociale",
-                4 => "Modérateur du CMS",
-                5 => "Membre du Bureau du CSE",
-                6 => "Elu du CSE",
-                7 => "Comptable",
-                1 => "Administrateur"
-            ];
+        $id_type = [
+            2 => "Contrôleur QRCode (émargement)",
+            3 => "Assistance sociale",
+            4 => "Modérateur du CMS",
+            5 => "Membre du Bureau du CSE",
+            6 => "Elu du CSE",
+            7 => "Comptable",
+            1 => "Administrateur"
+        ];
         ?>
-        <?php if($protect == 0): ?>
+        <?php if ($protect == 0): ?>
             <?php
-                html::printSelect('name="id_type" class="form-select"', $id_type, @$user["id_type"]);
+            html::printSelect('name="id_type" class="form-select"', $id_type, @$user["id_type"]);
             ?>
         <?php endif; ?>
-        <?php if($protect == 1 OR $protect == 2): ?>
-            <?php 
-                html::printSelect('class="form-control" disabled', $id_type, @$user["id_type"]);
-                html::printInput('type="hidden" name="id_type"', $user["id_type"]);
+        <?php if ($protect == 1 or $protect == 2): ?>
+            <?php
+            html::printSelect('class="form-control" disabled', $id_type, @$user["id_type"]);
+            html::printInput('type="hidden" name="id_type"', $user["id_type"]);
             ?>
         <?php endif; ?>
     </div>
     <br />
 
-    <?php 
-        if(access::ifAccesss("add-user") AND core::ifGet("id") AND (isset($user["deleted"]) AND $user["deleted"] == 0)) { ?>
-            <div class="form-group">
-                <label>Rôles aditionnels</label>
-                <?= html::printInput('type="text" name="tags" id="tags"', @$user["tags"]) ?>
-            </div>
-            <br />
-    <?php 
-        } elseif(isset($user["tags"])) { ?>
+    <?php
+    if (access::ifAccesss("add-user") and core::ifGet("id") and (isset($user["deleted"]) and $user["deleted"] == 0)) { ?>
+        <div class="form-group">
+            <label>Rôles aditionnels</label>
+            <?= html::printInput('type="text" name="tags" id="tags"', @$user["tags"]) ?>
+        </div>
+        <br />
+    <?php
+    } elseif (isset($user["tags"])) { ?>
         <div class="form-group">
             <label>Rôles aditionnels</label>
             <?= html::printInput('type="text" class="form-control" name="tags" readonly="readonly"', @$user["tags"]) ?>
         </div>
         <br />
-    <?php 
-        } else {
-            html::printInput('type="hidden" name="tags"');
-        }
+    <?php
+    } else {
+        html::printInput('type="hidden" name="tags"');
+    }
     ?>
 
     <div class="form-group">
         <label>Prénom</label>
         <?php
-            $confPrenom = 'type="text" name="prenom" class="form-control" ';
-            $confPrenom .= $protect == 2 ? 'readonly="readonly"' : 'required';
-            html::printInput($confPrenom, @$user["prenom"]);
+        $confPrenom = 'type="text" name="prenom" class="form-control" ';
+        $confPrenom .= $protect == 2 ? 'readonly="readonly"' : 'required';
+        html::printInput($confPrenom, @$user["prenom"]);
         ?>
     </div>
     <br />
-    
+
     <div class="form-group">
         <label>nom</label>
         <?php
-            $confNom = 'type="text" name="nom" class="form-control" ';
-            $confNom .= $protect == 2 ? 'readonly="readonly"' : 'required';
-            html::printInput($confNom, @$user["nom"]);
+        $confNom = 'type="text" name="nom" class="form-control" ';
+        $confNom .= $protect == 2 ? 'readonly="readonly"' : 'required';
+        html::printInput($confNom, @$user["nom"]);
         ?>
     </div>
     <br />
-    
+
     <div class="form-group">
         <label>Email</label>
         <?php
-            $confEmail = 'type="text" name="email" class="form-control" ';
-            $confEmail .= $protect == 2 ? 'readonly="readonly"' : 'required';
-            html::printInput($confEmail, @$user["email"]);
+        $confEmail = 'type="text" name="email" class="form-control" ';
+        $confEmail .= $protect == 2 ? 'readonly="readonly"' : 'required';
+        html::printInput($confEmail, @$user["email"]);
         ?>
     </div>
     <br />
-    
-    <?php if($protect != 2): ?>
 
-    <div class="form-group">
-        <label>Mot de passe</label>
-        <?php
+    <?php if ($protect != 2): ?>
+
+        <div class="form-group">
+            <label>Mot de passe</label>
+            <?php
             $confPassword = 'type="password" class="form-control" minlength="8" maxlength="25" name="password" ';
             $confPassword .= core::ifGet("add") ? 'required"' : NULL;
             html::printInput($confPassword);
-        ?>
-    </div>
-    <br />
-    
-    <div class="form-group">
-        <label>Confirmation du mot de passe</label>
-        <?php
+            ?>
+        </div>
+        <br />
+
+        <div class="form-group">
+            <label>Confirmation du mot de passe</label>
+            <?php
             $confPassword2 = 'type="password" class="form-control" minlength="8" maxlength="25" name="password2" ';
             $confPassword2 .= core::ifGet("add") ? 'required"' : NULL;
             html::printInput($confPassword2);
-        ?>
-    </div>
-    <br />
+            ?>
+        </div>
+        <br />
 
-    <div class="form-group">
-        <label>Google Authenticator</label>
-        <?php
+        <div class="form-group">
+            <label>Google Authenticator</label>
+            <?php
             $googleAuthenticator = [
-                    0 => "Désactivé",
-                    1 => "Activé",
-                ];
-            html::printSelect('name="googleAuthenticator" class="form-select"', $googleAuthenticator, @$user["googleAuthenticator"]);
-        ?>
-    </div>
-    <br />
-
-    <?php endif; ?>
+                0 => "Désactivé",
+                1 => "Activé",
+            ];
+            // Vérifier si le 2FA est en attente de validation
+            $is2FAPending = isset($user["id"]) ? user::is2FAPending($user["id"]) : false;
+            $is2FAActive = isset($user["googleAuthenticator"]) && $user["googleAuthenticator"] == 1;
 
-    <?php if(isset($user["googleAuthenticator"]) AND $user["googleAuthenticator"] == 1 AND $protect == 1): ?>
-        <div class="card text-center" style="width: 18rem;">
-                <?php
-                    $qrCodeUrl = googleAuthenticator::getGoogleUrl("CMS CSE Invent: " . ENVIRONNEMENT,  user::getMyGoogleAuthenticator(session::getId()));
-                    myQrcode::printQRCode($qrCodeUrl);
+            if ($is2FAPending) {
+                // Afficher "En attente de validation" quand le 2FA est pending
+                echo '<div class="alert alert-warning"><i class="bi bi-hourglass-split"></i> En attente de validation du code</div>';
+                html::printInput('type="hidden" name="googleAuthenticator" value="0"');
+            } else {
+                $bgStyle = $is2FAActive ? 'background-color: #d4edda; color: #155724;' : 'background-color: #f8d7da; color: #721c24;';
+                $iconClass = $is2FAActive ? 'bi-shield-check' : 'bi-shield-x';
                 ?>
-            <div class="card-footer text-body-secondary">
-                QRCode à scanner dans votre application Google Authenticator<br />
-                <a href="https://play.google.com/store/apps/details?id=com.google.android.apps.authenticator2" target="_blank"><?= icon::getFont(["type" => "bi bi-android2", "size" => "40px"]) ?></a>&nbsp;&nbsp;&nbsp;<a href="https://apps.apple.com/fr/app/google-authenticator/id388497605" target="_blank"><?= icon::getFont(["type" => "bi bi-apple", "size" => "40px"]) ?></a>
-            </div>
-        </div>  
+                <div class="input-group">
+                    <span class="input-group-text" style="<?= $bgStyle ?>">
+                        <i class="bi <?= $iconClass ?>"></i>
+                    </span>
+                    <?php html::printSelect('name="googleAuthenticator" class="form-select" style="' . $bgStyle . '"', $googleAuthenticator, @$user["googleAuthenticator"]); ?>
+                </div>
+                <?php
+            }
+            ?>
+        </div>
         <br />
+
     <?php endif; ?>
 
-    <?php if($protect == 0): ?>
-    <div class="form-group">
-        <label>Etat du compte</label>
-        <?php
+    <?php if ($protect == 0): ?>
+        <div class="form-group">
+            <label>Etat du compte</label>
+            <?php
             $actif = [
-                    0 => "Compte désactivé",
-                    1 => "Compte activé",
-                ];
+                0 => "Compte désactivé",
+                1 => "Compte activé",
+            ];
             html::printSelect('name="actif" class="form-select"', $actif, @$user["actif"]);
-        ?>
-    </div>
-    <br />
+            ?>
+        </div>
+        <br />
     <?php endif; ?>
 
-    <?php if($protect == 1): ?>
+    <?php if ($protect == 1): ?>
         <?= html::printInput('type="hidden" name="actif"', $user["actif"]); ?>
     <?php endif; ?>
 
-    <?php if((isset($user["deleted"]) AND $user["deleted"] == 0) OR core::ifGet("add")) : ?>
-    <input class="btn btn-primary btn-lg" style="width: 100%; margin-bottom:20px;" type="button" value="<?php echo $submit ?>"  onclick="validateAndSubmit()">
+    <?php if ((isset($user["deleted"]) and $user["deleted"] == 0) or core::ifGet("add")) : ?>
+        <input class="btn btn-primary btn-lg" style="width: 100%; margin-bottom:20px;" type="button" value="<?php echo $submit ?>" onclick="validateAndSubmit()">
     <?php endif; ?>
 
-</form>
+    </form>
+
+    <?php
+    // Modal pour activer le 2FA si en attente de validation (pending)
+    if (isset($user["id"]) and user::is2FAPending($user["id"]) and $protect == 1): ?>
+        <style>
+            .qrcode-container img {
+                max-width: 100%;
+                height: auto;
+                display: block;
+                margin: 0 auto;
+            }
 
-<script>
-    $(document).ready(function () {
-        $('#tags').inputTags({
-            autocomplete: {
-                values: <?php echo tags::getJquery(1) ?>,
-                only: true
-            },
-            max: 3
+            #modal2FA .modal-content {
+                border-radius: 15px;
+            }
+
+            #modal2FA .modal-header {
+                border-radius: 15px 15px 0 0;
+            }
+        </style>
+
+        <!-- Modal 2FA -->
+        <div class="modal fade" id="modal2FA" data-bs-backdrop="static" data-bs-keyboard="false" tabindex="-1" aria-labelledby="modal2FALabel" aria-hidden="true">
+            <div class="modal-dialog modal-dialog-centered">
+                <div class="modal-content">
+                    <div class="modal-header bg-warning text-dark">
+                        <h5 class="modal-title" id="modal2FALabel">
+                            <i class="bi bi-shield-lock"></i> Activation de la double authentification
+                        </h5>
+                    </div>
+                    <div class="modal-body text-center">
+                        <div class="qrcode-container" style="max-width: 250px; margin: 0 auto;">
+                            <?php
+                            $qrCodeUrl = googleAuthenticator::getGoogleUrl("CMS CSE Invent: " . ENVIRONNEMENT,  user::getMyGoogleAuthenticator(session::getId()));
+                            myQrcode::printQRCode($qrCodeUrl);
+                            ?>
+                        </div>
+                        <p class="mt-3 mb-2">
+                            <small>1. Scannez ce QR code avec votre application Google Authenticator</small><br>
+                            <small>2. Entrez le code à 6 chiffres généré par l'application</small>
+                        </p>
+                        <form method="post" action="/submit.php" class="mt-3" id="form-validate-2fa">
+                            <input type="hidden" name="from" value="validate-2fa">
+                            <input type="hidden" name="id" value="<?= $user["id"] ?>">
+                            <div class="input-group mb-3" style="max-width: 280px; margin: 0 auto;">
+                                <input type="text" class="form-control text-center" name="totp_code" id="totp_code"
+                                    maxlength="6" pattern="[0-9]{6}" required
+                                    placeholder="000000" autocomplete="off"
+                                    style="font-size: 1.5rem; letter-spacing: 0.5rem; font-weight: bold;">
+                                <button class="btn btn-success" type="submit">
+                                    <i class="bi bi-check-lg"></i>
+                                </button>
+                            </div>
+                        </form>
+                        <div class="mt-2 mb-2">
+                            <a href="https://play.google.com/store/apps/details?id=com.google.android.apps.authenticator2" target="_blank" class="text-muted me-2">
+                                <?= icon::getFont(["type" => "bi bi-android2", "size" => "24px"]) ?>
+                            </a>
+                            <a href="https://apps.apple.com/fr/app/google-authenticator/id388497605" target="_blank" class="text-muted">
+                                <?= icon::getFont(["type" => "bi bi-apple", "size" => "24px"]) ?>
+                            </a>
+                        </div>
+                    </div>
+                    <div class="modal-footer justify-content-center border-0">
+                        <form method="post" action="/submit.php">
+                            <input type="hidden" name="from" value="cancel-2fa-pending">
+                            <input type="hidden" name="id" value="<?= $user["id"] ?>">
+                            <button class="btn btn-outline-secondary btn-sm" type="submit">
+                                <i class="bi bi-x-lg"></i> Annuler l'activation
+                            </button>
+                        </form>
+                    </div>
+                </div>
+            </div>
+        </div>
+
+        <script>
+            $(document).ready(function() {
+                // Ouvrir automatiquement la modal
+                var modal2FA = new bootstrap.Modal(document.getElementById('modal2FA'));
+                modal2FA.show();
+
+                // Focus sur le champ de code
+                $('#modal2FA').on('shown.bs.modal', function() {
+                    $('#totp_code').focus();
+                });
+            });
+        </script>
+    <?php endif; ?>
+
+    <script>
+        $(document).ready(function() {
+            $('#tags').inputTags({
+                autocomplete: {
+                    values: <?php echo tags::getJquery(1) ?>,
+                    only: true
+                },
+                max: 3
+            });
         });
-    });
-</script>
+    </script>
 
-<?php
+    <?php
     get::javascript("user");
-?>
+    ?>