2fa-email

This commit is contained in:
Yuri Kuznetsov
2021-09-13 13:09:09 +03:00
parent f6c8281c9e
commit bf72c8a4cc
19 changed files with 1103 additions and 2 deletions

View File

@@ -0,0 +1,77 @@
<?php
/************************************************************************
* This file is part of EspoCRM.
*
* EspoCRM - Open Source CRM application.
* Copyright (C) 2014-2021 Yurii Kuznietsov, Taras Machyshyn, Oleksii Avramenko
* Website: https://www.espocrm.com
*
* EspoCRM 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.
*
* EspoCRM 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 EspoCRM. If not, see http://www.gnu.org/licenses/.
*
* The interactive user interfaces in modified source and object code versions
* of this program must display Appropriate Legal Notices, as required under
* Section 5 of the GNU General Public License version 3.
*
* In accordance with Section 7(b) of the GNU General Public License version 3,
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
************************************************************************/
namespace Espo\Classes\Cleanup;
use Espo\Core\Cleanup\Cleanup;
use Espo\Core\Utils\Config;
use Espo\Core\Utils\DateTime as DateTimeUtil;
use Espo\ORM\EntityManager;
use Espo\Entities\TwoFactorCode;
use DateTime;
class TwoFactorCodes implements Cleanup
{
private const PERIOD = '5 days';
private $config;
private $entityManager;
public function __construct(Config $config, EntityManager $entityManager)
{
$this->config = $config;
$this->entityManager = $entityManager;
}
public function process(): void
{
$period = '-' . $this->config->get('cleanupTwoFactorCodesPeriod', self::PERIOD);
$from = (new DateTime())
->modify($period)
->format(DateTimeUtil::SYSTEM_DATE_TIME_FORMAT);
$query = $this->entityManager
->getQueryBuilder()
->delete()
->from(TwoFactorCode::ENTITY_TYPE)
->where([
'createdAt<' => $from,
])
->build();
$this->entityManager
->getQueryExecutor()
->execute($query);
}
}

View File

@@ -0,0 +1,80 @@
<?php
/************************************************************************
* This file is part of EspoCRM.
*
* EspoCRM - Open Source CRM application.
* Copyright (C) 2014-2021 Yurii Kuznietsov, Taras Machyshyn, Oleksii Avramenko
* Website: https://www.espocrm.com
*
* EspoCRM 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.
*
* EspoCRM 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 EspoCRM. If not, see http://www.gnu.org/licenses/.
*
* The interactive user interfaces in modified source and object code versions
* of this program must display Appropriate Legal Notices, as required under
* Section 5 of the GNU General Public License version 3.
*
* In accordance with Section 7(b) of the GNU General Public License version 3,
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
************************************************************************/
namespace Espo\Controllers;
use Espo\Core\Exceptions\Forbidden;
use Espo\Core\Exceptions\BadRequest;
use Espo\Core\Api\Request;
use Espo\Services\TwoFactorEmail as Service;
use Espo\Entities\User;
class TwoFactorEmail
{
private $service;
public function __construct(Service $service, User $user)
{
$this->service = $service;
$this->user = $user;
if (
!$this->user->isAdmin() &&
!$this->user->isRegular()
) {
throw new Forbidden();
}
}
public function postActionSendCode(Request $request): bool
{
$data = $request->getParsedBody();
$id = $data->id ?? null;
$emailAddress = $data->emailAddress ?? null;
if (!$id) {
throw new BadRequest("No 'id'.");
}
if (!$emailAddress) {
throw new BadRequest("No 'emailAddress'.");
}
if (!$this->user->isAdmin() && $id !== $this->user->getId()) {
throw new Forbidden();
}
$this->service->sendCode($id, $emailAddress);
return true;
}
}

View File

