diff --git a/application/Espo/Controllers/User.php b/application/Espo/Controllers/User.php index 5ab63e5394..a4f77beef7 100644 --- a/application/Espo/Controllers/User.php +++ b/application/Espo/Controllers/User.php @@ -118,6 +118,14 @@ class User extends \Espo\Core\Controllers\Record return $this->getRecordService()->generateNewApiKeyForEntity($data->id)->getValueMap(); } + public function postActionGenerateNewPassword($params, $data, $request) + { + if (empty($data->id)) throw new BadRequest(); + if (!$this->getUser()->isAdmin()) throw new Forbidden(); + $this->getRecordService()->generateNewPasswordForUser($data->id); + return true; + } + public function beforeCreateLink() { if (!$this->getUser()->isAdmin()) throw new Forbidden(); diff --git a/application/Espo/Core/Utils/Util.php b/application/Espo/Core/Utils/Util.php index 8d61b9b35a..293a90bf42 100644 --- a/application/Espo/Core/Utils/Util.php +++ b/application/Espo/Core/Utils/Util.php @@ -809,4 +809,46 @@ class Util return $url; } + + public static function generatePassword(int $letters = 5, int $numbers = 3, int $either = 0) + { + $chars = [ + 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz', + '0123456789', + 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789', + ]; + + $shuffle = function ($array) { + $currentIndex = count($array); + while (0 !== $currentIndex) { + $rand = (0 + (1 - 0) * (mt_rand() / mt_getrandmax())); + $randomIndex = intval(floor($rand * $currentIndex)); + $currentIndex -= 1; + $temporaryValue = $array[$currentIndex]; + $array[$currentIndex] = $array[$randomIndex]; + $array[$randomIndex] = $temporaryValue; + } + return $array; + }; + + $array = []; + + foreach ([$letters, $numbers, $either] as $i => $len) { + $set = $chars[$i]; + $subArray = []; + + $j = 0; + while ($j < $len) { + $rand = (0 + (1 - 0) * (mt_rand() / mt_getrandmax())); + $index = intval(floor($rand * strlen($set))); + $subArray[] = $set[$index]; + $j++; + } + + $array = array_merge($array, $subArray); + } + + + return implode('', $shuffle($array)); + } } diff --git a/application/Espo/Resources/i18n/en_US/User.json b/application/Espo/Resources/i18n/en_US/User.json index e4cbb4171a..104eb1c317 100644 --- a/application/Espo/Resources/i18n/en_US/User.json +++ b/application/Espo/Resources/i18n/en_US/User.json @@ -69,6 +69,7 @@ "Create Portal User": "Create Portal User", "Proceed w/o Contact": "Proceed w/o Contact", "Generate New API Key": "Generate New API Key", + "Generate New Password": "Generate New Password", "Code": "Code", "Back to login form": "Back to login form", "Security": "Security", @@ -101,6 +102,7 @@ "codeIsRequired": "Code is required", "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.", "securityResetConfimation": "Are you sure you want to reset security settings?" }, "options": { diff --git a/application/Espo/Services/User.php b/application/Espo/Services/User.php index e990b8d4f1..06a152bce6 100644 --- a/application/Espo/Services/User.php +++ b/application/Espo/Services/User.php @@ -363,6 +363,52 @@ class User extends Record return $entity; } + public function generateNewPasswordForUser(string $id) + { + $user = $this->getEntity($id); + if (!$user) throw new NotFound(); + + if (!$this->getUser()->isAdmin()) throw new Forbidden(); + if ($user->isApi()) throw new Forbidden(); + if ($user->isSuperAdmin()) throw new Forbidden(); + if ($user->isSystem()) throw new Forbidden(); + + if (!$user->get('emailAddress')) { + throw new Forbidden("Generate new password: Can't process because user desn't have email address."); + } + + if (!$this->getConfig()->get('smtpServer') && !$this->getConfig()->get('internalSmtpServer')) { + throw new Forbidden("Generate new password: Can't process because SMTP is not configured."); + } + + $length = $this->getConfig()->get('passwordStrengthLength'); + $letterCount = $this->getConfig()->get('passwordStrengthLetterCount'); + $numberCount = $this->getConfig()->get('passwordStrengthNumberCount'); + + $generateLength = $this->getConfig()->get('passwordGenerateLength', 8); + $generateLetterCount = $this->getConfig()->get('passwordGenerateLetterCount', 5); + $generateNumberCount = $this->getConfig()->get('passwordGenerateNumberCount', 2); + + $length = is_null($length) ? $generateLength : $length; + $letterCount = is_null($letterCount) ? $generateLetterCount : $letterCount; + $numberCount = is_null($letterCount) ? $generateNumberCount : $numberCount; + + if ($length < $generateLength) $length = $generateLength; + if ($letterCount < $generateLetterCount) $letterCount = $generateLetterCount; + if ($numberCount < $generateNumberCount) $numberCount = $generateNumberCount; + + $otherCount = $length - ($letterCount + $numberCount); + if ($otherCount < 0) $otherCount = 0; + + $password = Util::generatePassword($letterCount, $numberCount, $otherCount); + + $this->sendPassword($user, $password); + + $passwordHash = new \Espo\Core\Utils\PasswordHash($this->getConfig()); + $user->set('password', $passwordHash->hash($password)); + $this->getEntityManager()->saveEntity($user); + } + protected function getInternalUserCount() { return $this->getEntityManager()->getRepository('User')->where([ diff --git a/client/src/views/user/record/detail.js b/client/src/views/user/record/detail.js index 07822c8723..e99352b863 100644 --- a/client/src/views/user/record/detail.js +++ b/client/src/views/user/record/detail.js @@ -26,7 +26,7 @@ * these Appropriate Legal Notices must retain the display of the "EspoCRM" word. ************************************************************************/ -Espo.define('views/user/record/detail', 'views/record/detail', function (Dep) { +define('views/user/record/detail', 'views/record/detail', function (Dep) { return Dep.extend({ @@ -76,6 +76,31 @@ Espo.define('views/user/record/detail', 'views/record/detail', function (Dep) { }); } + if ( + this.getUser().isAdmin() + && + (this.model.isRegular() || this.model.isAdmin() || this.model.isPortal()) + && + !this.model.isSuperAdmin() + ) { + this.addDropdownItem({ + name: 'generateNewPassword', + label: 'Generate New Password', + action: 'generateNewPassword', + hidden: !this.model.get('emailAddress') || !this.getConfig().get('smtpServer'), + }); + + if (!this.model.get('emailAddress') && this.getConfig().get('smtpServer')) { + this.listenTo(this.model, 'sync', function () { + if (this.model.get('emailAddress')) { + this.showActionItem('generateNewPassword'); + } else { + this.hideActionItem('generateNewPassword'); + } + }, this); + } + } + if (this.model.isPortal() || this.model.isApi()) { this.hideActionItem('duplicate'); } @@ -307,5 +332,22 @@ Espo.define('views/user/record/detail', 'views/record/detail', function (Dep) { }, this); }, + actionGenerateNewPassword: function () { + this.confirm( + this.translate('generateAndSendNewPassword', 'messages', 'User') + ).then( + function () { + Espo.Ui.notify(this.translate('pleaseWait', 'messages')); + Espo.Ajax.postRequest('User/action/generateNewPassword', { + id: this.model.id + }).then( + function () { + Espo.Ui.success(this.translate('Done')); + }.bind(this) + ); + }.bind(this) + ); + }, + }); });