diff --git a/Core/Controller/EditUser.php b/Core/Controller/EditUser.php index 708b125f27..0ce7a35857 100644 --- a/Core/Controller/EditUser.php +++ b/Core/Controller/EditUser.php @@ -1,7 +1,7 @@ + * Copyright (C) 2017-2025 Carlos Garcia Gomez * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as @@ -76,6 +76,48 @@ private function allowUpdate(): bool return $user->nick === $this->user->nick; } + protected function authentication2factorAction(): bool + { + $user = $this->getModel(); + if (false === $this->validateFormToken()) { + return false; + } elseif (false === $this->allowUpdate()) { + Tools::log()->error('not-allowed-modify'); + return false; + } elseif (false === $user->loadFromCode($this->request->get('code'))) { + Tools::log()->error('record-not-found'); + return false; + } elseif ($user->two_factor_enabled) { + Tools::log()->error('two-factor-authentication-already-enabled', ['%nick%' => $user->nick]); + return false; + } + + // Obtener el código TOTP enviado en la solicitud + $totpCode = $this->request->request->get('code_totp'); + if (empty($totpCode)) { + Tools::log()->error('totp-code-not-received'); + return false; + } + + // Validar el código TOTP + if (false === TwoFactorManager::verifyCode($user->two_factor_secret_key, $totpCode)) { + Tools::log()->error('incorrect-totp-code.'); + return false; + } + + // Activar la autenticación de dos factores y guardar el estado + $user->two_factor_enabled = true; + if (false === $user->save()) { + Tools::log()->error("error-saving two-factor-status-for-user", ['%nick%' => $user->nick]); + return false; + } + + Tools::log()->info("totp-code-correct-two-step-authentication-has-been-activated-for-the-user", [ + '%nick%' => $user->nick + ]); + return true; + } + /** * Load views */ @@ -146,6 +188,33 @@ protected function createViewsRole(string $viewName = 'EditRoleUser'): void ->disableColumn('user', true); } + protected function deauthentication2factorAction(): bool + { + $user = $this->getModel(); + if (false === $this->validateFormToken()) { + return false; + } elseif (false === $this->allowUpdate()) { + Tools::log()->error('not-allowed-modify'); + return false; + } elseif (false === $user->loadFromCode($this->request->get('code'))) { + Tools::log()->error('record-not-found'); + return false; + } elseif (false === $user->two_factor_enabled) { + Tools::log()->error('two-factor-authentication-already-enabled', ['%nick%' => $user->nick]); + return false; + } + + // Disable two-factor authentication + $user->two_factor_enabled = false; + if (false === $user->save()) { + Tools::log()->error("error-saving two-factor-status-for-user", ['%nick%' => $user->nick]); + return false; + } + + Tools::log()->info("two-step-authentication-has-been-deactivated-for-the-user", ['%nick%' => $user->nick]); + return true; + } + protected function deleteAction(): bool { // only admin can delete users @@ -153,6 +222,17 @@ protected function deleteAction(): bool return parent::deleteAction(); } + protected function execPreviousAction($action) + { + if ($action === 'authentication2factor') { + return $this->authentication2factorAction(); + } elseif ($action === 'deauthentication2factor') { + return $this->deauthentication2factorAction(); + } + + return parent::execPreviousAction($action); + } + protected function editAction(): bool { $this->permissions->allowUpdate = $this->allowUpdate(); @@ -180,13 +260,6 @@ protected function editAction(): bool return $result; } - protected function insertAction(): bool - { - // only admin can create users - $this->permissions->allowUpdate = $this->user->admin; - return parent::insertAction(); - } - /** * Return a list of pages where user has access. * @@ -225,53 +298,11 @@ protected function getUserPages(User $user): array return $pageList; } - protected function execAfterAction($action) - { - if ($action === 'modal2fa') { - // Obtener el código TOTP enviado en la solicitud - $totpCode = $this->request->request->get('codetime'); - - // Validar que el código no esté vacío - if (empty($totpCode)) { - Tools::log()->error('totp-code-not-received'); - return parent::execAfterAction($action); - } - - // Obtener el modelo de usuario para validar el TOTP - $userModel = $this->views['EditUser']->model; - - // Validar el código TOTP - if ($this->validateTotpCode($userModel, $totpCode)) { - // Activar la autenticación de dos factores y guardar el estado - $userModel->two_factor_enabled = true; - if ($userModel->save()) { - Tools::log()->info("totp-code-correct-two-step-authentication-has-been-activated-for-the-user", ['%nick%' => $userModel->nick]); - } else { - Tools::log()->error("error-saving two-factor-status-for-user", ['%nick%' => $userModel->nick]); - } - } else { - Tools::log()->error('incorrect-totp-code.'); - } - } - - return parent::execAfterAction($action); - } - - /** - * Valida el código TOTP proporcionado por el usuario. - * - * @param User $userModel El modelo del usuario. - * @param string $totpCode El código TOTP introducido. - * @return bool Verdadero si el código es válido, falso en caso contrario. - */ - private function validateTotpCode(User $userModel, string $totpCode): bool + protected function insertAction(): bool { - if (empty($userModel->two_factor_secret_key)) { - Tools::log()->error("El usuario con nick {$userModel->nick} no tiene una clave secreta de TOTP configurada."); - return false; - } - - return TwoFactorManager::verifyCode($userModel->two_factor_secret_key, $totpCode); + // only admin can create users + $this->permissions->allowUpdate = $this->user->admin; + return parent::insertAction(); } /** @@ -298,7 +329,7 @@ protected function loadData($viewName, $view) $this->loadLanguageValues(); // guarda el usuario si no tiene clave secreta de dos factores - if (empty($view->model->two_factor_secret_key)) { + if ($view->model->exists() && empty($view->model->two_factor_secret_key)) { $view->model->save(); } @@ -327,6 +358,12 @@ protected function loadData($viewName, $view) ]; $view->loadData('', $where); break; + + case 'UserTwoFactor': + if (empty($this->getViewModelValue($mvn, 'two_factor_secret_key'))) { + $this->tab($viewName)->setSettings('active', false); + } + break; } } diff --git a/Core/Lib/TwoFactorManager.php b/Core/Lib/TwoFactorManager.php index b92e9b2ebc..658bf508bd 100644 --- a/Core/Lib/TwoFactorManager.php +++ b/Core/Lib/TwoFactorManager.php @@ -1,7 +1,7 @@ + * Copyright (C) 2024-2025 Carlos Garcia Gomez * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as @@ -28,11 +28,9 @@ class TwoFactorManager { - // Constantes configurables private const QR_CODE_SIZE = 400; private const VERIFICATION_WINDOW = 8; - // Instancia de Google2FA reutilizable private static $google2fa; /** @@ -48,69 +46,74 @@ private static function getGoogle2FA(): Google2FA /** * Genera una nueva clave secreta para la autenticación de dos factores. - * - * @return string La clave secreta generada. */ public static function getSecretKey(): string { - return self::getGoogle2FA()->generateSecretKey(); + try { + return self::getGoogle2FA()->generateSecretKey(); + } catch (Exception $e) { + Tools::log()->error('error-generating-secret-key', [ + '%message%' => $e->getMessage(), + ]); + return ''; + } } /** * Genera la URL para el código QR que puede ser escaneado por una aplicación TOTP. - * - * @param string $companyName Nombre de la compañía. - * @param string $email Correo electrónico del usuario. - * @param string $secretKey La clave secreta generada. - * @return string La URL del código QR. */ public static function getQRCodeUrl(string $companyName, string $email, string $secretKey): string { - return self::getGoogle2FA()->getQRCodeUrl($companyName, $email, $secretKey); + try { + return self::getGoogle2FA()->getQRCodeUrl($companyName, $email, $secretKey); + } catch (Exception $e) { + Tools::log()->error('error-generating-qr-code-url', [ + '%message%' => $e->getMessage(), + '%companyName%' => $companyName, + '%email%' => $email, + '%secretKey%' => $secretKey, + ]); + return ''; + } } /** * Genera una imagen de código QR en formato base64 a partir de una URL. - * - * @param string $url La URL del código QR. - * @return string La imagen del código QR codificada en base64. - * @throws Exception Si ocurre un error al generar la imagen. */ public static function getQRCodeImage(string $url): string { try { - $QRcode = QrCode::create($url) + $qrCode = QrCode::create($url) ->setSize(self::QR_CODE_SIZE) ->setForegroundColor(new Color(0, 0, 0)) ->setBackgroundColor(new Color(255, 255, 255)); $writer = new PngWriter(); - $result = $writer->write($QRcode); + $result = $writer->write($qrCode); return $result->getDataUri(); - /*$writer = new Writer( - new ImageRenderer( - new RendererStyle(self::QR_CODE_SIZE), - new ImagickImageBackEnd() - ) - ); - - return base64_encode($writer->writeString($url));*/ } catch (Exception $e) { - // Loguea el error si ocurre - Tools::log()->error("Error generating QR code: " . $e->getMessage()); - throw new Exception("Failed to generate QR code image."); + Tools::log()->error('error-generating-qr-code', [ + '%message%' => $e->getMessage(), + '%url%' => $url, + ]); + return ''; } } /** * Verifica si un código TOTP es válido. - * - * @param string $secretKey La clave secreta asociada con el usuario. - * @param string $code El código TOTP introducido por el usuario. - * @return bool Verdadero si el código es válido, falso si no lo es. */ public static function verifyCode(string $secretKey, string $code): bool { - return self::getGoogle2FA()->verifyKey($secretKey, $code, self::VERIFICATION_WINDOW); + try { + return self::getGoogle2FA()->verifyKey($secretKey, $code, self::VERIFICATION_WINDOW); + } catch (Exception $e) { + Tools::log()->error('error-verifying-code', [ + '%message%' => $e->getMessage(), + '%secretKey%' => $secretKey, + '%code%' => $code, + ]); + return false; + } } } diff --git a/Core/Model/User.php b/Core/Model/User.php index 8156c857f0..41669a8f28 100644 --- a/Core/Model/User.php +++ b/Core/Model/User.php @@ -199,10 +199,6 @@ public function getRoles(): array public function getTwoFactorUrl(): string { - if (empty($this->two_factor_secret_key)) { - $this->two_factor_secret_key = TwoFactorManager::getSecretKey(); - } - return TwoFactorManager::getQRCodeUrl('FacturaScripts', $this->email, $this->two_factor_secret_key); } diff --git a/Core/View/Master/MenuTemplate.html.twig b/Core/View/Master/MenuTemplate.html.twig index 15d946b79d..77851497c8 100644 --- a/Core/View/Master/MenuTemplate.html.twig +++ b/Core/View/Master/MenuTemplate.html.twig @@ -1,7 +1,7 @@ {# /** * This file is part of FacturaScripts - * Copyright (C) 2017-2024 Carlos Garcia Gomez + * Copyright (C) 2017-2025 Carlos Garcia Gomez * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as @@ -43,8 +43,8 @@ {% include includeView['path'] %} {% endfor %} {% block css %} - - + + {% endblock %} {# Adds custom CSS assets #} @@ -59,11 +59,11 @@ {% endfor %} {% block javascripts %} - + - + {% endblock %} {# Adds custom JS assets #} diff --git a/Core/View/Master/MicroTemplate.html.twig b/Core/View/Master/MicroTemplate.html.twig index 8d7e183bd6..2d5b99ca6c 100644 --- a/Core/View/Master/MicroTemplate.html.twig +++ b/Core/View/Master/MicroTemplate.html.twig @@ -1,7 +1,7 @@ {# /** * This file is part of FacturaScripts - * Copyright (C) 2017-2023 Carlos Garcia Gomez + * Copyright (C) 2017-2025 Carlos Garcia Gomez * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as @@ -36,8 +36,8 @@ {% endblock %} {% block css %} - - + +