@@ -0,0 +1,105 @@
<?php
/************************************************************************
* This file is part of EspoCRM.
*
* EspoCRM - Open Source CRM application.
* Copyright (C) 2014-2021 Yurii Kuznietsov, Taras Machyshyn, Oleksii Avramenko
* Website: https://www.espocrm.com
*
* EspoCRM 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.
*
* EspoCRM 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 EspoCRM. If not, see http://www.gnu.org/licenses/.
*
* The interactive user interfaces in modified source and object code versions
* of this program must display Appropriate Legal Notices, as required under
* Section 5 of the GNU General Public License version 3.
*
* In accordance with Section 7(b) of the GNU General Public License version 3,
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
************************************************************************/
namespace Espo\Core\Authentication\TwoFactor\Email;
use Espo\ORM\EntityManager;
use Espo\Entities\User;
use Espo\Entities\UserData;
use Espo\Repositories\UserData as UserDataRepository;
use Espo\Core\Authentication\TwoFactor\Login;
use Espo\Core\Authentication\Result;
use Espo\Core\Authentication\ResultData;
use Espo\Core\Authentication\FailReason;
use Espo\Core\Api\Request;
class EmailLogin implements Login
{
private $entityManager;
private $util;
public function __construct(EntityManager $entityManager, Util $util)
{
$this->entityManager = $entityManager;
$this->util = $util;
}
public function login(Result $result, Request $request): Result
{
$code = $request->getHeader('Espo-Authorization-Code');
if (!$code) {
$this->util->sendCode($result->getLoggedUser());
return Result::secondStepRequired($result->getUser(), $this->getLoginData());
}
$loggedUser = $result->getLoggedUser();
if ($this->verifyCode($loggedUser, $code)) {
return $result;
}
return Result::fail(FailReason::CODE_NOT_VERIFIED);
}
private function getLoginData(): ResultData
{
return ResultData::createWithMessage('enterCodeSentInEmail');
}
private function verifyCode(User $user, string $code): bool
{
$userData = $this->getUserDataRepository()->getByUserId($user->getId());
if (!$userData) {
return false;
}
if (!$userData->get('auth2FA')) {
return false;
}
if ($userData->get('auth2FAMethod') !== 'Email') {
return false;
}
return $this->util->verifyCode($user, $code);
}
private function getUserDataRepository(): UserDataRepository
{
return $this->entityManager->getRepository(UserData::ENTITY_TYPE);
}
}

View File

@@ -0,0 +1,72 @@
<?php
/************************************************************************
* This file is part of EspoCRM.
*
* EspoCRM - Open Source CRM application.
* Copyright (C) 2014-2021 Yurii Kuznietsov, Taras Machyshyn, Oleksii Avramenko
* Website: https://www.espocrm.com
*
* EspoCRM 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.
*
* EspoCRM 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 EspoCRM. If not, see http://www.gnu.org/licenses/.
*
* The interactive user interfaces in modified source and object code versions
* of this program must display Appropriate Legal Notices, as required under
* Section 5 of the GNU General Public License version 3.
*
* In accordance with Section 7(b) of the GNU General Public License version 3,
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
************************************************************************/
namespace Espo\Core\Authentication\TwoFactor\Email;
use Espo\Entities\User;
use Espo\Core\Authentication\TwoFactor\UserSetup;
use Espo\Core\Exceptions\Error;
use stdClass;
class EmailUserSetup implements UserSetup
{
private $util;
public function __construct(Util $util)
{
$this->util = $util;
}
public function getData(User $user): stdClass
{
return (object) [
'emailAddressList' => $user->getEmailAddressGroup()->getAddressList(),
];
}
public function verifyData(User $user, stdClass $payloadData): bool
{
$code = $payloadData->code ?? null;
if ($code === null) {
throw new Error("No code.");
}
$codeModified = str_replace(' ', '', trim($code));
if (!$codeModified) {
return false;
}
return $this->util->verifyCode($user, $codeModified);
}
}

View File

