From bf72c8a4ccca99351ad4d6cc6cfb33dc1c9e9423 Mon Sep 17 00:00:00 2001 From: Yuri Kuznetsov Date: Mon, 13 Sep 2021 13:09:09 +0300 Subject: [PATCH] 2fa-email --- .../Espo/Classes/Cleanup/TwoFactorCodes.php | 77 ++++ .../Espo/Controllers/TwoFactorEmail.php | 80 +++++ .../TwoFactor/Email/EmailLogin.php | 105 ++++++ .../TwoFactor/Email/EmailUserSetup.php | 72 ++++ .../Authentication/TwoFactor/Email/Util.php | 340 ++++++++++++++++++ application/Espo/Entities/TwoFactorCode.php | 67 ++++ .../Espo/Resources/i18n/en_US/Admin.json | 1 + .../Espo/Resources/i18n/en_US/Settings.json | 3 +- .../Espo/Resources/i18n/en_US/User.json | 5 +- .../app/authentication2FAMethods.json | 8 + .../Espo/Resources/metadata/app/cleanup.json | 3 + .../Resources/metadata/app/templates.json | 3 + .../metadata/entityDefs/TwoFactorCode.json | 56 +++ .../metadata/entityDefs/UserData.json | 3 + .../templates/twoFactorCode/en_US/body.tpl | 3 + .../templates/twoFactorCode/en_US/subject.tpl | 1 + application/Espo/Services/TwoFactorEmail.php | 71 ++++ .../user-security/modals/two-factor-email.tpl | 15 + .../user-security/modals/two-factor-email.js | 192 ++++++++++ 19 files changed, 1103 insertions(+), 2 deletions(-) create mode 100644 application/Espo/Classes/Cleanup/TwoFactorCodes.php create mode 100644 application/Espo/Controllers/TwoFactorEmail.php create mode 100644 application/Espo/Core/Authentication/TwoFactor/Email/EmailLogin.php create mode 100644 application/Espo/Core/Authentication/TwoFactor/Email/EmailUserSetup.php create mode 100644 application/Espo/Core/Authentication/TwoFactor/Email/Util.php create mode 100644 application/Espo/Entities/TwoFactorCode.php create mode 100644 application/Espo/Resources/metadata/entityDefs/TwoFactorCode.json create mode 100644 application/Espo/Resources/templates/twoFactorCode/en_US/body.tpl create mode 100644 application/Espo/Resources/templates/twoFactorCode/en_US/subject.tpl create mode 100644 application/Espo/Services/TwoFactorEmail.php create mode 100644 client/res/templates/user-security/modals/two-factor-email.tpl create mode 100644 client/src/views/user-security/modals/two-factor-email.js diff --git a/application/Espo/Classes/Cleanup/TwoFactorCodes.php b/application/Espo/Classes/Cleanup/TwoFactorCodes.php new file mode 100644 index 0000000000..1d627ce0c9 --- /dev/null +++ b/application/Espo/Classes/Cleanup/TwoFactorCodes.php @@ -0,0 +1,77 @@ +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); + } +} diff --git a/application/Espo/Controllers/TwoFactorEmail.php b/application/Espo/Controllers/TwoFactorEmail.php new file mode 100644 index 0000000000..fd675a8431 --- /dev/null +++ b/application/Espo/Controllers/TwoFactorEmail.php @@ -0,0 +1,80 @@ +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; + } +} diff --git a/application/Espo/Core/Authentication/TwoFactor/Email/EmailLogin.php b/application/Espo/Core/Authentication/TwoFactor/Email/EmailLogin.php new file mode 100644 index 0000000000..d366ab2c07 --- /dev/null +++ b/application/Espo/Core/Authentication/TwoFactor/Email/EmailLogin.php @@ -0,0 +1,105 @@ +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); + } +} diff --git a/application/Espo/Core/Authentication/TwoFactor/Email/EmailUserSetup.php b/application/Espo/Core/Authentication/TwoFactor/Email/EmailUserSetup.php new file mode 100644 index 0000000000..99f62548a6 --- /dev/null +++ b/application/Espo/Core/Authentication/TwoFactor/Email/EmailUserSetup.php @@ -0,0 +1,72 @@ +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); + } +} diff --git a/application/Espo/Core/Authentication/TwoFactor/Email/Util.php b/application/Espo/Core/Authentication/TwoFactor/Email/Util.php new file mode 100644 index 0000000000..351567d05c --- /dev/null +++ b/application/Espo/Core/Authentication/TwoFactor/Email/Util.php @@ -0,0 +1,340 @@ +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; + } +} diff --git a/application/Espo/Entities/TwoFactorCode.php b/application/Espo/Entities/TwoFactorCode.php new file mode 100644 index 0000000000..8459b39dd8 --- /dev/null +++ b/application/Espo/Entities/TwoFactorCode.php @@ -0,0 +1,67 @@ +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); + } +} diff --git a/application/Espo/Resources/i18n/en_US/Admin.json b/application/Espo/Resources/i18n/en_US/Admin.json index 54f8fdb1c7..a2efbcec90 100644 --- a/application/Espo/Resources/i18n/en_US/Admin.json +++ b/application/Espo/Resources/i18n/en_US/Admin.json @@ -317,6 +317,7 @@ "readable": "Readable" }, "templates": { + "twoFactorCode": "2FA Code", "accessInfo": "Access Info", "accessInfoPortal": "Access Info for Portals", "assignment": "Assignment", diff --git a/application/Espo/Resources/i18n/en_US/Settings.json b/application/Espo/Resources/i18n/en_US/Settings.json index 93258183ca..fa652ed41a 100644 --- a/application/Espo/Resources/i18n/en_US/Settings.json +++ b/application/Espo/Resources/i18n/en_US/Settings.json @@ -154,7 +154,8 @@ "EmailReceived": "Received emails" }, "auth2FAMethodList": { - "Totp": "TOTP" + "Totp": "TOTP", + "Email": "Email" } }, "tooltips": { diff --git a/application/Espo/Resources/i18n/en_US/User.json b/application/Espo/Resources/i18n/en_US/User.json index 11a88296a2..e43cdf74b0 100644 --- a/application/Espo/Resources/i18n/en_US/User.json +++ b/application/Espo/Resources/i18n/en_US/User.json @@ -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.", diff --git a/application/Espo/Resources/metadata/app/authentication2FAMethods.json b/application/Espo/Resources/metadata/app/authentication2FAMethods.json index d25df8b2fa..328293de1a 100644 --- a/application/Espo/Resources/metadata/app/authentication2FAMethods.json +++ b/application/Espo/Resources/metadata/app/authentication2FAMethods.json @@ -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" } } diff --git a/application/Espo/Resources/metadata/app/cleanup.json b/application/Espo/Resources/metadata/app/cleanup.json index 09db587b02..e031a00925 100644 --- a/application/Espo/Resources/metadata/app/cleanup.json +++ b/application/Espo/Resources/metadata/app/cleanup.json @@ -6,5 +6,8 @@ "webhookQueue": { "className": "Espo\\Classes\\Cleanup\\WebhookQueue", "order": 11 + }, + "twoFactorCodes": { + "className": "Espo\\Classes\\Cleanup\\TwoFactorCodes" } } diff --git a/application/Espo/Resources/metadata/app/templates.json b/application/Espo/Resources/metadata/app/templates.json index a9ae5eea3c..930adb5ff5 100644 --- a/application/Espo/Resources/metadata/app/templates.json +++ b/application/Espo/Resources/metadata/app/templates.json @@ -25,5 +25,8 @@ }, "passwordChangeLink": { "scope": "User" + }, + "twoFactorCode": { + "scope": "User" } } diff --git a/application/Espo/Resources/metadata/entityDefs/TwoFactorCode.json b/application/Espo/Resources/metadata/entityDefs/TwoFactorCode.json new file mode 100644 index 0000000000..c5158331d0 --- /dev/null +++ b/application/Espo/Resources/metadata/entityDefs/TwoFactorCode.json @@ -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" + ] + } + } +} diff --git a/application/Espo/Resources/metadata/entityDefs/UserData.json b/application/Espo/Resources/metadata/entityDefs/UserData.json index 1942b994eb..7a4cc0d368 100644 --- a/application/Espo/Resources/metadata/entityDefs/UserData.json +++ b/application/Espo/Resources/metadata/entityDefs/UserData.json @@ -15,6 +15,9 @@ "auth2FATotpSecret": { "type": "varchar", "maxLength": 32 + }, + "auth2FAEmailAddress": { + "type": "varchar" } }, "links": { diff --git a/application/Espo/Resources/templates/twoFactorCode/en_US/body.tpl b/application/Espo/Resources/templates/twoFactorCode/en_US/body.tpl new file mode 100644 index 0000000000..f37be9a078 --- /dev/null +++ b/application/Espo/Resources/templates/twoFactorCode/en_US/body.tpl @@ -0,0 +1,3 @@ +

