diff --git a/core/module/config/config.php b/core/module/config/config.php index 5563db6..48e3cad 100644 --- a/core/module/config/config.php +++ b/core/module/config/config.php @@ -529,7 +529,8 @@ class config extends common 'autoDisconnect' => $this->getInput('connectAutoDisconnect', helper::FILTER_BOOLEAN), 'captchaType' => $this->getInput('connectCaptchaType'), 'showPassword' => $this->getInput('connectShowPassword', helper::FILTER_BOOLEAN), - 'redirectLogin' => $this->getInput('connectRedirectLogin', helper::FILTER_BOOLEAN) + 'redirectLogin' => $this->getInput('connectRedirectLogin', helper::FILTER_BOOLEAN), + 'mailAuth' => $this->getInput('connectAuthMail', helper::FILTER_BOOLEAN), ] ] ]); diff --git a/core/module/config/view/connect/connect.php b/core/module/config/view/connect/connect.php index 2f6ee63..74c08e1 100644 --- a/core/module/config/view/connect/connect.php +++ b/core/module/config/view/connect/connect.php @@ -3,13 +3,7 @@

-

-
-
+
'Limitation des tentatives', 'selected' => $this->getData(['config', 'connect', 'attempt']) ]); ?>
-
+
'Blocage après échecs', 'selected' => $this->getData(['config', 'connect', 'timeout']) ]); ?>
-
- - helper::baseUrl() . 'config/blacklistDownload', - 'value' => 'Télécharger la liste', - 'ico' => 'download' - ]); ?> -
-
- 'buttonRed', - 'href' => helper::baseUrl() . 'config/blacklistReset', - 'value' => 'Réinitialiser la liste', - 'ico' => 'trash' - ]); ?> +
+ 'Aucune'], self::$groupNews), [ + 'label' => 'Validation par messagerie', + 'selected' => $this->getData(['config', 'connect', 'mailAuth']), + 'help' => 'La connexion est confirmée par une clé adressée par messagerie. Depuis le groupe sélectionnée et les groupes supérieurs.' ]); ?>
+
+
+
+
+
+

+

- $this->getData(['config', 'connect', 'captcha']) ]); ?>
@@ -92,40 +77,66 @@

-