@@ -0,0 +1,340 @@
<?php
/************************************************************************
* This file is part of EspoCRM.
*
* EspoCRM - Open Source CRM application.
* Copyright (C) 2014-2021 Yurii Kuznietsov, Taras Machyshyn, Oleksii Avramenko
* Website: https://www.espocrm.com
*
* EspoCRM 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.
*
* EspoCRM 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 EspoCRM. If not, see http://www.gnu.org/licenses/.
*
* The interactive user interfaces in modified source and object code versions
* of this program must display Appropriate Legal Notices, as required under
* Section 5 of the GNU General Public License version 3.
*
* In accordance with Section 7(b) of the GNU General Public License version 3,
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
************************************************************************/
namespace Espo\Core\Authentication\TwoFactor\Email;
use Espo\Core\Exceptions\Error;
use Espo\Core\Exceptions\Forbidden;
use Espo\Core\Utils\Config;
use Espo\Core\Mail\EmailSender;
use Espo\Core\Mail\EmailFactory;
use Espo\Core\Utils\TemplateFileManager;
use Espo\Core\Htmlizer\HtmlizerFactory;
use Espo\Core\Field\DateTime;
use Espo\ORM\EntityManager;
use Espo\ORM\Query\Part\Condition as Cond;
use Espo\Entities\User;
use Espo\Entities\Email;
use Espo\Entities\TwoFactorCode;
use Espo\Entities\UserData;
use Espo\Repositories\UserData as UserDataRepository;
use const STR_PAD_LEFT;
class Util
{
/**
* A lifetime of a code.
*/
private const CODE_LIFETIME_PERIOD = '10 minutes';
/*
* A max number of attempts to try a single code.
*/
private const CODE_ATTEMPTS_COUNT = 5;
/**
* A length of a code.
*/
private const CODE_LENGTH = 7;
/**
* A max number of codes tried by a user in a period defined by `CODE_LIMIT_PERIOD`.
*/
private const CODE_LIMIT = 5;
/**
* A period for limiting trying to too many codes.
*/
private const CODE_LIMIT_PERIOD = '10 minutes';
private $entityManager;
private $config;
private $emailSender;
private $templateFileManager;
private $htmlizerFactory;
private $emailFactory;
public function __construct(
EntityManager $entityManager,
Config $config,
EmailSender $emailSender,
TemplateFileManager $templateFileManager,
HtmlizerFactory $htmlizerFactory,
EmailFactory $emailFactory
) {
$this->entityManager = $entityManager;
$this->config = $config;
$this->emailSender = $emailSender;
$this->templateFileManager = $templateFileManager;
$this->htmlizerFactory = $htmlizerFactory;
$this->emailFactory = $emailFactory;
}
public function storeEmailAddress(User $user, string $emailAddress): void
{
$this->checkEmailAddressIsUsers($user, $emailAddress);
$userData = $this->getUserDataRepository()->getByUserId($user->getId());
$userData->set('auth2FAEmailAddress', $emailAddress);
$this->entityManager->saveEntity($userData);
}
public function verifyCode(User $user, string $code): bool
{
$codeEntity = $this->findCodeEntity($user);
if (!$codeEntity) {
return false;
}
if ($codeEntity->getAttemptsLeft() <= 1) {
$this->decrementAttemptsLeft($codeEntity);
$this->inactivateExistingCodeRecords($user);
return false;
}
if ($codeEntity->getCode() !== $code) {
$this->decrementAttemptsLeft($codeEntity);
return false;
}
if (!$this->isCodeValidByLifetime($codeEntity)) {
$this->inactivateExistingCodeRecords($user);
return false;
}
$this->inactivateExistingCodeRecords($user);
return true;
}
public function sendCode(User $user, ?string $emailAddress = null): void
{
if ($emailAddress === null) {
$emailAddress = $this->getEmailAddress($user);
}
$this->checkEmailAddressIsUsers($user, $emailAddress);
$this->checkCodeLimit($user);
$code = $this->generateCode();
$this->inactivateExistingCodeRecords($user);
$this->createCodeRecord($user, $code);
$email = $this->createEmail($user, $code, $emailAddress);
$this->emailSender->send($email);
}
private function isCodeValidByLifetime(TwoFactorCode $codeEntity): bool
{
$period = $this->config->get('auth2FAEmailCodeLifetimePeriod') ?? self::CODE_LIFETIME_PERIOD;
$validUntil = $codeEntity->getCreatedAt()->modify($period);
if (DateTime::createNow()->diff($validUntil)->invert) {
return false;
}
return true;
}
private function findCodeEntity(User $user): ?TwoFactorCode
{
return $this->entityManager
->getRDBRepository(TwoFactorCode::ENTITY_TYPE)
->where([
'method' => 'Email',
'userId' => $user->getId(),
'isActive' => true,
])
->findOne();
}
private function getEmailAddress(User $user): string
{
$userData = $this->getUserDataRepository()->getByUserId($user->getId());
if (!$userData) {
throw new Error("UserData not found.");
}
$emailAddress = $userData->get('auth2FAEmailAddress');
if ($emailAddress) {
return $emailAddress;
}
if ($user->getEmailAddressGroup()->getCount() === 0) {
throw new Error("User does not have email address.");
}
return $user->getEmailAddressGroup()->getPrimary()->getAddress();
}
private function checkEmailAddressIsUsers(User $user, string $emailAddress): void
{
$userAddressList = array_map(
function (string $item) {
return strtolower($item);
},
$user->getEmailAddressGroup()->getAddressList()
);
if (!in_array(strtolower($emailAddress), $userAddressList)) {
throw new Forbidden("Email address is not one of user's.");
}
}
private function checkCodeLimit(User $user): void
{
$limit = $this->config->get('auth2FAEmailCodeLimit') ?? self::CODE_LIMIT;
$period = $this->config->get('auth2FAEmailCodeLimitPeriod') ?? self::CODE_LIMIT_PERIOD;
$from = DateTime::createNow()
->modify('-' . $period)
->getString();
$count = $this->entityManager
->getRDBRepository(TwoFactorCode::ENTITY_TYPE)
->where(
Cond::and(
Cond::equal(Cond::column('method'), 'Email'),
Cond::equal(Cond::column('userId'), $user->getId()),
Cond::greaterOrEqual(Cond::column('createdAt'), $from),
Cond::lessOrEqual(Cond::column('attemptsLeft'), 0),
)
)
->count();
if ($count >= $limit) {
throw new Forbidden("Max code count exceeded.");
}
}
private function generateCode(): string
{
$codeLength = $this->config->get('auth2FAEmailCodeLength') ?? self::CODE_LENGTH;
$max = pow(10, $codeLength) - 1;
return str_pad(
(string) random_int(0, $max),
$codeLength,
'0',
STR_PAD_LEFT
);
}
private function createEmail(User $user, string $code, string $emailAddress): Email
{
$subjectTpl = $this->templateFileManager->getTemplate('twoFactorCode', 'subject');
$bodyTpl = $this->templateFileManager->getTemplate('twoFactorCode', 'body');
$htmlizer = $this->htmlizerFactory->create();
$data = [
'code' => $code,
];
$subject = $htmlizer->render($user, $subjectTpl, null, $data, true);
$body = $htmlizer->render($user, $bodyTpl, null, $data, true);
$email = $this->emailFactory->create();
$email->setSubject($subject);
$email->setBody($body);
$email->addToAddress($emailAddress);
return $email;
}
private function inactivateExistingCodeRecords(User $user): void
{
$query = $this->entityManager
->getQueryBuilder()
->update()
->in(TwoFactorCode::ENTITY_TYPE)
->where([
'userId' => $user->getId(),
'method' => 'Email',
])
->set([
'isActive' => false,
])
->build();
$this->entityManager
->getQueryExecutor()
->execute($query);
}
private function createCodeRecord(User $user, string $code): void
{
$this->entityManager->createEntity(TwoFactorCode::ENTITY_TYPE, [
'code' => $code,
'userId' => $user->getId(),
'method' => 'Email',
'attemptsLeft' => $this->getCodeAttemptsCount(),
]);
}
private function getUserDataRepository(): UserDataRepository
{
return $this->entityManager->getRepository(UserData::ENTITY_TYPE);
}
private function decrementAttemptsLeft(TwoFactorCode $codeEntity): void
{
$codeEntity->decrementAttemptsLeft();
$this->entityManager->saveEntity($codeEntity);
}
private function getCodeAttemptsCount(): int
{
return $this->config->get('auth2FAEmailCodeAttemptsCount') ?? self::CODE_ATTEMPTS_COUNT;
}
}

