diff --git a/app/Contracts/UserInterface.php b/app/Contracts/UserInterface.php index 302d15057aa..51f8748c43d 100644 --- a/app/Contracts/UserInterface.php +++ b/app/Contracts/UserInterface.php @@ -31,6 +31,7 @@ interface UserInterface public const string PREF_IS_ADMINISTRATOR = 'canadmin'; public const string PREF_IS_EMAIL_VERIFIED = 'verified'; public const string PREF_IS_VISIBLE_ONLINE = 'visibleonline'; + public const string PREF_IS_STATUS_MFA = 'statusmfa'; public const string PREF_LANGUAGE = 'language'; public const string PREF_NEW_ACCOUNT_COMMENT = 'comment'; public const string PREF_TIMESTAMP_REGISTERED = 'reg_timestamp'; diff --git a/app/Http/RequestHandlers/AccountEdit.php b/app/Http/RequestHandlers/AccountEdit.php index c69f0d0bdd8..f27e776d344 100644 --- a/app/Http/RequestHandlers/AccountEdit.php +++ b/app/Http/RequestHandlers/AccountEdit.php @@ -28,6 +28,7 @@ use Fisharebest\Webtrees\Registry; use Fisharebest\Webtrees\Services\MessageService; use Fisharebest\Webtrees\Services\ModuleService; +use Fisharebest\Webtrees\Site; use Fisharebest\Webtrees\Tree; use Fisharebest\Webtrees\Validator; use Psr\Http\Message\ResponseInterface; @@ -83,6 +84,7 @@ public function handle(ServerRequestInterface $request): ResponseInterface }); $show_delete_option = $user->getPreference(UserInterface::PREF_IS_ADMINISTRATOR) !== '1'; + $show_2fa = Site::getPreference('SHOW_2FA_OPTION') === '1'; $timezone_ids = DateTimeZone::listIdentifiers(); $timezones = array_combine($timezone_ids, $timezone_ids); $title = I18N::translate('My account'); @@ -93,6 +95,7 @@ public function handle(ServerRequestInterface $request): ResponseInterface 'languages' => $languages->all(), 'my_individual_record' => $my_individual_record, 'show_delete_option' => $show_delete_option, + 'show_2fa' => $show_2fa, 'timezones' => $timezones, 'title' => $title, 'tree' => $tree, diff --git a/app/Http/RequestHandlers/AccountUpdate.php b/app/Http/RequestHandlers/AccountUpdate.php index 3bfa21e05db..574220731b3 100644 --- a/app/Http/RequestHandlers/AccountUpdate.php +++ b/app/Http/RequestHandlers/AccountUpdate.php @@ -67,15 +67,22 @@ public function handle(ServerRequestInterface $request): ResponseInterface $language = Validator::parsedBody($request)->string('language'); $real_name = Validator::parsedBody($request)->string('real_name'); $password = Validator::parsedBody($request)->string('password'); + $secret = Validator::parsedBody($request)->string('secret'); $time_zone = Validator::parsedBody($request)->string('timezone'); $user_name = Validator::parsedBody($request)->string('user_name'); $visible_online = Validator::parsedBody($request)->boolean('visible-online', false); + $status_mfa = Validator::parsedBody($request)->boolean('status-mfa', false); + // Change the password if ($password !== '') { $user->setPassword($password); } + // Change the secret + if ($secret !== '' || $status_mfa === false) { + $user->setSecret($secret); + } // Change the username if ($user_name !== $user->userName()) { if ($this->user_service->findByUserName($user_name) === null) { @@ -99,6 +106,7 @@ public function handle(ServerRequestInterface $request): ResponseInterface $user->setPreference(UserInterface::PREF_LANGUAGE, $language); $user->setPreference(UserInterface::PREF_TIME_ZONE, $time_zone); $user->setPreference(UserInterface::PREF_IS_VISIBLE_ONLINE, (string) $visible_online); + $user->setPreference(UserInterface::PREF_IS_STATUS_MFA, (string) $status_mfa); if ($tree instanceof Tree) { $default_xref = Validator::parsedBody($request)->string('default-xref'); diff --git a/app/Http/RequestHandlers/LoginAction.php b/app/Http/RequestHandlers/LoginAction.php index 64744f3cdda..e232980fccb 100644 --- a/app/Http/RequestHandlers/LoginAction.php +++ b/app/Http/RequestHandlers/LoginAction.php @@ -69,10 +69,11 @@ public function handle(ServerRequestInterface $request): ResponseInterface $default_url = route(HomePage::class); $username = Validator::parsedBody($request)->string('username'); $password = Validator::parsedBody($request)->string('password'); + $code2fa = Validator::parsedBody($request)->string('code2fa'); $url = Validator::parsedBody($request)->isLocalUrl()->string('url', $default_url); try { - $this->doLogin($username, $password); + $this->doLogin($username, $password, $code2fa); if (Auth::isAdmin() && $this->upgrade_service->isUpgradeAvailable()) { FlashMessages::addMessage(I18N::translate('A new version of webtrees is available.') . ' ' . I18N::translate('Upgrade to webtrees %s.', '' . $this->upgrade_service->latestVersion() . '') . ''); @@ -97,11 +98,12 @@ public function handle(ServerRequestInterface $request): ResponseInterface * * @param string $username * @param string $password + * @param string $code2fa * * @return void * @throws Exception */ - private function doLogin(string $username, #[\SensitiveParameter] string $password): void + private function doLogin(string $username, #[\SensitiveParameter] string $password, string $code2fa): void { if ($_COOKIE === []) { Log::addAuthenticationLog('Login failed (no session cookies): ' . $username); @@ -129,7 +131,16 @@ private function doLogin(string $username, #[\SensitiveParameter] string $passwo Log::addAuthenticationLog('Login failed (not approved by admin): ' . $username); throw new Exception(I18N::translate('This account has not been approved. Please wait for an administrator to approve it.')); } - + if ($user->getPreference(UserInterface::PREF_IS_STATUS_MFA) !== '') { + # covers scenario where 2fa not enabled by user + if ($code2fa != '') { + if (!$user->check2FAcode($code2fa)) { + throw new Exception(I18N::translate('2FA code does not match. Please try again.')); + } + } else { + throw new Exception(I18N::translate('2FA code must be entered as you have 2FA authentication enabled. Please try again.')); + } + } Auth::login($user); Log::addAuthenticationLog('Login: ' . Auth::user()->userName() . '/' . Auth::user()->realName()); Auth::user()->setPreference(UserInterface::PREF_TIMESTAMP_ACTIVE, (string) time()); diff --git a/app/Http/RequestHandlers/RegisterAction.php b/app/Http/RequestHandlers/RegisterAction.php index 3fcc6e6caed..71d0261e236 100644 --- a/app/Http/RequestHandlers/RegisterAction.php +++ b/app/Http/RequestHandlers/RegisterAction.php @@ -135,6 +135,7 @@ public function handle(ServerRequestInterface $request): ResponseInterface $user->setPreference(UserInterface::PREF_CONTACT_METHOD, MessageService::CONTACT_METHOD_INTERNAL_AND_EMAIL); $user->setPreference(UserInterface::PREF_NEW_ACCOUNT_COMMENT, $comments); $user->setPreference(UserInterface::PREF_IS_VISIBLE_ONLINE, '1'); + $user->setPreference(UserInterface::PREF_IS_STATUS_MFA, '0'); $user->setPreference(UserInterface::PREF_AUTO_ACCEPT_EDITS, ''); $user->setPreference(UserInterface::PREF_IS_ADMINISTRATOR, ''); $user->setPreference(UserInterface::PREF_TIMESTAMP_ACTIVE, '0'); diff --git a/app/Http/RequestHandlers/SetupWizard.php b/app/Http/RequestHandlers/SetupWizard.php index 7d532911933..d56dcbef6b4 100644 --- a/app/Http/RequestHandlers/SetupWizard.php +++ b/app/Http/RequestHandlers/SetupWizard.php @@ -83,6 +83,7 @@ class SetupWizard implements RequestHandlerInterface 'wtuser' => '', 'wtpass' => '', 'wtemail' => '', + 'wtsecret' => '', ]; private const array DEFAULT_PORTS = [ @@ -408,6 +409,7 @@ private function createConfigFile(array $data): void $admin = $this->user_service->create($data['wtuser'], $data['wtname'], $data['wtemail'], $data['wtpass']); $admin->setPreference(UserInterface::PREF_LANGUAGE, $data['lang']); $admin->setPreference(UserInterface::PREF_IS_VISIBLE_ONLINE, '1'); + $admin->setPreference(UserInterface::PREF_IS_STATUS_MFA, '0'); } else { $admin->setPassword($_POST['wtpass']); } diff --git a/app/Http/RequestHandlers/SiteRegistrationAction.php b/app/Http/RequestHandlers/SiteRegistrationAction.php index 6e58dfec0fa..b2c8cb93bc5 100644 --- a/app/Http/RequestHandlers/SiteRegistrationAction.php +++ b/app/Http/RequestHandlers/SiteRegistrationAction.php @@ -46,11 +46,13 @@ public function handle(ServerRequestInterface $request): ResponseInterface $text = Validator::parsedBody($request)->string('WELCOME_TEXT_AUTH_MODE_4'); $allow_registration = Validator::parsedBody($request)->boolean('USE_REGISTRATION_MODULE'); $show_caution = Validator::parsedBody($request)->boolean('SHOW_REGISTER_CAUTION'); + $show_2fa = Validator::parsedBody($request)->boolean('SHOW_2FA_OPTION'); Site::setPreference('WELCOME_TEXT_AUTH_MODE', $mode); Site::setPreference('WELCOME_TEXT_AUTH_MODE_' . I18N::languageTag(), $text); Site::setPreference('USE_REGISTRATION_MODULE', (string) $allow_registration); Site::setPreference('SHOW_REGISTER_CAUTION', (string) $show_caution); + Site::setPreference('SHOW_2FA_OPTION', (string) $show_2fa); FlashMessages::addMessage(I18N::translate('The website preferences have been updated.'), 'success'); diff --git a/app/Schema/Migration0.php b/app/Schema/Migration0.php index e536dff521b..3773b896d2e 100644 --- a/app/Schema/Migration0.php +++ b/app/Schema/Migration0.php @@ -61,6 +61,7 @@ public function upgrade(): void $table->string('real_name', 64); $table->string('email', 64); $table->string('password', 128); + $table->string('secret', 128); $table->unique('user_name'); $table->unique('email'); diff --git a/app/Schema/SeedUserTable.php b/app/Schema/SeedUserTable.php index 17b147237d5..ed45a07f8a3 100644 --- a/app/Schema/SeedUserTable.php +++ b/app/Schema/SeedUserTable.php @@ -46,6 +46,7 @@ public function run(): void 'real_name' => 'DEFAULT_USER', 'email' => 'DEFAULT_USER', 'password' => 'DEFAULT_USER', + 'secret' => 'DEFAULT_USER', ]); if (DB::driverName() === DB::SQL_SERVER) { diff --git a/app/Services/UserService.php b/app/Services/UserService.php index 5d821fd39fa..6bc49b02d30 100644 --- a/app/Services/UserService.php +++ b/app/Services/UserService.php @@ -319,6 +319,7 @@ public function create(string $user_name, string $real_name, string $email, #[\S 'real_name' => $real_name, 'email' => $email, 'password' => password_hash($password, PASSWORD_DEFAULT), + 'secret' => '', ]); $user_id = DB::lastInsertId(); diff --git a/app/User.php b/app/User.php index 7d879d225ae..dabafc92211 100644 --- a/app/User.php +++ b/app/User.php @@ -21,6 +21,8 @@ use Closure; use Fisharebest\Webtrees\Contracts\UserInterface; +use PragmaRX\Google2FA\Google2FA; +use chillerlan\QRCode\QRCode; use function is_string; @@ -110,6 +112,24 @@ public function realName(): string { return $this->real_name; } + /** + * Generate a QR code image based on 2FA secret and return both. + * + * @return array + */ + + public function genQRcode(): array + { + $qrinfo = array(); + $google2fa = new Google2FA(); + $qrinfo['secret'] = $google2fa->generateSecretKey(); + $servername = $_SERVER['SERVER_NAME']; + settype($servername, "string"); + $data = 'otpauth://totp/' . $this->user_id . '?secret=' . $qrinfo['secret'] . '&issuer=' . $servername; + $qrcode = new QRCode(); + $qrinfo['qrcode'] = $qrcode->render($data); + return $qrinfo; + } /** * Set the real name of this user. @@ -243,6 +263,45 @@ public function checkPassword(#[\SensitiveParameter] string $password): bool return false; } + /** + * Set the Secret of this user. + * + * @param string $secret + * + * @return User + */ + public function setSecret(#[\SensitiveParameter] string $secret): User + { + DB::table('user') + ->where('user_id', '=', $this->user_id) + ->update([ + 'secret' => $secret, + ]); + + return $this; + } + + /** + * Validate a supplied 2fa code + * + * @param string $code2fa + * + * @return bool + */ + public function check2facode(string $code2fa): bool + { + $secret = DB::table('user') + ->where('user_id', '=', $this->id()) + ->value('secret'); + settype($secret, "string"); + $google2fa = new Google2FA(); + $googleverifystatus = $google2fa->verifyKey($secret, $code2fa); + settype($googleverifystatus, "bool"); + if ($googleverifystatus) { + return true; + } + return false; + } /** * A closure which will create an object from a database row. diff --git a/composer.json b/composer.json index ad06a77d4c2..5982b91ae99 100644 --- a/composer.json +++ b/composer.json @@ -42,6 +42,7 @@ "ext-session": "*", "ext-xml": "*", "aura/router": "3.3.0", + "chillerlan/php-qrcode": "4.4.0", "ezyang/htmlpurifier": "4.18.0", "fig/http-message-util": "1.1.5", "fisharebest/algorithm": "1.6.0", @@ -62,6 +63,7 @@ "nyholm/psr7": "1.8.2", "nyholm/psr7-server": "1.1.0", "oscarotero/middleland": "1.0.1", + "pragmarx/google2fa": "^8.0", "psr/cache": "3.0.0", "psr/http-message": "1.1", "psr/http-server-handler": "1.0.2", diff --git a/composer.lock b/composer.lock index 9564b83c79c..88bf4f26c29 100644 --- a/composer.lock +++ b/composer.lock @@ -188,6 +188,153 @@ ], "time": "2024-02-09T16:56:22+00:00" }, + { + "name": "chillerlan/php-qrcode", + "version": "4.4.0", + "source": { + "type": "git", + "url": "https://github.com/chillerlan/php-qrcode.git", + "reference": "52889cd7ab1b78e6a345edafe24aa74bc5becc08" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/chillerlan/php-qrcode/zipball/52889cd7ab1b78e6a345edafe24aa74bc5becc08", + "reference": "52889cd7ab1b78e6a345edafe24aa74bc5becc08", + "shasum": "" + }, + "require": { + "chillerlan/php-settings-container": "^2.1.4 || ^3.1", + "ext-mbstring": "*", + "php": "^7.4 || ^8.0" + }, + "require-dev": { + "phan/phan": "^5.4", + "phpmd/phpmd": "^2.13", + "phpunit/phpunit": "^9.6", + "setasign/fpdf": "^1.8.2", + "squizlabs/php_codesniffer": "^3.7" + }, + "suggest": { + "chillerlan/php-authenticator": "Yet another Google authenticator! Also creates URIs for mobile apps.", + "setasign/fpdf": "Required to use the QR FPDF output.", + "simple-icons/simple-icons": "SVG icons that you can use to embed as logos in the QR Code" + }, + "type": "library", + "autoload": { + "psr-4": { + "chillerlan\\QRCode\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Kazuhiko Arase", + "homepage": "https://github.com/kazuhikoarase" + }, + { + "name": "Smiley", + "email": "smiley@chillerlan.net", + "homepage": "https://github.com/codemasher" + }, + { + "name": "Contributors", + "homepage": "https://github.com/chillerlan/php-qrcode/graphs/contributors" + } + ], + "description": "A QR code generator with a user friendly API. PHP 7.4+", + "homepage": "https://github.com/chillerlan/php-qrcode", + "keywords": [ + "phpqrcode", + "qr", + "qr code", + "qrcode", + "qrcode-generator" + ], + "support": { + "issues": "https://github.com/chillerlan/php-qrcode/issues", + "source": "https://github.com/chillerlan/php-qrcode/tree/4.4.0" + }, + "funding": [ + { + "url": "https://www.paypal.com/donate?hosted_button_id=WLYUNAT9ZTJZ4", + "type": "custom" + }, + { + "url": "https://ko-fi.com/codemasher", + "type": "ko_fi" + } + ], + "time": "2023-11-23T23:53:20+00:00" + }, + { + "name": "chillerlan/php-settings-container", + "version": "3.2.1", + "source": { + "type": "git", + "url": "https://github.com/chillerlan/php-settings-container.git", + "reference": "95ed3e9676a1d47cab2e3174d19b43f5dbf52681" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/chillerlan/php-settings-container/zipball/95ed3e9676a1d47cab2e3174d19b43f5dbf52681", + "reference": "95ed3e9676a1d47cab2e3174d19b43f5dbf52681", + "shasum": "" + }, + "require": { + "ext-json": "*", + "php": "^8.1" + }, + "require-dev": { + "phpmd/phpmd": "^2.15", + "phpstan/phpstan": "^1.11", + "phpstan/phpstan-deprecation-rules": "^1.2", + "phpunit/phpunit": "^10.5", + "squizlabs/php_codesniffer": "^3.10" + }, + "type": "library", + "autoload": { + "psr-4": { + "chillerlan\\Settings\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Smiley", + "email": "smiley@chillerlan.net", + "homepage": "https://github.com/codemasher" + } + ], + "description": "A container class for immutable settings objects. Not a DI container.", + "homepage": "https://github.com/chillerlan/php-settings-container", + "keywords": [ + "Settings", + "configuration", + "container", + "helper" + ], + "support": { + "issues": "https://github.com/chillerlan/php-settings-container/issues", + "source": "https://github.com/chillerlan/php-settings-container" + }, + "funding": [ + { + "url": "https://www.paypal.com/donate?hosted_button_id=WLYUNAT9ZTJZ4", + "type": "custom" + }, + { + "url": "https://ko-fi.com/codemasher", + "type": "ko_fi" + } + ], + "time": "2024-07-16T11:13:48+00:00" + }, { "name": "dflydev/dot-access-data", "version": "v3.0.3", @@ -2768,6 +2915,125 @@ }, "time": "2020-12-06T00:59:17+00:00" }, + { + "name": "paragonie/constant_time_encoding", + "version": "v3.0.0", + "source": { + "type": "git", + "url": "https://github.com/paragonie/constant_time_encoding.git", + "reference": "df1e7fde177501eee2037dd159cf04f5f301a512" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/paragonie/constant_time_encoding/zipball/df1e7fde177501eee2037dd159cf04f5f301a512", + "reference": "df1e7fde177501eee2037dd159cf04f5f301a512", + "shasum": "" + }, + "require": { + "php": "^8" + }, + "require-dev": { + "phpunit/phpunit": "^9", + "vimeo/psalm": "^4|^5" + }, + "type": "library", + "autoload": { + "psr-4": { + "ParagonIE\\ConstantTime\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Paragon Initiative Enterprises", + "email": "security@paragonie.com", + "homepage": "https://paragonie.com", + "role": "Maintainer" + }, + { + "name": "Steve 'Sc00bz' Thomas", + "email": "steve@tobtu.com", + "homepage": "https://www.tobtu.com", + "role": "Original Developer" + } + ], + "description": "Constant-time Implementations of RFC 4648 Encoding (Base-64, Base-32, Base-16)", + "keywords": [ + "base16", + "base32", + "base32_decode", + "base32_encode", + "base64", + "base64_decode", + "base64_encode", + "bin2hex", + "encoding", + "hex", + "hex2bin", + "rfc4648" + ], + "support": { + "email": "info@paragonie.com", + "issues": "https://github.com/paragonie/constant_time_encoding/issues", + "source": "https://github.com/paragonie/constant_time_encoding" + }, + "time": "2024-05-08T12:36:18+00:00" + }, + { + "name": "pragmarx/google2fa", + "version": "v8.0.3", + "source": { + "type": "git", + "url": "https://github.com/antonioribeiro/google2fa.git", + "reference": "6f8d87ebd5afbf7790bde1ffc7579c7c705e0fad" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/antonioribeiro/google2fa/zipball/6f8d87ebd5afbf7790bde1ffc7579c7c705e0fad", + "reference": "6f8d87ebd5afbf7790bde1ffc7579c7c705e0fad", + "shasum": "" + }, + "require": { + "paragonie/constant_time_encoding": "^1.0|^2.0|^3.0", + "php": "^7.1|^8.0" + }, + "require-dev": { + "phpstan/phpstan": "^1.9", + "phpunit/phpunit": "^7.5.15|^8.5|^9.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "PragmaRX\\Google2FA\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Antonio Carlos Ribeiro", + "email": "acr@antoniocarlosribeiro.com", + "role": "Creator & Designer" + } + ], + "description": "A One Time Password Authentication package, compatible with Google Authenticator.", + "keywords": [ + "2fa", + "Authentication", + "Two Factor Authentication", + "google2fa" + ], + "support": { + "issues": "https://github.com/antonioribeiro/google2fa/issues", + "source": "https://github.com/antonioribeiro/google2fa/tree/v8.0.3" + }, + "time": "2024-09-05T11:56:40+00:00" + }, { "name": "psr/cache", "version": "3.0.0", diff --git a/public/js/totp.js b/public/js/totp.js new file mode 100644 index 00000000000..c94b53dad25 --- /dev/null +++ b/public/js/totp.js @@ -0,0 +1,31 @@ +$( document ).ready(function() { +// resize the qr code and hide as default in edit user page + $('div#qrcode').css('maxWidth', '300px'); + $('div#qrcode').hide(); + +// show a link to get another qr code if 2fa enabled + if($('input#status-mfa-1').is(':checked')) { + $('input#status-mfa-1').parent().append("  - (Click to generate new QR code) ") + } + +// click to get new qr code and secret +$("a#getnewqr").click(function(){ + $('div#qrcode').show(); + $("a#getnewqr").hide(); + $('input#secret').val($('input#newsecret').val()) +}); +// deal with toggling of 2fa setting to ensure no secret saved if no 2fa required but the secret associated with any generated qr code is saved. + $('input#status-mfa-1').change(function() { + if(this.checked) { + $(this).parent().append("  - (Click to generate new QR code) ") + $('div#qrcode').show(); + $('input#secret').val($('input#newsecret').val()) + } + else { + $('div#qrcode').hide(); + $("a#getnewqr").hide(); + $('input#secret').val(''); + } + }); +}); + diff --git a/resources/views/admin/site-registration.phtml b/resources/views/admin/site-registration.phtml index 930121f0ef7..c628ff7e366 100644 --- a/resources/views/admin/site-registration.phtml +++ b/resources/views/admin/site-registration.phtml @@ -71,6 +71,21 @@ use Fisharebest\Webtrees\Site; + + +
+ + + +
+ 'SHOW_2FA_OPTION', 'options' => [I18N::translate('no'), I18N::translate('yes')], 'selected' => (int) Site::getPreference('SHOW_2FA_OPTION')]) ?> +
+
+
+
+ +
diff --git a/resources/views/admin/users-edit.phtml b/resources/views/admin/users-edit.phtml index da9a2eb2217..f9dff5c343f 100644 --- a/resources/views/admin/users-edit.phtml +++ b/resources/views/admin/users-edit.phtml @@ -36,6 +36,7 @@ use Illuminate\Support\Collection;
+
diff --git a/resources/views/edit-account-page.phtml b/resources/views/edit-account-page.phtml index 991dde6344d..45bef4a9bcf 100644 --- a/resources/views/edit-account-page.phtml +++ b/resources/views/edit-account-page.phtml @@ -14,6 +14,7 @@ use Fisharebest\Webtrees\Tree; * @var array $languages * @var Individual|null $my_individual_record * @var bool $show_delete_option + * @var bool $show_2fa * @var array $timezones * @var string $title * @var Tree|null $tree @@ -153,7 +154,25 @@ use Fisharebest\Webtrees\Tree;
- + +
+ + + +
+ I18N::translate('Enable or disable 2FA status'), 'name' => 'status-mfa', 'checked' => (bool) $user->getPreference(UserInterface::PREF_IS_STATUS_MFA)]) ?> +
+ +
+
+
+ +
+ genQRcode(); ?> + + + QR Code +
diff --git a/resources/views/layouts/default.phtml b/resources/views/layouts/default.phtml index 006026025fa..5b1e7ad03bb 100644 --- a/resources/views/layouts/default.phtml +++ b/resources/views/layouts/default.phtml @@ -144,6 +144,7 @@ use Psr\Http\Message\ServerRequestInterface; +