Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
d0c2725
MFA authentication for webtrees. Integrates with setup, admin account…
kbwhizz May 22, 2025
eba292b
Adding ability for site admin to switch 2fa on or off site-wide
kbwhizz Oct 11, 2024
89a2f46
Reinstate account update tests, removing secret defined value as para…
kbwhizz Mar 13, 2025
07842e2
remove old composer.lock
kbwhizz Jun 17, 2025
3526c4d
Removing old css image files no longer in wt
kbwhizz May 22, 2025
00101b7
checking if site is enabled for 2FA as well as if user has enabled it
kbwhizz May 14, 2025
f2f03ad
Refining workflow so that there is a two stage process and user is on…
kbwhizz May 19, 2025
c4ee481
Tidied up 2 stage login functionality
kbwhizz May 21, 2025
d27417a
Adding tests for LoginPageMfa; fixing phpstan errors
kbwhizz May 22, 2025
5192856
Condensing use of findIdentifierBy function and changing refs from 2f…
kbwhizz May 27, 2025
fd87beb
Remove secret as parameter to create in UserServices
kbwhizz May 27, 2025
0e7b18c
Remove 5th param from user create function as it is set in the fn itself
kbwhizz May 27, 2025
5dcfbb1
Disconnect genQRcode from need to use UserInterface
kbwhizz May 30, 2025
817c3b2
Call genQRcode from AccountEdit.php instead of User.php as it doesn't…
kbwhizz Jun 2, 2025
3a4ac4e
Fix service class name and add use to AccountEdit
kbwhizz Jun 3, 2025
6119a39
Put user in to QrcodeService so it can access it.
kbwhizz Jun 3, 2025
b9aad63
Put cherillan lib in use too and send user obj from AccountEdit.
kbwhizz Jun 3, 2025
264b02e
Correct userid getter and phpstan fixes
kbwhizz Jun 3, 2025
bf38150
fixing qrcode declaration in edit-account html page
kbwhizz Jun 8, 2025
b2dc85d
send secret as affirmed string
kbwhizz Jun 9, 2025
c905d21
Code style fixes and space cleaning
kbwhizz Jun 9, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
308 changes: 308 additions & 0 deletions 1
Original file line number Diff line number Diff line change
@@ -0,0 +1,308 @@
<?php