View File

@@ -0,0 +1,67 @@
<?php
/************************************************************************
* This file is part of EspoCRM.
*
* EspoCRM - Open Source CRM application.
* Copyright (C) 2014-2021 Yurii Kuznietsov, Taras Machyshyn, Oleksii Avramenko
* Website: https://www.espocrm.com
*
* EspoCRM 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.
*
* EspoCRM 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 EspoCRM. If not, see http://www.gnu.org/licenses/.
*
* The interactive user interfaces in modified source and object code versions
* of this program must display Appropriate Legal Notices, as required under
* Section 5 of the GNU General Public License version 3.
*
* In accordance with Section 7(b) of the GNU General Public License version 3,
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
************************************************************************/
namespace Espo\Entities;
use Espo\Core\Field\DateTime;
class TwoFactorCode extends \Espo\Core\ORM\Entity
{
public const ENTITY_TYPE = 'TwoFactorCode';
public function isActive(): bool
{
return $this->get('isActive');
}
public function getCreatedAt(): DateTime
{
return $this->getValueObject('createdAt');
}
public function getCode(): string
{
return $this->get('code');
}
public function getAttemptsLeft(): int
{
return $this->get('attemptsLeft');
}
public function setInactive(): void
{
$this->set('isActive', false);
}
public function decrementAttemptsLeft(): void
{
$this->set('attemptsLeft', $this->getAttemptsLeft() - 1);
}
}