Enter this code to log in to EspoCRM.

+ +

Code: {{code}}

diff --git a/application/Espo/Resources/templates/twoFactorCode/en_US/subject.tpl b/application/Espo/Resources/templates/twoFactorCode/en_US/subject.tpl new file mode 100644 index 0000000000..ab3fa58556 --- /dev/null +++ b/application/Espo/Resources/templates/twoFactorCode/en_US/subject.tpl @@ -0,0 +1 @@ +EspoCRM authentication code \ No newline at end of file diff --git a/application/Espo/Services/TwoFactorEmail.php b/application/Espo/Services/TwoFactorEmail.php new file mode 100644 index 0000000000..b8f10dfe16 --- /dev/null +++ b/application/Espo/Services/TwoFactorEmail.php @@ -0,0 +1,71 @@ +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); + } +} diff --git a/client/res/templates/user-security/modals/two-factor-email.tpl b/client/res/templates/user-security/modals/two-factor-email.tpl new file mode 100644 index 0000000000..15a6f8c17e --- /dev/null +++ b/client/res/templates/user-security/modals/two-factor-email.tpl @@ -0,0 +1,15 @@ +
+
+

+ {{translate 'choose2FaEmailAddress' category='messages' scope='User'}} +

+

+ +

+ +
+
+ +
{{{record}}}
diff --git a/client/src/views/user-security/modals/two-factor-email.js b/client/src/views/user-security/modals/two-factor-email.js new file mode 100644 index 0000000000..30153877a8 --- /dev/null +++ b/client/src/views/user-security/modals/two-factor-email.js @@ -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'); + }); + }, + + }); +});