/**
* webtrees: online genealogy
* Copyright (C) 2025 webtrees development team
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/

declare(strict_types=1);

namespace Fisharebest\Webtrees;

use Closure;
use Fisharebest\Webtrees\Contracts\UserInterface;
use PragmaRX\Google2FA\Google2FA;
use chillerlan\QRCode\QRCode;

use function is_string;

/**
* Provide an interface to the wt_user table.
*/
class User implements UserInterface
{
private int $user_id;

private string $user_name;

private string $real_name;

private string $email;

/** @var array<string,string> */
private array $preferences;

/**
* @param int $user_id
* @param string $user_name
* @param string $real_name
* @param string $email
*/
public function __construct(int $user_id, string $user_name, string $real_name, string $email)
{
$this->user_id = $user_id;
$this->user_name = $user_name;
$this->real_name = $real_name;
$this->email = $email;

$this->preferences = DB::table('user_setting')
->where('user_id', '=', $this->user_id)
->pluck('setting_value', 'setting_name')
->all();
}

/**
* The user‘s internal identifier.
*
* @return int
*/
public function id(): int
{
return $this->user_id;
}

/**
* The users email address.
*
* @return string
*/
public function email(): string
{
return $this->email;
}

/**
* Set the email address of this user.
*
* @param string $email
*
* @return User
*/
public function setEmail(string $email): User
{
if ($this->email !== $email) {
$this->email = $email;

DB::table('user')
->where('user_id', '=', $this->user_id)
->update([
'email' => $email,
]);
}

return $this;
}

/**
* The user‘s real name.
*
* @return string
*/
public function realName(): string
{
return $this->real_name;
}
/**
* Generate a QR code image based on 2FA secret and return both.
*
* @return array<mixed>
*/

public function genQRcode(): array
{
$qrinfo = array();
$google2fa = new Google2FA();
$qrinfo['secret'] = $google2fa->generateSecretKey();
$data = 'otpauth://totp/' . (string)$this->user_id . '?secret=' . (string)$qrinfo['secret'] . '&issuer=' . (string)$_SERVER['SERVER_NAME'];
$qrinfo['qrcode'] = (new QRCode())->render($data);
return $qrinfo;
}

/**
* Set the real name of this user.
*
* @param string $real_name
*
* @return User
*/
public function setRealName(string $real_name): User
{
if ($this->real_name !== $real_name) {
$this->real_name = $real_name;

DB::table('user')
->where('user_id', '=', $this->user_id)
->update([
'real_name' => $real_name,
]);
}

return $this;
}

/**
* The user‘s login name.
*
* @return string
*/
public function userName(): string
{
return $this->user_name;
}

/**
* Set the login name for this user.
*
* @param string $user_name
*
* @return self
*/
public function setUserName(string $user_name): self
{
if ($this->user_name !== $user_name) {
$this->user_name = $user_name;

DB::table('user')
->where('user_id', '=', $this->user_id)
->update([
'user_name' => $user_name,
]);
}

return $this;
}

/**
* Fetch a user option/setting from the wt_user_setting table.
* Since we'll fetch several settings for each user, and since there aren't
* that many of them, fetch them all in one database query
*
* @param string $setting_name
* @param string $default
*
* @return string
*/
public function getPreference(string $setting_name, string $default = ''): string
{
return $this->preferences[$setting_name] ?? $default;
}

/**
* Update a setting for the user.
*
* @param string $setting_name
* @param string $setting_value
*
* @return void
*/
public function setPreference(string $setting_name, string $setting_value): void
{
if ($this->getPreference($setting_name) !== $setting_value) {
DB::table('user_setting')->updateOrInsert([
'user_id' => $this->user_id,
'setting_name' => $setting_name,
], [
'setting_value' => $setting_value,
]);

$this->preferences[$setting_name] = $setting_value;
}
}

/**
* Set the password of this user.
*
* @param string $password
*
* @return User
*/
public function setPassword(#[\SensitiveParameter] string $password): User
{
DB::table('user')
->where('user_id', '=', $this->user_id)
->update([
'password' => password_hash($password, PASSWORD_DEFAULT),
]);

return $this;
}

/**
* Validate a supplied password
*
* @param string $password
*
* @return bool
*/
public function checkPassword(#[\SensitiveParameter] string $password): bool
{
$password_hash = DB::table('user')
->where('user_id', '=', $this->id())
->value('password');

if (is_string($password_hash) && password_verify($password, $password_hash)) {
if (password_needs_rehash($password_hash, PASSWORD_DEFAULT)) {
$this->setPassword($password);
}

return true;
}

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');
$google2fa = new Google2FA();
if ($google2fa->verifyKey($secret, $code2fa)) {
return true;
}
return false;
}

/**
* A closure which will create an object from a database row.
*
* @return Closure(object):User
*/
public static function rowMapper(): Closure
{
return static fn (object $row): User => new self((int) $row->user_id, $row->user_name, $row->real_name, $row->email);
}
}
2 changes: 1 addition & 1 deletion app/Contracts/UserInterface.php
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ interface UserInterface
public const string PREF_IS_EMAIL_VERIFIED = 'verified';
public const string PREF_IS_VISIBLE_ONLINE = 'visibleonline';
public const string PREF_LANGUAGE = 'language';
public const string PREF_IS_STATUS_MFA = 'statusmfa';
public const string PREF_NEW_ACCOUNT_COMMENT = 'comment';
public const string PREF_TIMESTAMP_REGISTERED = 'reg_timestamp';
public const string PREF_TIMESTAMP_ACTIVE = 'sessiontime';
Expand Down Expand Up @@ -72,7 +73,6 @@ public function email(): string;
* @return string
*/
public function realName(): string;

/**
* The user‘s login name.
*
Expand Down
11 changes: 10 additions & 1 deletion app/Http/RequestHandlers/AccountEdit.php
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@
use Fisharebest\Webtrees\Registry;
use Fisharebest\Webtrees\Services\MessageService;
use Fisharebest\Webtrees\Services\ModuleService;
use Fisharebest\Webtrees\Services\QrcodeService;
use Fisharebest\Webtrees\Site;
use Fisharebest\Webtrees\Tree;
use Fisharebest\Webtrees\Validator;
use Psr\Http\Message\ResponseInterface;
Expand All @@ -47,14 +49,18 @@ class AccountEdit implements RequestHandlerInterface

private ModuleService $module_service;

private QrcodeService $qrcode_service;

/**
* @param MessageService $message_service
* @param ModuleService $module_service
* @param QrcodeService $qrcode_service
*/
public function __construct(MessageService $message_service, ModuleService $module_service)
public function __construct(MessageService $message_service, ModuleService $module_service, QrcodeService $qrcode_service)
{
$this->message_service = $message_service;
$this->module_service = $module_service;
$this->qrcode_service = $qrcode_service;
}

/**
Expand Down Expand Up @@ -83,6 +89,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');
Expand All @@ -93,6 +100,8 @@ 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,
'qrcode' => $this->qrcode_service->genQRcode($user),
'timezones' => $timezones,
'title' => $title,
'tree' => $tree,
Expand Down
Loading