View File

@@ -317,6 +317,7 @@
"readable": "Readable"
},
"templates": {
"twoFactorCode": "2FA Code",
"accessInfo": "Access Info",
"accessInfoPortal": "Access Info for Portals",
"assignment": "Assignment",

View File

@@ -154,7 +154,8 @@
"EmailReceived": "Received emails"
},
"auth2FAMethodList": {
"Totp": "TOTP"
"Totp": "TOTP",
"Email": "Email"
}
},
"tooltips": {

View File

@@ -77,7 +77,8 @@
"Security": "Security",
"Reset 2FA": "Reset 2FA",
"Code": "Code",
"Secret": "Secret"
"Secret": "Secret",
"Send Code": "Send Code"
},
"tooltips": {
"defaultTeam": "All records created by this user will be related to this team by default.",
@@ -108,6 +109,8 @@
"userNameExists": "User Name already exists",
"wrongCode": "Wrong code",
"codeIsRequired": "Code is required",
"choose2FaEmailAddress": "Select an email address that will be used for 2FA. It's highly recommended to use a non-primary email address.",
"enterCodeSentInEmail": "Enter the code sent to your email address.",
"enterTotpCode": "Enter a code from your authenticator app.",
"verifyTotpCode": "Scan the QR-code with your mobile authenticator app. If you have a trouble with scanning, you can enter the secret manually. After that you will see a 6-digit code in your application. Enter this code in the field below.",
"generateAndSendNewPassword": "A new password will be generated and sent to the user's email address.",

View File

@@ -6,5 +6,13 @@
"userApplyView": "views/user-security/modals/totp",
"loginClassName": "Espo\\Core\\Authentication\\TwoFactor\\Totp\\TotpLogin",
"userSetupClassName": "Espo\\Core\\Authentication\\TwoFactor\\Totp\\TotpUserSetup"
},
"Email": {
"settings": {
"isAvailable": true
},
"userApplyView": "views/user-security/modals/two-factor-email",
"loginClassName": "Espo\\Core\\Authentication\\TwoFactor\\Email\\EmailLogin",
"userSetupClassName": "Espo\\Core\\Authentication\\TwoFactor\\Email\\EmailUserSetup"
}
}