-
- $this->getData(['config', 'connect', 'log']) - ]); ?> +
+
+
+ $this->getData(['config', 'connect', 'log']) + ]); ?> +
+
+ 'Anonymat des adresses IP', + 'selected' => $this->getData(['config', 'connect', 'anonymousIp']), + 'help' => 'La règlementation française impose un anonymat de niveau 2' + ]); ?> +
+
+
+
+ helper::baseUrl() . 'config/logDownload', + 'value' => 'Télécharger le journal', + 'ico' => 'download' + ]); ?> +
+
+ 'buttonRed', + 'href' => helper::baseUrl() . 'config/logReset', + 'value' => 'Réinitialiser le journal', + 'ico' => 'trash' + ]); ?> +
+
-
- 'Anonymat des adresses IP', - 'selected' => $this->getData(['config', 'connect', 'anonymousIp']), - 'help' => 'La règlementation française impose un anonymat de niveau 2' - ]); ?> -
-
- helper::baseUrl() . 'config/logDownload', - 'value' => 'Télécharger le journal', - 'ico' => 'download' - ]); ?> -
-
- 'buttonRed', - 'href' => helper::baseUrl() . 'config/logReset', - 'value' => 'Réinitialiser le journal', - 'ico' => 'trash' - ]); ?> +
+
+
+ + helper::baseUrl() . 'config/blacklistDownload', + 'value' => 'Télécharger la liste', + 'ico' => 'download' + ]); ?> +
+
+ 'buttonRed', + 'href' => helper::baseUrl() . 'config/blacklistReset', + 'value' => 'Réinitialiser la liste', + 'ico' => 'trash' + ]); ?> +
+
diff --git a/core/module/course/view/add/add.php b/core/module/course/view/add/add.php index dae1106..dade2aa 100644 --- a/core/module/course/view/add/add.php +++ b/core/module/course/view/add/add.php @@ -88,8 +88,9 @@
- 'Ne s\'applique pas à l\'inscription anonyme', + 'Enregistre une trace des consultations. Ne s\'applique pas à l\'inscription anonyme', + 'checked' => true ]); ?>
diff --git a/core/module/course/view/edit/edit.php b/core/module/course/view/edit/edit.php index 6487662..96b1b17 100644 --- a/core/module/course/view/edit/edit.php +++ b/core/module/course/view/edit/edit.php @@ -91,9 +91,9 @@
- $this->getdata(['course', $this->getUrl(2), 'report']), - 'help' => 'Ne s\'applique pas à l\'inscription anonyme', + 'help' => 'Enregistre une trace des consultations. Ne s\'applique pas à l\'inscription anonyme', ]); ?>
diff --git a/core/module/user/user.php b/core/module/user/user.php index 2954669..563afa7 100644 --- a/core/module/user/user.php +++ b/core/module/user/user.php @@ -27,6 +27,7 @@ class user extends common 'logout' => self::GROUP_MEMBER, 'forgot' => self::GROUP_VISITOR, 'login' => self::GROUP_VISITOR, + 'auth' => self::GROUP_VISITOR, 'reset' => self::GROUP_VISITOR, 'profil' => self::GROUP_ADMIN, 'profilEdit' => self::GROUP_ADMIN, @@ -1227,9 +1228,6 @@ class user extends common } } - /** - * Connexion - */ public function login() { // Soumission du formulaire @@ -1262,7 +1260,7 @@ class user extends common 'lastFail' => time(), 'ip' => helper::getIp() ] - ], false); + ]); // Verrouillage des IP $ipBlackList = helper::arrayColumn($this->getData(['blacklist']), 'ip'); if ( @@ -1303,37 +1301,14 @@ class user extends common and $this->getData(['user', $userId, 'group']) >= self::GROUP_MEMBER and $captcha === true ) { - // RAZ + + // RAZ des compteurs de blocage $this->setData(['user', $userId, 'connectFail', 0], false); $this->setData(['user', $userId, 'connectTimeout', 0], false); - // Clé d'authenfication - $authKey = uniqid('', true) . bin2hex(random_bytes(8)); - $this->setData(['user', $userId, 'authKey', $authKey], false); - - // Validité du cookie - $expire = $this->getInput('userLoginLongTime', helper::FILTER_BOOLEAN) === true ? strtotime("+1 year") : 0; - switch ($this->getInput('userLoginLongTime', helper::FILTER_BOOLEAN)) { - case false: - // Cookie de session - setcookie('ZWII_USER_ID', $userId, $expire, helper::baseUrl(false, false), '', helper::isHttps(), true); - //setcookie('ZWII_USER_PASSWORD', $this->getData(['user', $userId, 'password']), $expire, helper::baseUrl(false, false), '', helper::isHttps(), true); - - // Connexion par clé - setcookie('ZWII_AUTH_KEY', $authKey, $expire, helper::baseUrl(false, false), '', helper::isHttps(), true); - break; - default: - // Cookie persistant - setcookie('ZWII_USER_ID', $userId, $expire, helper::baseUrl(false, false)); - //setcookie('ZWII_USER_PASSWORD', $this->getData(['user', $userId, 'password']), $expire, helper::baseUrl(false, false)); - - // Connexion par clé - setcookie('ZWII_AUTH_KEY', $authKey, $expire, helper::baseUrl(false, false)); - break; - } - // Accès multiples avec le même compte $this->setData(['user', $userId, 'accessCsrf', $_SESSION['csrf']], false); + // Valeurs en sortie lorsque le site est en maintenance et que l'utilisateur n'est pas administrateur if ( $this->getData(['config', 'maintenance']) @@ -1345,7 +1320,49 @@ class user extends common 'state' => false ]); } else { - $logStatus = 'Connexion réussie'; + /** + * Le site n'est pas en maintenance + * Double authentification en cas de saisie correcte + */ + + // Clé d'authenfication utlisée pour lié le compte au cookie au lieu de stocke le hash du mot de passe + $authKey = uniqid('', true) . bin2hex(random_bytes(8)); + if ($this->getData(['config', 'connect', 'mailAuth']) >= $this->getData(['user', $userId, 'group'])) { + $logStatus = 'Envoi du mail d\'authentification'; + // Redirection vers la page d'authentification + $authRedirect = 'user/auth/'; + // Stocker la clé envoyée par email + $this->setData(['user', $userId, 'authKey', rand(100000, 999999)]); + + } else { + $logStatus = 'Connexion réussie'; + // La page d'autentification est vide + $authRedirect = ''; + $this->setData(['user', $userId, 'authKey', $authKey]); + + } + + // Validité du cookie + $expire = $this->getInput('userLoginLongTime', helper::FILTER_BOOLEAN) === true ? strtotime("+1 year") : 0; + switch ($this->getInput('userLoginLongTime', helper::FILTER_BOOLEAN)) { + case false: + // Cookie de session + setcookie('ZWII_USER_ID', $userId, $expire, helper::baseUrl(false, false), '', helper::isHttps(), true); + //setcookie('ZWII_USER_PASSWORD', $this->getData(['user', $userId, 'password']), $expire, helper::baseUrl(false, false), '', helper::isHttps(), true); + + // Connexion par clé + setcookie('ZWII_AUTH_KEY', $authKey, $expire, helper::baseUrl(false, false), '', helper::isHttps(), true); + break; + default: + // Cookie persistant + setcookie('ZWII_USER_ID', $userId, $expire, helper::baseUrl(false, false)); + //setcookie('ZWII_USER_PASSWORD', $this->getData(['user', $userId, 'password']), $expire, helper::baseUrl(false, false)); + + // Connexion par clé + setcookie('ZWII_AUTH_KEY', $authKey, $expire, helper::baseUrl(false, false)); + break; + } + $pageId = $this->getUrl(2); if ( $this->getData(['config', 'page404']) === $pageId @@ -1353,7 +1370,7 @@ class user extends common ) { $pageId = ''; } - $redirect = ($pageId && strpos($pageId, 'user_reset') !== 0) ? helper::baseUrl() . str_replace('_', '/', str_replace('__', '#', $pageId)) : helper::baseUrl(); + $redirect = ($pageId && strpos($pageId, 'user_reset') !== 0) ? helper::baseUrl() . $authRedirect . str_replace('_', '/', str_replace('__', '#', $pageId)) : helper::baseUrl() . $authRedirect; // Valeurs en sortie $this->addOutput([ 'notification' => sprintf(helper::translate('Bienvenue %s %s'), $this->getData(['user', $userId, 'firstname']), $this->getData(['user', $userId, 'lastname'])), @@ -1366,7 +1383,7 @@ class user extends common $notification = helper::translate('Captcha, identifiant ou mot de passe incorrects'); $logStatus = $captcha === true ? 'Erreur de mot de passe' : 'Erreur de captcha'; // Cas 1 le nombre de connexions est inférieur aux tentatives autorisées : incrément compteur d'échec - if ($this->getData(['user', $userId, 'connectFail']) < $this->getData(['config', 'connect', 'attempt'])) { + if ($this->getData(['user', $userId, 'connectFail']) < $this->getData(['config', 'connect', 'attempt'], false)) { $this->setData(['user', $userId, 'connectFail', $this->getdata(['user', $userId, 'connectFail']) + 1], false); } // Cas 2 la limite du nombre de connexion est atteinte : placer le timer @@ -1384,10 +1401,9 @@ class user extends common ]); } } - // Sauvegarde la base manuellement - $this->saveDB(module: 'user'); + // Force la sauvegarde + $this->saveDB('user'); } - // Journalisation $this->saveLog($logStatus); @@ -1403,16 +1419,108 @@ class user extends common ]); } + /** + * + * Validation de la connexion par email + * @return void + */ + public function auth() + { + // Soumission du formulaire + if ( + $this->isPost() + ) { + // Vérifier la clé saisie + $targetKey = $this->getData(['user', $this->getUser('id'), 'authKey']); + $inputKey = $this->getInput('userAuthKey', helper::FILTER_INT); + if ( + $targetKey === $inputKey && + $this->getData(['user', $this->getUser('id'), 'connectTimeout']) + 3600 >= time() + ) { + $pageId = $this->getUrl(2); + // La fiche de l'utilisateur contient la clé d'authentification + $this->setData(['user', $this->getUser('id'), 'authKey', $this->getInput('ZWII_AUTH_KEY')]); + $redirect = ($pageId && strpos($pageId, 'user_reset') !== 0) ? helper::baseUrl() . str_replace('_', '/', str_replace('__', '#', $pageId)) : helper::baseUrl(); + // Journalisation + $this->saveLog('Connexion réussie'); + // Réinitialiser le compteur de temps + $this->setData(['user', $this->getUser('id'), 'connectTimeout', 0]); + // Valeurs en sortie + $this->addOutput([ + 'redirect' => $redirect, + 'notification' => helper::translate('Connexion réussie'), + 'state' => true + ]); + } else { + + // Supprime la clé stockée et le temps limite + $this->deleteData(['user', $this->getUser('id'), 'authKey']); + // Réinitialiser le compteur de temps + $this->setData(['user', $this->getUser('id'), 'connectTimeout', 0]); + + // Détruit les cookies d'authenfication + helper::deleteCookie('ZWII_USER_ID'); + helper::deleteCookie('ZWII_AUTH_KEY'); + + // Détruit la session + session_destroy(); + + // Journalisation + $this->saveLog('Erreur de vérification de la clé envoyée par email ' . $this->getUser('id')); + + // Valeurs en sortie + $this->addOutput([ + 'redirect' => helper::baseUrl(), + 'notification' => helper::translate('La clé est incorrecte'), + 'state' => false + ]); + } + } else { + /** + * Envoi d'un email contenant une clé + * Stockage de la clé dans le compte de l'utilisateur + */ + // La clé est envoyée une seule fois + $sent = false; + if ( + $this->getData(['user', $this->getUser('id'), 'authKey']) + && $this->getData(['user', $this->getUser('id'), 'connectTimeout']) === 0 + ) { + $sent = $this->sendMail( + $this->getUser('mail'), + 'Tentative de connexion à votre', + //'Bonjour ' . $item['prenom'] . ' ' . $item['nom'] . ',

' . + '

Clé de validation à saisir dans le formulaire :

' . + '

' . $this->getData(['user', $this->getUser('id'), 'authKey']) . '

', + null, + $this->getData(['config', 'smtp', 'from']) + ); + // Stocker l'envoi de l'email + $this->setData(['user', $this->getUser('id'), 'connectTimeout', time()]); + } + + // Message envoyé sinon la connexion est réalisée pour ne pas bloquer. + if ($sent === false) { + + } + // Valeurs en sortie + $this->addOutput([ + 'title' => helper::translate('Double authentification'), + 'view' => 'auth', + 'display' => self::DISPLAY_LAYOUT_LIGHT, + ]); + } + } + /** * Déconnexion */ public function logout() { + // Détruit les cookies d'authenfication helper::deleteCookie('ZWII_USER_ID'); - //helper::deleteCookie('ZWII_USER_PASSWORD'); helper::deleteCookie('ZWII_AUTH_KEY'); - // Détruit la session session_destroy(); diff --git a/core/module/user/view/auth/auth.css b/core/module/user/view/auth/auth.css new file mode 100644 index 0000000..e217932 --- /dev/null +++ b/core/module/user/view/auth/auth.css @@ -0,0 +1,36 @@ +/** + * This file is part of Zwii. + * + * For full copyright and license information, please see the LICENSE + * file that was distributed with this source code. + * + * @author Rémi Jean + * @copyright Copyright (C) 2008-2018, Rémi Jean + * @author Frédéric Tempez + * @copyright Copyright (C) 2018-2024, Frédéric Tempez + * @license CC Attribution-NonCommercial-NoDerivatives 4.0 International + * @link http://zwiicms.fr/ + */ + +/** @import url("site/data/admin.css"); */ + +/** NE PAS EFFACER +* admin.css +*/ + +@media screen and (max-width: 768px) { + #buttonsContainer { + display: grid; + } + #loginContainer { + order: 1; + } + #backContainer{ + order: 2; + } +} + +#userAuthKey { + text-align: center; + font-size: 1.3rem; +} \ No newline at end of file diff --git a/core/module/user/view/auth/auth.php b/core/module/user/view/auth/auth.php new file mode 100644 index 0000000..ca948b6 --- /dev/null +++ b/core/module/user/view/auth/auth.php @@ -0,0 +1,23 @@ + +
+
+ helper::translate('Clé reçue par couriel') + ]); ?> +
+
+
+
+ $this->getUrl(2) ? helper::baseUrl() . ' user/login' . str_replace('_', '/', str_replace('__', '#', $this->getUrl(2))) : helper::baseUrl() . ' user/login', + 'value' => template::ico('left') + ]); ?> +
+
+ helper::translate('Authentification'), + 'ico' => '' + ]); ?> +
+
+ \ No newline at end of file