View File

@@ -6,5 +6,8 @@
"webhookQueue": {
"className": "Espo\\Classes\\Cleanup\\WebhookQueue",
"order": 11
},
"twoFactorCodes": {
"className": "Espo\\Classes\\Cleanup\\TwoFactorCodes"
}
}

View File

@@ -25,5 +25,8 @@
},
"passwordChangeLink": {
"scope": "User"
},
"twoFactorCode": {
"scope": "User"
}
}

View File

@@ -0,0 +1,56 @@
{
"fields": {
"code": {
"type": "varchar",
"maxLength": 100
},
"method": {
"type": "varchar",
"maxLength": 100
},
"attemptsLeft": {
"type": "int"
},
"isActive": {
"type": "bool",
"default": true
},
"createdAt": {
"type": "datetime",
"readOnly": true
}
},
"links": {
"user": {
"type": "belongsTo",
"entity": "User"
}
},
"indexes": {
"createdAt": {
"columns": [
"createdAt"
]
},
"userIdMethod": {
"columns": [
"userId",
"method"
]
},
"userIdMethodIsActive": {
"columns": [
"userId",
"method",
"isActive"
]
},
"userIdMethodCreatedAt": {
"columns": [
"userId",
"method",
"createdAt"
]
}
}
}

View File

@@ -15,6 +15,9 @@
"auth2FATotpSecret": {
"type": "varchar",
"maxLength": 32
},
"auth2FAEmailAddress": {
"type": "varchar"
}
},
"links": {

View File

@@ -0,0 +1,3 @@
<p>Enter this code to log in to EspoCRM.</p>
<p>Code: <strong>{{code}}</strong></p>

View File

@@ -0,0 +1 @@
EspoCRM authentication code

View File

@@ -0,0 +1,71 @@
<?php
/************************************************************************
* This file is part of EspoCRM.
*
* EspoCRM - Open Source CRM application.
* Copyright (C) 2014-2021 Yurii Kuznietsov, Taras Machyshyn, Oleksii Avramenko
* Website: https://www.espocrm.com
*
* EspoCRM 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.
*
* EspoCRM 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 EspoCRM. If not, see http://www.gnu.org/licenses/.
*
* The interactive user interfaces in modified source and object code versions
* of this program must display Appropriate Legal Notices, as required under
* Section 5 of the GNU General Public License version 3.
*
* In accordance with Section 7(b) of the GNU General Public License version 3,
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
************************************************************************/
namespace Espo\Services;
use Espo\Core\Exceptions\Forbidden;
use Espo\Core\Exceptions\NotFound;
use Espo\Core\Authentication\TwoFactor\Email\Util;
use Espo\ORM\EntityManager;
use Espo\Entities\User;
class TwoFactorEmail
{
private $util;
private $user;
private $entityManager;
public function __construct(Util $util, User $user, EntityManager $entityManager)
{
$this->util = $util;
$this->user = $user;
$this->entityManager = $entityManager;
}
public function sendCode(string $userId, string $emailAddress): void
{
if (!$this->user->isAdmin() && $userId !== $this->user->getId()) {
throw new Forbidden();
}
$user = $this->entityManager->getEntity(User::ENTITY_TYPE, $userId);
if (!$user) {
throw new NotFound();
}
$this->util->sendCode($user, $emailAddress);
$this->util->storeEmailAddress($user, $emailAddress);
}
}

View File

@@ -0,0 +1,15 @@
<div class="panel no-side-margin">
<div class="panel-body">
<p class="p-info">
{{translate 'choose2FaEmailAddress' category='messages' scope='User'}}
</p>
<p class="p-button">
<button class="btn btn-default" data-action="sendCode">{{translate 'Send Code' scope='User'}}</button>
</p>
<p class="p-info-after hidden">
{{translate 'enterCodeSentInEmail' category='messages' scope='User'}}
</p>
</div>
</div>
<div class="record">{{{record}}}</div>

View File

@@ -0,0 +1,192 @@
/************************************************************************
* This file is part of EspoCRM.
*
* EspoCRM - Open Source CRM application.
* Copyright (C) 2014-2021 Yurii Kuznietsov, Taras Machyshyn, Oleksii Avramenko
* Website: https://www.espocrm.com
*
* EspoCRM 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.
*
* EspoCRM 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 EspoCRM. If not, see http://www.gnu.org/licenses/.
*
* The interactive user interfaces in modified source and object code versions
* of this program must display Appropriate Legal Notices, as required under
* Section 5 of the GNU General Public License version 3.
*
* In accordance with Section 7(b) of the GNU General Public License version 3,
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
************************************************************************/
define('views/user-security/modals/two-factor-email',
['views/modal', 'model'],
function (Dep, Model) {
return Dep.extend({
template: 'user-security/modals/two-factor-email',
className: 'dialog dialog-record',
events: {
'click [data-action="sendCode"]': function () {
this.actionSendCode();
},
},
setup: function () {
this.buttonList = [
{
name: 'apply',
label: 'Apply',
style: 'danger',
hidden: true,
},
{
name: 'cancel',
label: 'Cancel',
}
];
this.headerHtml = false;
let codeLength = this.getConfig().get('auth2FAEmailCodeLength') || 7;
let model = new Model();
model.name = 'UserSecurity';
model.set('emailAddress', null);
model.setDefs({
fields: {
'code': {
type: 'varchar',
required: true,
maxLength: codeLength,
},
'emailAddress': {
type: 'enum',
required: true,
},
}
});
this.internalModel = model;
this.wait(
Espo.Ajax
.postRequest('UserSecurity/action/getTwoFactorUserSetupData', {
id: this.model.id,
password: this.model.get('password'),
auth2FAMethod: this.model.get('auth2FAMethod'),
reset: this.options.reset,
})
.then(data => {
this.emailAddressList = data.emailAddressList;
this.createView('record', 'views/record/edit-for-modal', {
scope: 'None',
el: this.getSelector() + ' .record',
model: model,
detailLayout: [
{
rows: [
[
{
name: 'emailAddress',
labelText: this.translate('emailAddress', 'fields', 'User'),
},
false
],
[
{
name: 'code',
labelText: this.translate('Code', 'labels', 'User'),
},
false
],
]
}
],
}, view => {
view.setFieldOptionList('emailAddress', this.emailAddressList);
if (this.emailAddressList.length) {
model.set('emailAddress', this.emailAddressList[0]);
}
view.hideField('code');
});
})
);
},
afterRender: function () {
this.$sendCode = this.$el.find('[data-action="sendCode"]');
this.$pInfo = this.$el.find('p.p-info');
this.$pButton = this.$el.find('p.p-button');
this.$pInfoAfter = this.$el.find('p.p-info-after');
},
actionSendCode: function () {
this.$sendCode.attr('disabled', 'disabled').addClass('disabled');
Espo.Ajax
.postRequest('TwoFactorEmail/action/sendCode', {
id: this.model.id,
emailAddress: this.internalModel.get('emailAddress'),
})
.then(() => {
this.showButton('apply');
this.$pInfo.addClass('hidden');
this.$pButton.addClass('hidden');
this.$pInfoAfter.removeClass('hidden');
this.getView('record').setFieldReadOnly('emailAddress');
this.getView('record').showField('code');
})
.catch(() => {
this.$sendCode.removeAttr('disabled').removeClass('disabled');
});
},
actionApply: function () {
let data = this.getView('record').processFetch();
if (!data) {
return;
}
this.model.set('code', data.code);
this.hideButton('apply');
this.hideButton('cancel');
Espo.Ui.notify(this.translate('pleaseWait', 'messages'));
this.model
.save()
.then(() => {
Espo.Ui.notify(false);
this.trigger('done');
})
.catch(() => {
this.showButton('apply');
this.showButton('cancel');
});
},
});
});