mirror of
https://github.com/espocrm/espocrm.git
synced 2026-06-28 06:56:05 +00:00
password change link
This commit is contained in:
@@ -32,6 +32,7 @@ namespace Espo\Classes\FieldProcessing\Portal;
|
||||
use Espo\ORM\Entity;
|
||||
|
||||
use Espo\Repositories\Portal as PortalRepository;
|
||||
use Espo\Entities\Portal;
|
||||
|
||||
use Espo\Core\{
|
||||
FieldProcessing\Loader,
|
||||
@@ -50,6 +51,8 @@ class UrlLoader implements Loader
|
||||
|
||||
public function process(Entity $entity, Params $params): void
|
||||
{
|
||||
/** @var Portal $entity */
|
||||
|
||||
$this->getPortalRepository()->loadUrlField($entity);
|
||||
}
|
||||
|
||||
|
||||
@@ -156,6 +156,23 @@ class User extends Record
|
||||
return true;
|
||||
}
|
||||
|
||||
public function postActionSendPasswordChangeLink(Request $request): bool
|
||||
{
|
||||
if (!$this->user->isAdmin()) {
|
||||
throw new Forbidden();
|
||||
}
|
||||
|
||||
$id = $request->getParsedBody()->id ?? null;
|
||||
|
||||
if (!$id) {
|
||||
throw new BadRequest();
|
||||
}
|
||||
|
||||
$this->getUserService()->sendPasswordChangeLink($id);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public function postActionCreateLink(Request $request): bool
|
||||
{
|
||||
if (!$this->user->isAdmin()) {
|
||||
|
||||
@@ -32,11 +32,16 @@ namespace Espo\Core\Password;
|
||||
use Espo\Core\Utils\Util;
|
||||
use Espo\Entities\User;
|
||||
use Espo\Entities\PasswordChangeRequest;
|
||||
use Espo\Entities\Portal;
|
||||
|
||||
use Espo\Repositories\Portal as PortalRepository;
|
||||
|
||||
use Espo\Core\Exceptions\Forbidden;
|
||||
use Espo\Core\Exceptions\NotFound;
|
||||
use Espo\Core\Exceptions\Error;
|
||||
|
||||
use Espo\Core\Field\DateTime;
|
||||
|
||||
use Espo\Core\{
|
||||
ORM\EntityManager,
|
||||
Utils\Config,
|
||||
@@ -47,46 +52,30 @@ use Espo\Core\{
|
||||
Job\QueueName,
|
||||
};
|
||||
|
||||
use DateTime;
|
||||
|
||||
class Recovery
|
||||
{
|
||||
/**
|
||||
* Milliseconds.
|
||||
*/
|
||||
const REQUEST_DELAY = 3000;
|
||||
private const REQUEST_DELAY = 3000;
|
||||
|
||||
const REQUEST_LIFETIME = '3 hours';
|
||||
private const REQUEST_LIFETIME = '3 hours';
|
||||
|
||||
/**
|
||||
* @var EntityManager
|
||||
*/
|
||||
protected $entityManager;
|
||||
private const NEW_USER_REQUEST_LIFETIME = '2 days';
|
||||
|
||||
/**
|
||||
* @var Config
|
||||
*/
|
||||
protected $config;
|
||||
private const EXISTING_USER_REQUEST_LIFETIME = '2 days';
|
||||
|
||||
/**
|
||||
* @var EmailSender
|
||||
*/
|
||||
protected $emailSender;
|
||||
protected EntityManager $entityManager;
|
||||
|
||||
/**
|
||||
* @var HtmlizerFactory
|
||||
*/
|
||||
protected $htmlizerFactory;
|
||||
protected Config $config;
|
||||
|
||||
/**
|
||||
* @var TemplateFileManager
|
||||
*/
|
||||
protected $templateFileManager;
|
||||
protected EmailSender $emailSender;
|
||||
|
||||
/**
|
||||
* @var Log
|
||||
*/
|
||||
private $log;
|
||||
protected HtmlizerFactory $htmlizerFactory;
|
||||
|
||||
protected TemplateFileManager $templateFileManager;
|
||||
|
||||
private Log $log;
|
||||
|
||||
public function __construct(
|
||||
EntityManager $entityManager,
|
||||
@@ -107,13 +96,12 @@ class Recovery
|
||||
public function getRequest(string $id): PasswordChangeRequest
|
||||
{
|
||||
$config = $this->config;
|
||||
$em = $this->entityManager;
|
||||
|
||||
if ($config->get('passwordRecoveryDisabled')) {
|
||||
throw new Forbidden("Password recovery: Disabled.");
|
||||
}
|
||||
|
||||
$request = $em
|
||||
$request = $this->entityManager
|
||||
->getRDBRepository('PasswordChangeRequest')
|
||||
->where([
|
||||
'requestId' => $id,
|
||||
@@ -132,11 +120,9 @@ class Recovery
|
||||
return $request;
|
||||
}
|
||||
|
||||
public function removeRequest(string $id)
|
||||
public function removeRequest(string $id): void
|
||||
{
|
||||
$em = $this->entityManager;
|
||||
|
||||
$request = $em
|
||||
$request = $this->entityManager
|
||||
->getRDBRepository('PasswordChangeRequest')
|
||||
->where([
|
||||
'requestId' => $id,
|
||||
@@ -144,14 +130,13 @@ class Recovery
|
||||
->findOne();
|
||||
|
||||
if ($request) {
|
||||
$em->removeEntity($request);
|
||||
$this->entityManager->removeEntity($request);
|
||||
}
|
||||
}
|
||||
|
||||
public function request(string $emailAddress, string $userName, ?string $url): bool
|
||||
{
|
||||
$config = $this->config;
|
||||
$em = $this->entityManager;
|
||||
|
||||
$noExposure = $config->get('passwordRecoveryNoExposure') ?? false;
|
||||
|
||||
@@ -159,8 +144,8 @@ class Recovery
|
||||
throw new Forbidden("Password recovery: Disabled.");
|
||||
}
|
||||
|
||||
$user = $em
|
||||
->getRDBRepository('User')
|
||||
$user = $this->entityManager
|
||||
->getRDBRepository('User')
|
||||
->where([
|
||||
'userName' => $userName,
|
||||
'emailAddress' => $emailAddress,
|
||||
@@ -213,14 +198,14 @@ class Recovery
|
||||
return false;
|
||||
}
|
||||
|
||||
$passwordChangeRequest = $em
|
||||
$existingRequest = $this->entityManager
|
||||
->getRDBRepository('PasswordChangeRequest')
|
||||
->where([
|
||||
'userId' => $user->getId(),
|
||||
])
|
||||
->findOne();
|
||||
|
||||
if ($passwordChangeRequest) {
|
||||
if ($existingRequest) {
|
||||
if (!$noExposure) {
|
||||
throw new Forbidden(json_encode(['reason' => 'Already-Sent']));
|
||||
}
|
||||
@@ -230,39 +215,17 @@ class Recovery
|
||||
return false;
|
||||
}
|
||||
|
||||
$requestId = Util::generateCryptId();
|
||||
|
||||
$passwordChangeRequest = $em->getEntity('PasswordChangeRequest');
|
||||
|
||||
$passwordChangeRequest->set([
|
||||
'userId' => $user->id,
|
||||
'requestId' => $requestId,
|
||||
'url' => $url,
|
||||
]);
|
||||
$request = $this->createRequestNoSave($user, $url);
|
||||
|
||||
$microtime = microtime(true);
|
||||
|
||||
$this->send($requestId, $emailAddress, $user);
|
||||
$this->send($request->getRequestId(), $emailAddress, $user);
|
||||
|
||||
$em->saveEntity($passwordChangeRequest);
|
||||
|
||||
if (!$passwordChangeRequest->id) {
|
||||
throw new Error();
|
||||
}
|
||||
$this->entityManager->saveEntity($request);
|
||||
|
||||
$lifetime = $config->get('passwordRecoveryRequestLifetime') ?? self::REQUEST_LIFETIME;
|
||||
|
||||
$dt = new DateTime();
|
||||
|
||||
$dt->modify('+' . $lifetime);
|
||||
|
||||
$em->createEntity('Job', [
|
||||
'serviceName' => 'User',
|
||||
'methodName' => 'removeChangePasswordRequestJob',
|
||||
'data' => ['id' => $passwordChangeRequest->id],
|
||||
'executeTime' => $dt->format('Y-m-d H:i:s'),
|
||||
'queue' => QueueName::Q1,
|
||||
]);
|
||||
$this->createCleanupRequestJob($request->getId(), $lifetime);
|
||||
|
||||
$timeDiff = $this->getDelay() - floor((microtime(true) - $microtime) / 1000);
|
||||
|
||||
@@ -273,45 +236,104 @@ class Recovery
|
||||
return true;
|
||||
}
|
||||
|
||||
private function getDelay()
|
||||
private function createRequestNoSave(User $user, ?string $url = null): PasswordChangeRequest
|
||||
{
|
||||
$entity = $this->entityManager->getNewEntity(PasswordChangeRequest::ENTITY_TYPE);
|
||||
|
||||
$entity->set([
|
||||
'userId' => $user->getId(),
|
||||
'requestId' => Util::generateCryptId(),
|
||||
'url' => $url,
|
||||
]);
|
||||
|
||||
return $entity;
|
||||
}
|
||||
|
||||
public function createRequestForNewUser(User $user, ?string $url = null): PasswordChangeRequest
|
||||
{
|
||||
$entity = $this->createRequestNoSave($user, $url);
|
||||
|
||||
$this->entityManager->saveEntity($entity);
|
||||
|
||||
$lifetime = $this->config->get('passwordChangeRequestNewUserLifetime') ?? self::NEW_USER_REQUEST_LIFETIME;
|
||||
|
||||
$this->createCleanupRequestJob($entity->getId(), $lifetime);
|
||||
|
||||
return $entity;
|
||||
}
|
||||
|
||||
public function createAndSendRequestForExistingUser(User $user, ?string $url = null): PasswordChangeRequest
|
||||
{
|
||||
if (!$user->getEmailAddressGroup()->getPrimary()) {
|
||||
throw new Error("No email address.");
|
||||
}
|
||||
|
||||
$emailAddress = $user->getEmailAddressGroup()->getPrimary()->getAddress();
|
||||
|
||||
$entity = $this->createRequestNoSave($user, $url);
|
||||
|
||||
$this->entityManager->saveEntity($entity);
|
||||
|
||||
$lifetime = $this->config->get('passwordChangeRequestExistingUserLifetime') ??
|
||||
self::EXISTING_USER_REQUEST_LIFETIME;
|
||||
|
||||
$this->createCleanupRequestJob($entity->getId(), $lifetime);
|
||||
|
||||
$this->send($entity->getRequestId(), $emailAddress, $user);
|
||||
|
||||
return $entity;
|
||||
}
|
||||
|
||||
private function createCleanupRequestJob(string $id, string $lifetime): void
|
||||
{
|
||||
$this->entityManager->createEntity('Job', [
|
||||
'serviceName' => 'User',
|
||||
'methodName' => 'removeChangePasswordRequestJob',
|
||||
'data' => ['id' => $id],
|
||||
'executeTime' => DateTime::createNow()
|
||||
->modify('+' . $lifetime)
|
||||
->getString(),
|
||||
'queue' => QueueName::Q1,
|
||||
]);
|
||||
}
|
||||
|
||||
private function getDelay(): int
|
||||
{
|
||||
return $this->config->get('passwordRecoveryRequestDelay') ?? self::REQUEST_DELAY;
|
||||
}
|
||||
|
||||
protected function delay(?int $delay = null)
|
||||
protected function delay(?int $delay = null): void
|
||||
{
|
||||
$delay = $delay ?? $this->getDelay();
|
||||
if ($delay === null) {
|
||||
$delay = $this->getDelay();
|
||||
}
|
||||
|
||||
usleep($delay * 1000);
|
||||
}
|
||||
|
||||
protected function send(string $requestId, string $emailAddress, User $user)
|
||||
protected function send(string $requestId, string $emailAddress, User $user): void
|
||||
{
|
||||
$config = $this->config;
|
||||
$em = $this->entityManager;
|
||||
$htmlizerFactory = $this->htmlizerFactory;
|
||||
|
||||
$templateFileManager = $this->templateFileManager;
|
||||
|
||||
if (!$emailAddress) {
|
||||
return;
|
||||
}
|
||||
|
||||
$email = $em->getEntity('Email');
|
||||
$email = $this->entityManager->getEntity('Email');
|
||||
|
||||
if (!$this->emailSender->hasSystemSmtp() && !$config->get('internalSmtpServer')) {
|
||||
if (!$this->emailSender->hasSystemSmtp() && !$this->config->get('internalSmtpServer')) {
|
||||
throw new Error("Password recovery: SMTP credentials are not defined.");
|
||||
}
|
||||
|
||||
$sender = $this->emailSender->create();
|
||||
|
||||
$subjectTpl = $templateFileManager->getTemplate('passwordChangeLink', 'subject', 'User');
|
||||
$bodyTpl = $templateFileManager->getTemplate('passwordChangeLink', 'body', 'User');
|
||||
$subjectTpl = $this->templateFileManager->getTemplate('passwordChangeLink', 'subject', 'User');
|
||||
$bodyTpl = $this->templateFileManager->getTemplate('passwordChangeLink', 'body', 'User');
|
||||
|
||||
$siteUrl = $config->getSiteUrl();
|
||||
$siteUrl = $this->config->getSiteUrl();
|
||||
|
||||
if ($user->isPortal()) {
|
||||
$portal = $em->getRDBRepository('Portal')
|
||||
/** @var Portal|null $portal */
|
||||
$portal = $this->entityManager
|
||||
->getRDBRepository(Portal::ENTITY_TYPE)
|
||||
->distinct()
|
||||
->join('users')
|
||||
->where([
|
||||
@@ -320,10 +342,16 @@ class Recovery
|
||||
])
|
||||
->findOne();
|
||||
|
||||
if ($portal) {
|
||||
if ($portal->get('customUrl')) {
|
||||
$siteUrl = $portal->get('customUrl');
|
||||
}
|
||||
if (!$portal) {
|
||||
throw new Error("Portal user does not belong to any potral.");
|
||||
}
|
||||
|
||||
$this->getPortalRepository()->loadUrlField($portal);
|
||||
|
||||
$siteUrl = $portal->getUrl();
|
||||
|
||||
if (!$siteUrl) {
|
||||
throw new Error("Portal does not have URL.");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -333,7 +361,7 @@ class Recovery
|
||||
|
||||
$data['link'] = $link;
|
||||
|
||||
$htmlizer = $htmlizerFactory->create(true);
|
||||
$htmlizer = $this->htmlizerFactory->create(true);
|
||||
|
||||
$subject = $htmlizer->render($user, $subjectTpl, null, $data, true);
|
||||
$body = $htmlizer->render($user, $bodyTpl, null, $data, true);
|
||||
@@ -347,15 +375,15 @@ class Recovery
|
||||
|
||||
if (!$this->emailSender->hasSystemSmtp()) {
|
||||
$sender->withSmtpParams([
|
||||
'server' => $config->get('internalSmtpServer'),
|
||||
'port' => $config->get('internalSmtpPort'),
|
||||
'auth' => $config->get('internalSmtpAuth'),
|
||||
'username' => $config->get('internalSmtpUsername'),
|
||||
'password' => $config->get('internalSmtpPassword'),
|
||||
'security' => $config->get('internalSmtpSecurity'),
|
||||
'fromAddress' => $config->get(
|
||||
'server' => $this->config->get('internalSmtpServer'),
|
||||
'port' => $this->config->get('internalSmtpPort'),
|
||||
'auth' => $this->config->get('internalSmtpAuth'),
|
||||
'username' => $this->config->get('internalSmtpUsername'),
|
||||
'password' => $this->config->get('internalSmtpPassword'),
|
||||
'security' => $this->config->get('internalSmtpSecurity'),
|
||||
'fromAddress' => $this->config->get(
|
||||
'internalOutboundEmailFromAddress',
|
||||
$config->get('outboundEmailFromAddress')
|
||||
$this->config->get('outboundEmailFromAddress')
|
||||
),
|
||||
]);
|
||||
}
|
||||
@@ -363,11 +391,9 @@ class Recovery
|
||||
$sender->send($email);
|
||||
}
|
||||
|
||||
private function fail(?string $msg = null, int $errorCode = 403)
|
||||
private function fail(?string $msg = null, int $errorCode = 403): void
|
||||
{
|
||||
$config = $this->config;
|
||||
|
||||
$noExposure = $config->get('passwordRecoveryNoExposure') ?? false;
|
||||
$noExposure = $this->config->get('passwordRecoveryNoExposure') ?? false;
|
||||
|
||||
if ($msg) {
|
||||
$this->log->warning($msg);
|
||||
@@ -377,14 +403,20 @@ class Recovery
|
||||
if ($errorCode === 403) {
|
||||
throw new Forbidden();
|
||||
}
|
||||
else if ($errorCode === 404) {
|
||||
|
||||
if ($errorCode === 404) {
|
||||
throw new NotFound();
|
||||
}
|
||||
else {
|
||||
throw new Error();
|
||||
}
|
||||
|
||||
throw new Error();
|
||||
}
|
||||
|
||||
$this->delay();
|
||||
}
|
||||
|
||||
private function getPortalRepository(): PortalRepository
|
||||
{
|
||||
/** @var PortalRepository */
|
||||
return $this->entityManager->getRDBRepository(Portal::ENTITY_TYPE);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -888,8 +888,13 @@ class Util
|
||||
return $url;
|
||||
}
|
||||
|
||||
public static function generatePassword(int $length = 8, int $letters = 5, int $numbers = 3, bool $bothCases = false)
|
||||
{
|
||||
public static function generatePassword(
|
||||
int $length = 8,
|
||||
int $letters = 5,
|
||||
int $numbers = 3,
|
||||
bool $bothCases = false
|
||||
): string {
|
||||
|
||||
$chars = [
|
||||
'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz',
|
||||
'0123456789',
|
||||
@@ -900,28 +905,39 @@ class Util
|
||||
|
||||
$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;
|
||||
};
|
||||
|
||||
$upperCase = 0;
|
||||
$lowerCase = 0;
|
||||
|
||||
if ($bothCases) {
|
||||
$upperCase = 1;
|
||||
$lowerCase = 1;
|
||||
if ($letters >= 2) $letters = $letters - 2;
|
||||
else $letters = 0;
|
||||
|
||||
if ($letters >= 2) {
|
||||
$letters = $letters - 2;
|
||||
} else {
|
||||
$letters = 0;
|
||||
}
|
||||
}
|
||||
|
||||
$either = $length - ($letters + $numbers + $upperCase + $lowerCase);
|
||||
if ($either < 0) $either = 0;
|
||||
|
||||
if ($either < 0) {
|
||||
$either = 0;
|
||||
}
|
||||
|
||||
$array = [];
|
||||
|
||||
@@ -930,6 +946,7 @@ class Util
|
||||
$subArray = [];
|
||||
|
||||
$j = 0;
|
||||
|
||||
while ($j < $len) {
|
||||
$rand = (0 + (1 - 0) * (mt_rand() / mt_getrandmax()));
|
||||
$index = intval(floor($rand * strlen($set)));
|
||||
|
||||
@@ -32,4 +32,9 @@ namespace Espo\Entities;
|
||||
class PasswordChangeRequest extends \Espo\Core\ORM\Entity
|
||||
{
|
||||
public const ENTITY_TYPE = 'PasswordChangeRequest';
|
||||
|
||||
public function getRequestId(): string
|
||||
{
|
||||
return $this->get('requestId');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -52,4 +52,9 @@ class Portal extends \Espo\Core\ORM\Entity
|
||||
{
|
||||
return $this->settingsAttributeList;
|
||||
}
|
||||
|
||||
public function getUrl(): ?string
|
||||
{
|
||||
return $this->get('url');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,31 +29,29 @@
|
||||
|
||||
namespace Espo\EntryPoints;
|
||||
|
||||
use Espo\Core\Exceptions\NotFound;
|
||||
use Espo\Core\Exceptions\BadRequest;
|
||||
|
||||
use Espo\Core\{
|
||||
EntryPoint\EntryPoint,
|
||||
EntryPoint\Traits\NoAuth,
|
||||
Utils\Config,
|
||||
Utils\ClientManager,
|
||||
ORM\EntityManager,
|
||||
Api\Request,
|
||||
Api\Response,
|
||||
};
|
||||
use Espo\Entities\PasswordChangeRequest;
|
||||
|
||||
use Espo\Core\EntryPoint\EntryPoint;
|
||||
use Espo\Core\EntryPoint\Traits\NoAuth;
|
||||
use Espo\Core\Utils\Config;
|
||||
use Espo\Core\Utils\ClientManager;
|
||||
|
||||
use Espo\Core\Api\Request;
|
||||
use Espo\Core\Api\Response;
|
||||
|
||||
use Espo\ORM\EntityManager;
|
||||
|
||||
class ChangePassword implements EntryPoint
|
||||
{
|
||||
use NoAuth;
|
||||
|
||||
/** @var Config */
|
||||
protected $config;
|
||||
private Config $config;
|
||||
|
||||
/** @var ClientManager */
|
||||
protected $clientManager;
|
||||
private ClientManager $clientManager;
|
||||
|
||||
/** @var EntityManager */
|
||||
protected $entityManager;
|
||||
private EntityManager $entityManager;
|
||||
|
||||
public function __construct(Config $config, ClientManager $clientManager, EntityManager $entityManager)
|
||||
{
|
||||
@@ -71,34 +69,36 @@ class ChangePassword implements EntryPoint
|
||||
}
|
||||
|
||||
$passwordChangeRequest = $this->entityManager
|
||||
->getRDBRepository('PasswordChangeRequest')
|
||||
->getRDBRepository(PasswordChangeRequest::ENTITY_TYPE)
|
||||
->where([
|
||||
'requestId' => $requestId,
|
||||
])
|
||||
->findOne();
|
||||
|
||||
$strengthParams = [
|
||||
'passwordGenerateLength' => $this->config->get('passwordGenerateLength'),
|
||||
'passwordGenerateLetterCount' => $this->config->get('passwordGenerateLetterCount'),
|
||||
'generateNumberCount' => $this->config->get('generateNumberCount'),
|
||||
'passwordStrengthLength' => $this->config->get('passwordStrengthLength'),
|
||||
'passwordStrengthLetterCount' => $this->config->get('passwordStrengthLetterCount'),
|
||||
'passwordStrengthNumberCount' => $this->config->get('passwordStrengthNumberCount'),
|
||||
'passwordStrengthBothCases' => $this->config->get('passwordStrengthBothCases'),
|
||||
];
|
||||
|
||||
if (!$passwordChangeRequest) {
|
||||
throw new NotFound();
|
||||
}
|
||||
|
||||
$options = [
|
||||
'id' => $requestId,
|
||||
'strengthParams' => $strengthParams,
|
||||
'notFound' => !$passwordChangeRequest,
|
||||
];
|
||||
|
||||
$runScript = "
|
||||
app.getController('PasswordChangeRequest', function (controller) {
|
||||
controller.doAction('passwordChange', ".json_encode($options).");
|
||||
app.getController('PasswordChangeRequest', controller => {
|
||||
controller.doAction('passwordChange', " . json_encode($options) . ");
|
||||
});
|
||||
";
|
||||
|
||||
$this->clientManager->display($runScript);
|
||||
$response->writeBody(
|
||||
$this->clientManager->render($runScript)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,7 +29,7 @@
|
||||
|
||||
namespace Espo\Repositories;
|
||||
|
||||
use Espo\ORM\Entity;
|
||||
use Espo\Entities\Portal as PortalEntity;
|
||||
|
||||
use Espo\Core\Repositories\Database;
|
||||
|
||||
@@ -41,7 +41,7 @@ class Portal extends Database implements
|
||||
{
|
||||
use Di\ConfigSetter;
|
||||
|
||||
public function loadUrlField(Entity $entity)
|
||||
public function loadUrlField(PortalEntity $entity): void
|
||||
{
|
||||
if ($entity->get('customUrl')) {
|
||||
$entity->set('url', $entity->get('customUrl'));
|
||||
|
||||
@@ -72,6 +72,7 @@
|
||||
"Proceed w/o Contact": "Proceed w/o Contact",
|
||||
"Generate New API Key": "Generate New API Key",
|
||||
"Generate New Password": "Generate New Password",
|
||||
"Send Password Change Link": "Send Password Change Link",
|
||||
"Back to login form": "Back to login form",
|
||||
"Requirements": "Requirements",
|
||||
"Security": "Security",
|
||||
@@ -91,6 +92,7 @@
|
||||
"portals": "Portals which this user has access to."
|
||||
},
|
||||
"messages": {
|
||||
"sendPasswordChangeLinkConfirmation": "An email with a unique link will be sent to the user allowing them to change their password. The link will expire after a specific amount of time.",
|
||||
"passwordRecoverySentIfMatched": "Assuming the entered data matched any user account.",
|
||||
"passwordStrengthLength": "Must be at least {length} characters long.",
|
||||
"passwordStrengthLetterCount": "Must contain at least {count} letter(s).",
|
||||
@@ -120,7 +122,8 @@
|
||||
"security2FaResetConfimation": "Are you sure you want to reset the current 2FA settings?",
|
||||
"auth2FARequiredHeader": "2 factor authentication required",
|
||||
"auth2FARequired": "You need to set up 2 factor authentication. Use an authenticator application on your mobile phone (e.g. Google Authenticator).",
|
||||
"ldapUserInEspoNotFound": "User is not found in EspoCRM. Contact your administrator to create the user."
|
||||
"ldapUserInEspoNotFound": "User is not found in EspoCRM. Contact your administrator to create the user.",
|
||||
"passwordChangeRequestNotFound": "The password change request is not found. It might be expired. Try to initiate a new password recovery from the [login page]({url})."
|
||||
},
|
||||
"options": {
|
||||
"gender": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<h3>Vaše přístupové informace jsou</h3>
|
||||
|
||||
<p>Přihlašovací jméno: {{userName}}</p>
|
||||
<p>Heslo: {{password}}</p>
|
||||
<p>{{#if password}}Heslo: {{password}}{{/if}}</p>
|
||||
|
||||
<p><a href="{{siteUrl}}">{{siteUrl}}</a></p>
|
||||
@@ -1,6 +1,6 @@
|
||||
<h3>Dine Logindetaljer</h3>
|
||||
|
||||
<p>Brugernavn: {{userName}}</p>
|
||||
<p>Kodeord: {{password}}</p>
|
||||
<p>{{#if password}}Kodeord: {{password}}{{/if}}</p>
|
||||
|
||||
<p><a href="{{siteUrl}}">{{siteUrl}}</a></p>
|
||||
@@ -1,6 +1,6 @@
|
||||
<h3>Ihre EspoCRM Zugriffsinformation</h3>
|
||||
|
||||
<p>Benutzername: {{userName}}</p>
|
||||
<p>Passwort: {{password}}</p>
|
||||
<p>{{#if password}}Passwort: {{password}}{{/if}}</p>
|
||||
|
||||
<p><a href="{{siteUrl}}">{{siteUrl}}</a></p>
|
||||
@@ -1,6 +1,6 @@
|
||||
<h3>Your access information</h3>
|
||||
|
||||
<p>Username: {{userName}}</p>
|
||||
<p>Password: {{password}}</p>
|
||||
<p>{{#if password}}Password: {{password}}{{/if}}</p>
|
||||
|
||||
<p><a href="{{siteUrl}}">{{siteUrl}}</a></p>
|
||||
@@ -1,6 +1,6 @@
|
||||
<h3>Información de tu cuenta</h3>
|
||||
|
||||
<p>Nombre Usuario: {{userName}}</p>
|
||||
<p>Contraseña: {{password}}</p>
|
||||
<p>{{#if password}}Contraseña: {{password}}{{/if}}</p>
|
||||
|
||||
<p><a href="{{siteUrl}}">{{siteUrl}}</a></p>
|
||||
@@ -1,6 +1,6 @@
|
||||
<h3>Información de tu cuenta</h3>
|
||||
|
||||
<p>Nombre Usuario: {{userName}}</p>
|
||||
<p>Contraseña: {{password}}</p>
|
||||
<p>{{#if password}}Contraseña: {{password}}{{/if}}</p>
|
||||
|
||||
<p><a href="{{siteUrl}}">{{siteUrl}}</a></p>
|
||||
@@ -1,6 +1,6 @@
|
||||
<h3>Vos identifiants sont les suivants</h3>
|
||||
|
||||
<p>Nom d'utilisateur: {{userName}}</p>
|
||||
<p>Mot de passe: {{password}}</p>
|
||||
<p>{{#if password}}Mot de passe: {{password}}{{/if}}</p>
|
||||
|
||||
<p><a href="{{siteUrl}}">{{siteUrl}}</a></p>
|
||||
@@ -1,6 +1,6 @@
|
||||
<h3>Vaši podaci za pristup</h3>
|
||||
|
||||
<p>Korisničko ime: {{userName}}</p>
|
||||
<p>Lozinka: {{password}}</p>
|
||||
<p>{{#if password}}Lozinka: {{password}}{{/if}}</p>
|
||||
|
||||
<p><a href="{{siteUrl}}">{{siteUrl}}</a></p>
|
||||
@@ -1,6 +1,6 @@
|
||||
<h3>A hozzáférési adatai</h3>
|
||||
|
||||
<p>Felhasználónév: {{userName}}</p>
|
||||
<p>Jelszó: {{password}}</p>
|
||||
<p>{{#if password}}Jelszó: {{password}}{{/if}}</p>
|
||||
|
||||
<p><a href="{{siteUrl}}">{{siteUrl}}</a></p>
|
||||
@@ -1,6 +1,6 @@
|
||||
<h3>Akses informasi Anda</h3>
|
||||
|
||||
<p>Username: {{userName}}</p>
|
||||
<p>Password: {{password}}</p>
|
||||
<p>{{#if password}}Password: {{password}}{{/if}}</p>
|
||||
|
||||
<p><a href="{{siteUrl}}">{{siteUrl}}</a></p>
|
||||
@@ -1,6 +1,6 @@
|
||||
<h3>I tuoi dati di accesso</h3>
|
||||
|
||||
<p>Username: {{userName}}</p>
|
||||
<p>Password: {{password}}</p>
|
||||
<p>{{#if password}}Password: {{password}}{{/if}}</p>
|
||||
|
||||
<p><a href="{{siteUrl}}">{{siteUrl}}</a></p>
|
||||
@@ -1,6 +1,6 @@
|
||||
<h3>Jūsų prisijungimo informacija</h3>
|
||||
|
||||
<p>Vartotojo vardas: {{userName}}</p>
|
||||
<p>Slaptažodis: {{password}}</p>
|
||||
<p>{{#if password}}Slaptažodis: {{password}}{{/if}}</p>
|
||||
|
||||
<p><a href="{{siteUrl}}">{{siteUrl}}</a></p>
|
||||
@@ -1,6 +1,6 @@
|
||||
<h3>Påloggingsinformasjon</h3>
|
||||
|
||||
<p>Brukernavn: {{userName}}</p>
|
||||
<p>Passord: {{password}}</p>
|
||||
<p>{{#if password}}Passord: {{password}}{{/if}}</p>
|
||||
|
||||
<p><a href="{{siteUrl}}">{{siteUrl}}</a></p>
|
||||
@@ -1,6 +1,6 @@
|
||||
<h3>Uw gegevens</h3>
|
||||
|
||||
<p>Gebruikersnaam: {{userName}}</p>
|
||||
<p>Wachtwoord: {{password}}</p>
|
||||
<p>{{#if password}}Wachtwoord: {{password}}{{/if}}</p>
|
||||
|
||||
<p><a href="{{siteUrl}}">{{siteUrl}}</a></p>
|
||||
@@ -1,6 +1,6 @@
|
||||
<h3>Informacje o twoim koncie</h3>
|
||||
|
||||
<p>Użytkownik: {{userName}}</p>
|
||||
<p>Hasło: {{password}}</p>
|
||||
<p>{{#if password}}Hasło: {{password}}{{/if}}</p>
|
||||
|
||||
<p><a href="{{siteUrl}}">{{siteUrl}}</a></p>
|
||||
@@ -1,6 +1,6 @@
|
||||
<h3>Informações da sua conta</h3>
|
||||
|
||||
<p>Usuário: {{userName}}</p>
|
||||
<p>Senha: {{password}}</p>
|
||||
<p>{{#if password}}Senha: {{password}}{{/if}}</p>
|
||||
|
||||
<p><a href="{{siteUrl}}">{{siteUrl}}</a></p>
|
||||
@@ -1,6 +1,6 @@
|
||||
<h3>Informațiile tale de acces</h3>
|
||||
|
||||
<p>Username: {{userName}}</p>
|
||||
<p>Password: {{password}}</p>
|
||||
<p>{{#if password}}Password: {{password}}{{/if}}</p>
|
||||
|
||||
<p><a href="{{siteUrl}}">{{siteUrl}}</a></p>
|
||||
@@ -1,6 +1,6 @@
|
||||
<h3>Информация о Вашей учетной записи</h3>
|
||||
|
||||
<p>Имя пользователя: {{userName}}</p>
|
||||
<p>Пароль: {{password}}</p>
|
||||
<p>{{#if password}}Пароль: {{password}}{{/if}}</p>
|
||||
|
||||
<p><a href="{{siteUrl}}">{{siteUrl}}</a></p>
|
||||
@@ -1,6 +1,6 @@
|
||||
<h3>Vaš prístupové informácie</h3>
|
||||
|
||||
<p>Username: {{userName}}</p>
|
||||
<p>Password: {{password}}</p>
|
||||
<p>{{#if password}}Password: {{password}}{{/if}}</p>
|
||||
|
||||
<p><a href="{{siteUrl}}">{{siteUrl}}</a></p>
|
||||
@@ -1,6 +1,6 @@
|
||||
<h3>Vaši podaci za pristup</h3>
|
||||
|
||||
<p>Korisničko ime: {{userName}}</p>
|
||||
<p>Lozinka: {{password}}</p>
|
||||
<p>{{#if password}}Lozinka: {{password}}{{/if}}</p>
|
||||
|
||||
<p><a href="{{siteUrl}}">{{siteUrl}}</a></p>
|
||||
@@ -1,4 +1,4 @@
|
||||
<p>Username: {{userName}}</p>
|
||||
<p>Password: {{password}}</p>
|
||||
<p>{{#if password}}Password: {{password}}{{/if}}</p>
|
||||
|
||||
<p><a href="{{siteUrl}}">{{siteUrl}}</a></p>
|
||||
@@ -1,6 +1,6 @@
|
||||
<h3>Ваша інформація для доступу</h3>
|
||||
|
||||
<p>Ім'я користувача: {{userName}}</p>
|
||||
<p>Пароль: {{password}}</p>
|
||||
<p>{{#if password}}Пароль: {{password}}{{/if}}</p>
|
||||
|
||||
<p><a href="{{siteUrl}}">{{siteUrl}}</a></p>
|
||||
@@ -1,4 +1,4 @@
|
||||
<p>Username: {{userName}}</p>
|
||||
<p>Password: {{password}}</p>
|
||||
<p>{{#if password}}Password: {{password}}{{/if}}</p>
|
||||
|
||||
<p><a href="{{siteUrl}}">{{siteUrl}}</a></p>
|
||||
@@ -1,6 +1,6 @@
|
||||
<h3>您的访问信息</h3>
|
||||
|
||||
<p>用户名: {{userName}}</p>
|
||||
<p>密码: {{password}}</p>
|
||||
<p>{{#if password}}密码: {{password}}{{/if}}</p>
|
||||
|
||||
<p><a href="{{siteUrl}}">{{siteUrl}}</a></p>
|
||||
@@ -1,7 +1,7 @@
|
||||
<h3>Ihre EspoCRM Zugriffsinformation</h3>
|
||||
|
||||
<p>Benutzername: {{userName}}</p>
|
||||
<p>Passwort: {{password}}</p>
|
||||
<p>{{#if password}}Passwort: {{password}}{{/if}}</p>
|
||||
|
||||
{{#each siteUrlList}}
|
||||
<p><a href="{{./this}}">{{./this}}</a></p>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<h3>Your access information</h3>
|
||||
|
||||
<p>Username: {{userName}}</p>
|
||||
<p>Password: {{password}}</p>
|
||||
<p>{{#if password}}Password: {{password}}{{/if}}</p>
|
||||
|
||||
{{#each siteUrlList}}
|
||||
<p><a href="{{./this}}">{{./this}}</a></p>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<h3>Información de tu cuenta</h3>
|
||||
|
||||
<p>Nombre Usuario: {{userName}}</p>
|
||||
<p>Contraseña: {{password}}</p>
|
||||
<p>{{#if password}}Contraseña: {{password}}{{/if}}</p>
|
||||
|
||||
{{#each siteUrlList}}
|
||||
<p><a href="{{./this}}">{{./this}}</a></p>
|
||||
|
||||
@@ -31,11 +31,18 @@ namespace Espo\Services;
|
||||
|
||||
use Espo\Entities\User as UserEntity;
|
||||
use Espo\Entities\Email as EmailEntity;
|
||||
use Espo\Entities\PasswordChangeRequest;
|
||||
use Espo\Entities\Portal as PortalEntity;
|
||||
|
||||
use Espo\Repositories\Portal as PortalRepository;
|
||||
|
||||
use Espo\Core\Mail\Sender;
|
||||
|
||||
use Espo\Core\{
|
||||
Exceptions\Forbidden,
|
||||
Exceptions\Error,
|
||||
Exceptions\NotFound,
|
||||
Exceptions\BadRequest,
|
||||
Utils\Util,
|
||||
Utils\PasswordHash,
|
||||
Utils\ApiKey as ApiKeyUtil,
|
||||
@@ -122,13 +129,11 @@ class User extends Record implements
|
||||
}
|
||||
|
||||
if ($checkCurrentPassword) {
|
||||
$passwordHash = new PasswordHash($this->getConfig());
|
||||
|
||||
$u = $this->getEntityManager()
|
||||
->getRDBRepository('User')
|
||||
->where([
|
||||
'id' => $user->getId(),
|
||||
'password' => $passwordHash->hash($currentPassword),
|
||||
'password' => $this->createPasswordHashUtil()->hash($currentPassword),
|
||||
])
|
||||
->findOne();
|
||||
|
||||
@@ -211,16 +216,19 @@ class User extends Record implements
|
||||
return true;
|
||||
}
|
||||
|
||||
private function createRecoveryService(): Recovery
|
||||
{
|
||||
return $this->injectableFactory->create(Recovery::class);
|
||||
}
|
||||
|
||||
public function passwordChangeRequest(string $userName, string $emailAddress, ?string $url = null): void
|
||||
{
|
||||
$recovery = $this->injectableFactory->create(Recovery::class);
|
||||
|
||||
$recovery->request($emailAddress, $userName, $url);
|
||||
$this->createRecoveryService()->request($emailAddress, $userName, $url);
|
||||
}
|
||||
|
||||
public function changePasswordByRequest(string $requestId, string $password): stdClass
|
||||
{
|
||||
$recovery = $this->injectableFactory->create(Recovery::class);
|
||||
$recovery = $this->createRecoveryService();
|
||||
|
||||
$request = $recovery->getRequest($requestId);
|
||||
|
||||
@@ -247,7 +255,7 @@ class User extends Record implements
|
||||
|
||||
$id = $data->id;
|
||||
|
||||
$p = $this->getEntityManager()->getEntity('PasswordChangeRequest', $id);
|
||||
$p = $this->getEntityManager()->getEntity(PasswordChangeRequest::ENTITY_TYPE, $id);
|
||||
|
||||
if ($p) {
|
||||
$this->getEntityManager()->removeEntity($p);
|
||||
@@ -280,11 +288,17 @@ class User extends Record implements
|
||||
|
||||
public function create(stdClass $data, CreateParams $params): Entity
|
||||
{
|
||||
$newPassword = null;
|
||||
$newPassword = $data->password ?? null;
|
||||
|
||||
if (property_exists($data, 'password')) {
|
||||
$newPassword = $data->password;
|
||||
if ($newPassword === '') {
|
||||
$newPassword = null;
|
||||
}
|
||||
|
||||
if ($newPassword !== null && !is_string($newPassword)) {
|
||||
throw new BadRequest();
|
||||
}
|
||||
|
||||
if ($newPassword !== null) {
|
||||
if (!$this->checkPasswordStrength($newPassword)) {
|
||||
throw new Forbidden("Password is weak.");
|
||||
}
|
||||
@@ -295,13 +309,23 @@ class User extends Record implements
|
||||
/** @var UserEntity $user */
|
||||
$user = parent::create($data, $params);
|
||||
|
||||
if (!is_null($newPassword) && !empty($data->sendAccessInfo)) {
|
||||
if ($user->isActive()) {
|
||||
try {
|
||||
$this->sendPassword($user, $newPassword);
|
||||
}
|
||||
catch (Exception $e) {}
|
||||
$sendAccessInfo = !empty($data->sendAccessInfo);
|
||||
|
||||
if (!$sendAccessInfo || !$user->isActive() || $user->isApi()) {
|
||||
return $user;
|
||||
}
|
||||
|
||||
try {
|
||||
if ($newPassword !== null) {
|
||||
$this->sendPassword($user, $newPassword);
|
||||
|
||||
return $user;
|
||||
}
|
||||
|
||||
$this->sendAccessInfoNew($user);
|
||||
}
|
||||
catch (Exception $e) {
|
||||
$this->log->error("Could not send user access info. " . $e->getMessage());
|
||||
}
|
||||
|
||||
return $user;
|
||||
@@ -406,10 +430,67 @@ class User extends Record implements
|
||||
return $entity;
|
||||
}
|
||||
|
||||
public function generateNewPasswordForUser(string $id, bool $allowNonAdmin = false)
|
||||
private function generatePassword(): string
|
||||
{
|
||||
$length = $this->config->get('passwordStrengthLength');
|
||||
$letterCount = $this->config->get('passwordStrengthLetterCount');
|
||||
$numberCount = $this->config->get('passwordStrengthNumberCount');
|
||||
|
||||
$generateLength = $this->config->get('passwordGenerateLength', 10);
|
||||
$generateLetterCount = $this->config->get('passwordGenerateLetterCount', 4);
|
||||
$generateNumberCount = $this->config->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;
|
||||
}
|
||||
|
||||
return Util::generatePassword($length, $letterCount, $numberCount, true);
|
||||
}
|
||||
|
||||
public function sendPasswordChangeLink(string $id, bool $allowNonAdmin = false): void
|
||||
{
|
||||
if (!$allowNonAdmin && !$this->user->isAdmin()) {
|
||||
throw new Forbidden();
|
||||
}
|
||||
|
||||
/** @var UserEntity|null $user */
|
||||
$user = $this->entityManager->getEntityById(UserEntity::ENTITY_TYPE, $id);
|
||||
|
||||
if (!$user) {
|
||||
throw new NotFound();
|
||||
}
|
||||
|
||||
if (!$user->isActive()) {
|
||||
throw new Forbidden("User is not active.");
|
||||
}
|
||||
|
||||
if (
|
||||
!$user->isRegular() &&
|
||||
!$user->isAdmin() &&
|
||||
!$user->isPortal()
|
||||
) {
|
||||
throw new Forbidden();
|
||||
}
|
||||
|
||||
$this->createRecoveryService()->createAndSendRequestForExistingUser($user);
|
||||
}
|
||||
|
||||
public function generateNewPasswordForUser(string $id, bool $allowNonAdmin = false): void
|
||||
{
|
||||
if (!$allowNonAdmin) {
|
||||
if (!$this->getUser()->isAdmin()) {
|
||||
if (!$this->user->isAdmin()) {
|
||||
throw new Forbidden();
|
||||
}
|
||||
}
|
||||
@@ -439,45 +520,34 @@ class User extends Record implements
|
||||
);
|
||||
}
|
||||
|
||||
if (!$this->emailSender->hasSystemSmtp() && !$this->getConfig()->get('internalSmtpServer')) {
|
||||
if (!$this->isSmtpConfigured()) {
|
||||
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', 10);
|
||||
$generateLetterCount = $this->getConfig()->get('passwordGenerateLetterCount', 4);
|
||||
$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;
|
||||
}
|
||||
|
||||
$password = Util::generatePassword($length, $letterCount, $numberCount, true);
|
||||
$password = $this->generatePassword();
|
||||
|
||||
$this->sendPassword($user, $password);
|
||||
|
||||
$passwordHash = new PasswordHash($this->getConfig());
|
||||
$this->saveUserPassword($user, $password);
|
||||
}
|
||||
|
||||
$user->set('password', $passwordHash->hash($password));
|
||||
private function isSmtpConfigured(): bool
|
||||
{
|
||||
return $this->emailSender->hasSystemSmtp() || $this->config->get('internalSmtpServer');
|
||||
}
|
||||
|
||||
$this->getEntityManager()->saveEntity($user);
|
||||
private function saveUserPassword(UserEntity $user, string $password, bool $silent = false): void
|
||||
{
|
||||
$user->set('password', $this->createPasswordHashUtil()->hash($password));
|
||||
|
||||
$this->entityManager->saveEntity($user, ['silent' => $silent]);
|
||||
}
|
||||
|
||||
private function createPasswordHashUtil(): PasswordHash
|
||||
{
|
||||
return $this->injectableFactory->create(PasswordHash::class);
|
||||
}
|
||||
|
||||
protected function getInternalUserCount()
|
||||
@@ -619,7 +689,70 @@ class User extends Record implements
|
||||
}
|
||||
}
|
||||
|
||||
protected function sendPassword(UserEntity $user, $password)
|
||||
/**
|
||||
* @return array{?string,?string,?array<string,mixed>}
|
||||
*/
|
||||
private function getAccessInfoTemplateData(
|
||||
UserEntity $user,
|
||||
?string $password = null,
|
||||
?PasswordChangeRequest $passwordChangeRequest = null
|
||||
): array {
|
||||
|
||||
$data = [];
|
||||
|
||||
if ($password !== null) {
|
||||
$data['password'] = $password;
|
||||
}
|
||||
|
||||
$urlSuffix = '';
|
||||
|
||||
if ($passwordChangeRequest !== null) {
|
||||
$urlSuffix = '?entryPoint=changePassword&id=' . $passwordChangeRequest->getRequestId();
|
||||
}
|
||||
|
||||
$siteUrl = $this->config->getSiteUrl() . '/' . $urlSuffix;
|
||||
|
||||
if ($user->isPortal()) {
|
||||
$subjectTpl = $this->templateFileManager->getTemplate('accessInfoPortal', 'subject', 'User');
|
||||
$bodyTpl = $this->templateFileManager->getTemplate('accessInfoPortal', 'body', 'User');
|
||||
|
||||
$urlList = [];
|
||||
|
||||
$portalList = $this->entityManager
|
||||
->getRDBRepository(PortalEntity::ENTITY_TYPE)
|
||||
->distinct()
|
||||
->join('users')
|
||||
->where([
|
||||
'isActive' => true,
|
||||
'users.id' => $user->getId(),
|
||||
])
|
||||
->find();
|
||||
|
||||
foreach ($portalList as $portal) {
|
||||
/** @var PortalEntity $portal */
|
||||
$this->getPortalRepository()->loadUrlField($portal);
|
||||
|
||||
$urlList[] = $portal->getUrl() . $urlSuffix;
|
||||
}
|
||||
|
||||
if (count($urlList) === 0) {
|
||||
return [null, null, null];
|
||||
}
|
||||
|
||||
$data['siteUrlList'] = $urlList;
|
||||
|
||||
return [$subjectTpl, $bodyTpl, $data];
|
||||
}
|
||||
|
||||
$subjectTpl = $this->templateFileManager->getTemplate('accessInfo', 'subject', 'User');
|
||||
$bodyTpl = $this->templateFileManager->getTemplate('accessInfo', 'body', 'User');
|
||||
|
||||
$data['siteUrl'] = $siteUrl;
|
||||
|
||||
return [$subjectTpl, $bodyTpl, $data];
|
||||
}
|
||||
|
||||
protected function sendPassword(UserEntity $user, string $password): void
|
||||
{
|
||||
$emailAddress = $user->get('emailAddress');
|
||||
|
||||
@@ -630,68 +763,17 @@ class User extends Record implements
|
||||
/** @var EmailEntity $email */
|
||||
$email = $this->getEntityManager()->getEntity('Email');
|
||||
|
||||
if (!$this->emailSender->hasSystemSmtp() && !$this->getConfig()->get('internalSmtpServer')) {
|
||||
if (!$this->isSmtpConfigured()) {
|
||||
return;
|
||||
}
|
||||
|
||||
$templateFileManager = $this->templateFileManager;
|
||||
[$subjectTpl, $bodyTpl, $data] = $this->getAccessInfoTemplateData($user, $password);
|
||||
|
||||
$siteUrl = $this->getConfig()->getSiteUrl() . '/';
|
||||
|
||||
$data = [];
|
||||
|
||||
if ($user->isPortal()) {
|
||||
$subjectTpl = $templateFileManager->getTemplate('accessInfoPortal', 'subject', 'User');
|
||||
$bodyTpl = $templateFileManager->getTemplate('accessInfoPortal', 'body', 'User');
|
||||
|
||||
$urlList = [];
|
||||
|
||||
$portalList = $this->entityManager
|
||||
->getRDBRepository('Portal')
|
||||
->distinct()
|
||||
->join('users')
|
||||
->where([
|
||||
'isActive' => true,
|
||||
'users.id' => $user->getId(),
|
||||
])
|
||||
->find();
|
||||
|
||||
foreach ($portalList as $portal) {
|
||||
if ($portal->get('customUrl')) {
|
||||
$urlList[] = $portal->get('customUrl');
|
||||
}
|
||||
else {
|
||||
$url = $siteUrl . 'portal/';
|
||||
|
||||
if ($this->getConfig()->get('defaultPortalId') !== $portal->getId()) {
|
||||
if ($portal->get('customId')) {
|
||||
$url .= $portal->get('customId');
|
||||
}
|
||||
else {
|
||||
$url .= $portal->getId();
|
||||
}
|
||||
}
|
||||
|
||||
$urlList[] = $url;
|
||||
}
|
||||
}
|
||||
|
||||
if (!count($urlList)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$data['siteUrlList'] = $urlList;
|
||||
}
|
||||
else {
|
||||
$subjectTpl = $templateFileManager->getTemplate('accessInfo', 'subject', 'User');
|
||||
$bodyTpl = $templateFileManager->getTemplate('accessInfo', 'body', 'User');
|
||||
|
||||
$data['siteUrl'] = $siteUrl;
|
||||
if ($data === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
$data['password'] = $password;
|
||||
|
||||
$htmlizer = $this->htmlizerFactory->create(true);
|
||||
$htmlizer = $this->htmlizerFactory->createNoAcl();
|
||||
|
||||
$subject = $htmlizer->render($user, $subjectTpl, null, $data, true);
|
||||
$body = $htmlizer->render($user, $bodyTpl, null, $data, true);
|
||||
@@ -702,24 +784,29 @@ class User extends Record implements
|
||||
'to' => $emailAddress,
|
||||
]);
|
||||
|
||||
$this->getEmailSenderForAccessInfo()->send($email);
|
||||
}
|
||||
|
||||
private function getEmailSenderForAccessInfo(): Sender
|
||||
{
|
||||
$sender = $this->emailSender->create();
|
||||
|
||||
if (!$this->emailSender->hasSystemSmtp()) {
|
||||
$sender->withSmtpParams([
|
||||
'server' => $this->getConfig()->get('internalSmtpServer'),
|
||||
'port' => $this->getConfig()->get('internalSmtpPort'),
|
||||
'auth' => $this->getConfig()->get('internalSmtpAuth'),
|
||||
'username' => $this->getConfig()->get('internalSmtpUsername'),
|
||||
'password' => $this->getConfig()->get('internalSmtpPassword'),
|
||||
'security' => $this->getConfig()->get('internalSmtpSecurity'),
|
||||
'fromAddress' => $this->getConfig()->get(
|
||||
'server' => $this->config->get('internalSmtpServer'),
|
||||
'port' => $this->config->get('internalSmtpPort'),
|
||||
'auth' => $this->config->get('internalSmtpAuth'),
|
||||
'username' => $this->config->get('internalSmtpUsername'),
|
||||
'password' => $this->config->get('internalSmtpPassword'),
|
||||
'security' => $this->config->get('internalSmtpSecurity'),
|
||||
'fromAddress' => $this->config->get(
|
||||
'internalOutboundEmailFromAddress',
|
||||
$this->getConfig()->get('outboundEmailFromAddress')
|
||||
$this->config->get('outboundEmailFromAddress')
|
||||
),
|
||||
]);
|
||||
}
|
||||
|
||||
$sender->send($email);
|
||||
return $sender;
|
||||
}
|
||||
|
||||
public function delete(string $id, DeleteParams $params): void
|
||||
@@ -803,4 +890,50 @@ class User extends Record implements
|
||||
|
||||
$this->dataManager->updateCacheTimestamp();
|
||||
}
|
||||
|
||||
private function sendAccessInfoNew(UserEntity $user): void
|
||||
{
|
||||
if ($user->getEmailAddressGroup()->getPrimary() === null) {
|
||||
throw new Error("Can't send access info for user '{$user->getId()}' w/o email address.");
|
||||
}
|
||||
|
||||
if (!$this->isSmtpConfigured()) {
|
||||
throw new Error("Can't send access info. SMTP is not configured.");
|
||||
}
|
||||
|
||||
$stubPassword = $this->generatePassword();
|
||||
|
||||
$this->saveUserPassword($user, $stubPassword, true);
|
||||
|
||||
$request = $this->createRecoveryService()->createRequestForNewUser($user);
|
||||
|
||||
[$subjectTpl, $bodyTpl, $data] = $this->getAccessInfoTemplateData($user, null, $request);
|
||||
|
||||
if ($data === null) {
|
||||
throw new Error("Could not send access info.");
|
||||
}
|
||||
|
||||
$emailAddress = $user->getEmailAddressGroup()->getPrimary()->getAddress();
|
||||
|
||||
/** @var EmailEntity $email */
|
||||
$email = $this->entityManager->getEntity(EmailEntity::ENTITY_TYPE);
|
||||
|
||||
$htmlizer = $this->htmlizerFactory->createNoAcl();
|
||||
|
||||
$subject = $htmlizer->render($user, $subjectTpl, null, $data, true);
|
||||
$body = $htmlizer->render($user, $bodyTpl, null, $data, true);
|
||||
|
||||
$email
|
||||
->addToAddress($emailAddress)
|
||||
->setSubject($subject)
|
||||
->setBody($body);
|
||||
|
||||
$this->getEmailSenderForAccessInfo()->send($email);
|
||||
}
|
||||
|
||||
private function getPortalRepository(): PortalRepository
|
||||
{
|
||||
/** @var PortalRepository */
|
||||
return $this->entityManager->getRDBRepository(PortalEntity::ENTITY_TYPE);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,23 +1,43 @@
|
||||
<div class="container content">
|
||||
<div class="col-md-4 col-md-offset-3 col-sm-8 col-sm-offset-2">
|
||||
<div class="col-md-8 col-md-offset-1 col-sm-12 col-sm-offset-0">
|
||||
<div class="panel panel-default password-change">
|
||||
<div class="panel-heading">
|
||||
<h4 class="panel-title">{{translate 'Change Password' scope='User'}}</h4>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<div>
|
||||
<div class="cell form-group">
|
||||
<label for="login" class="control-label">{{translate 'newPassword' category='fields' scope='User'}}</label>
|
||||
<div class="field" data-name="password">{{{password}}}</div>
|
||||
</div>
|
||||
<div class="cell form-group">
|
||||
<label for="login" class="control-label">{{translate 'newPasswordConfirm' category='fields' scope='User'}}</label>
|
||||
<div class="field" data-name="passwordConfirm">{{{passwordConfirm}}}</div>
|
||||
</div>
|
||||
<div>
|
||||
<button type="button" class="btn btn-danger" id="btn-submit">{{translate 'Submit'}}</button>
|
||||
</div>
|
||||
{{#unless notFound}}
|
||||
<div class="row">
|
||||
<div class="cell form-group col-sm-6">
|
||||
<label
|
||||
for="login"
|
||||
class="control-label"
|
||||
>{{translate 'newPassword' category='fields' scope='User'}}</label>
|
||||
<div class="field" data-name="password">{{{password}}}</div>
|
||||
</div>
|
||||
<div class="cell form-group col-sm-6">
|
||||
<label class="control-label"></label>
|
||||
<div class="field" data-name="generatePassword">{{{generatePassword}}}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="cell form-group col-sm-6">
|
||||
<label
|
||||
for="login"
|
||||
class="control-label"
|
||||
>{{translate 'newPasswordConfirm' category='fields' scope='User'}}</label>
|
||||
<div class="field" data-name="passwordConfirm">{{{passwordConfirm}}}</div>
|
||||
</div>
|
||||
<div class="cell form-group col-sm-6">
|
||||
<label class="control-label"></label>
|
||||
<div class="field" data-name="passwordPreview">{{{passwordPreview}}}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<button type="button" class="btn btn-danger" id="btn-submit">{{translate 'Submit'}}</button>
|
||||
</div>
|
||||
{{else}}
|
||||
<p class="complex-text">{{complexText notFoundMessage}}</p>
|
||||
{{/unless}}
|
||||
</div>
|
||||
</div>
|
||||
<div class="msg-box hidden"></div>
|
||||
|
||||
@@ -40,7 +40,8 @@ define('controllers/password-change-request', 'controller', function (Dep) {
|
||||
this.entire('views/user/password-change-request', {
|
||||
requestId: options.id,
|
||||
strengthParams: options.strengthParams,
|
||||
}, function (view) {
|
||||
notFound: options.notFound,
|
||||
}, view => {
|
||||
view.render();
|
||||
});
|
||||
},
|
||||
|
||||
@@ -30,7 +30,8 @@ define('views/user/fields/generate-password', 'views/fields/base', function (Dep
|
||||
|
||||
return Dep.extend({
|
||||
|
||||
_template: '<button type="button" class="btn btn-default" data-action="generatePassword">{{translate \'Generate\' scope=\'User\'}}</button>',
|
||||
templateContent: '<button type="button" class="btn btn-default" data-action="generatePassword">' +
|
||||
'{{translate \'Generate\' scope=\'User\'}}</button>',
|
||||
|
||||
events: {
|
||||
'click [data-action="generatePassword"]': function () {
|
||||
@@ -41,12 +42,35 @@ define('views/user/fields/generate-password', 'views/fields/base', function (Dep
|
||||
setup: function () {
|
||||
Dep.prototype.setup.call(this);
|
||||
|
||||
this.listenTo(this.model, 'change:password', function (model, value, o) {
|
||||
if (o.isGenerated) return;
|
||||
this.listenTo(this.model, 'change:password', (model, value, o) => {
|
||||
if (o.isGenerated) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.model.set({
|
||||
passwordPreview: '',
|
||||
});
|
||||
}, this);
|
||||
});
|
||||
|
||||
this.strengthParams = this.options.strengthParams || {};
|
||||
|
||||
this.passwordStrengthLength = this.strengthParams.passwordStrengthLength ||
|
||||
this.getConfig().get('passwordStrengthLength');
|
||||
|
||||
this.passwordStrengthLetterCount = this.strengthParams.passwordStrengthLetterCount ||
|
||||
this.getConfig().get('passwordStrengthLetterCount');
|
||||
|
||||
this.passwordStrengthNumberCount = this.strengthParams.passwordStrengthNumberCount ||
|
||||
this.getConfig().get('passwordStrengthNumberCount');
|
||||
|
||||
this.passwordGenerateLength = this.strengthParams.passwordGenerateLength ||
|
||||
this.getConfig().get('passwordGenerateLength');
|
||||
|
||||
this.passwordGenerateLetterCount = this.strengthParams.passwordGenerateLetterCount ||
|
||||
this.getConfig().get('passwordGenerateLetterCount');
|
||||
|
||||
this.passwordGenerateNumberCount = this.strengthParams.passwordGenerateNumberCount ||
|
||||
this.getConfig().get('passwordGenerateNumberCount');
|
||||
},
|
||||
|
||||
fetch: function () {
|
||||
@@ -54,13 +78,13 @@ define('views/user/fields/generate-password', 'views/fields/base', function (Dep
|
||||
},
|
||||
|
||||
actionGeneratePassword: function () {
|
||||
var length = this.getConfig().get('passwordStrengthLength');
|
||||
var letterCount = this.getConfig().get('passwordStrengthLetterCount');
|
||||
var numberCount = this.getConfig().get('passwordStrengthNumberCount');
|
||||
var length = this.passwordStrengthLength;
|
||||
var letterCount = this.passwordStrengthLetterCount;
|
||||
var numberCount = this.passwordStrengthNumberCount;
|
||||
|
||||
var generateLength = this.getConfig().get('passwordGenerateLength') || 10;
|
||||
var generateLetterCount = this.getConfig().get('passwordGenerateLetterCount') || 4;
|
||||
var generateNumberCount = this.getConfig().get('passwordGenerateNumberCount') || 2;
|
||||
var generateLength = this.passwordGenerateLength || 10;
|
||||
var generateLetterCount = this.passwordGenerateLetterCount || 4;
|
||||
var generateNumberCount = this.passwordGenerateNumberCount || 2;
|
||||
|
||||
length = (typeof length === 'undefined') ? generateLength : length;
|
||||
letterCount = (typeof letterCount === 'undefined') ? generateLetterCount : letterCount;
|
||||
@@ -94,17 +118,25 @@ define('views/user/fields/generate-password', 'views/fields/base', function (Dep
|
||||
if (bothCases) {
|
||||
upperCase = 1;
|
||||
lowerCase = 1;
|
||||
if (letters >= 2) letters = letters - 2;
|
||||
else letters = 0;
|
||||
|
||||
if (letters >= 2) {
|
||||
letters = letters - 2;
|
||||
} else {
|
||||
letters = 0;
|
||||
}
|
||||
}
|
||||
|
||||
var either = length - (letters + numbers + upperCase + lowerCase);
|
||||
if (either < 0) either = 0;
|
||||
|
||||
if (either < 0) {
|
||||
either = 0;
|
||||
}
|
||||
|
||||
var setList = [letters, numbers, either, upperCase, lowerCase];
|
||||
|
||||
var shuffle = function (array) {
|
||||
var currentIndex = array.length, temporaryValue, randomIndex;
|
||||
|
||||
while (0 !== currentIndex) {
|
||||
randomIndex = Math.floor(Math.random() * currentIndex);
|
||||
currentIndex -= 1;
|
||||
@@ -112,6 +144,7 @@ define('views/user/fields/generate-password', 'views/fields/base', function (Dep
|
||||
array[currentIndex] = array[randomIndex];
|
||||
array[randomIndex] = temporaryValue;
|
||||
}
|
||||
|
||||
return array;
|
||||
};
|
||||
|
||||
|
||||
@@ -26,7 +26,7 @@
|
||||
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
|
||||
************************************************************************/
|
||||
|
||||
define('views/user/password-change-request', ['view', 'model'], function (Dep, Model) {
|
||||
define('views/user/password-change-request', ['view', 'model', 'lib!Espo'], function (Dep, Model, Espo) {
|
||||
|
||||
return Dep.extend({
|
||||
|
||||
@@ -34,24 +34,27 @@ define('views/user/password-change-request', ['view', 'model'], function (Dep, M
|
||||
|
||||
data: function () {
|
||||
return {
|
||||
requestId: this.options.requestId
|
||||
requestId: this.options.requestId,
|
||||
notFound: this.options.notFound,
|
||||
notFoundMessage: this.notFoundMessage,
|
||||
};
|
||||
},
|
||||
|
||||
events: {
|
||||
'click #btn-submit': function () {
|
||||
this.submit();
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
setup: function () {
|
||||
var model = this.model = new Model;
|
||||
|
||||
model.name = 'User';
|
||||
|
||||
this.createView('password', 'views/user/fields/password', {
|
||||
model: model,
|
||||
mode: 'edit',
|
||||
el: this.options.el + ' .field[data-name="password"]',
|
||||
el: this.getSelector() + ' .field[data-name="password"]',
|
||||
defs: {
|
||||
name: 'password',
|
||||
params: {
|
||||
@@ -64,7 +67,7 @@ define('views/user/password-change-request', ['view', 'model'], function (Dep, M
|
||||
this.createView('passwordConfirm', 'views/fields/password', {
|
||||
model: model,
|
||||
mode: 'edit',
|
||||
el: this.options.el + ' .field[data-name="passwordConfirm"]',
|
||||
el: this.getSelector() + ' .field[data-name="passwordConfirm"]',
|
||||
defs: {
|
||||
name: 'passwordConfirm',
|
||||
params: {
|
||||
@@ -72,6 +75,34 @@ define('views/user/password-change-request', ['view', 'model'], function (Dep, M
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
this.createView('generatePassword', 'views/user/fields/generate-password', {
|
||||
model: model,
|
||||
mode: 'detail',
|
||||
readOnly: true,
|
||||
el: this.getSelector() + ' .field[data-name="generatePassword"]',
|
||||
defs: {
|
||||
name: 'generatePassword',
|
||||
},
|
||||
strengthParams: this.options.strengthParams,
|
||||
});
|
||||
|
||||
this.createView('passwordPreview', 'views/fields/base', {
|
||||
model: model,
|
||||
mode: 'detail',
|
||||
readOnly: true,
|
||||
el: this.getSelector() + ' .field[data-name="passwordPreview"]',
|
||||
defs: {
|
||||
name: 'passwordPreview',
|
||||
},
|
||||
});
|
||||
|
||||
this.model.on('change:passwordPreview', () => this.reRender());
|
||||
|
||||
let url = this.baseUrl = window.location.href.split('?')[0];
|
||||
|
||||
this.notFoundMessage = this.translate('passwordChangeRequestNotFound', 'messages', 'User')
|
||||
.replace('{url}', url);
|
||||
},
|
||||
|
||||
submit: function () {
|
||||
@@ -79,7 +110,7 @@ define('views/user/password-change-request', ['view', 'model'], function (Dep, M
|
||||
this.getView('passwordConfirm').fetchToModel();
|
||||
|
||||
var notValid = this.getView('password').validate() ||
|
||||
this.getView('passwordConfirm').validate();
|
||||
this.getView('passwordConfirm').validate();
|
||||
|
||||
var password = this.model.get('password');
|
||||
|
||||
@@ -87,30 +118,31 @@ define('views/user/password-change-request', ['view', 'model'], function (Dep, M
|
||||
return;
|
||||
}
|
||||
|
||||
var $submit = this.$el.find('.btn-submit');
|
||||
let $submit = this.$el.find('.btn-submit');
|
||||
|
||||
$submit.addClass('disabled');
|
||||
|
||||
Espo.Ajax.postRequest('User/changePasswordByRequest', {
|
||||
requestId: this.options.requestId,
|
||||
password: password,
|
||||
}).then(
|
||||
function (data) {
|
||||
Espo.Ajax
|
||||
.postRequest('User/changePasswordByRequest', {
|
||||
requestId: this.options.requestId,
|
||||
password: password,
|
||||
})
|
||||
.then(data => {
|
||||
this.$el.find('.password-change').remove();
|
||||
|
||||
var url = data.url || this.getConfig().get('siteUrl');
|
||||
var url = data.url || this.baseUrl;
|
||||
|
||||
var msg = this.translate('passwordChangedByRequest', 'messages', 'User');
|
||||
msg += ' <a href="' + url + '">' + this.translate('Login', 'labels', 'User') + '</a>.';
|
||||
var msg = this.translate('passwordChangedByRequest', 'messages', 'User') +
|
||||
' <a href="' + url + '">' + this.translate('Login', 'labels', 'User') + '</a>.';
|
||||
|
||||
this.$el.find('.msg-box').removeClass('hidden').html('<span class="text-success">' + msg + '</span>');
|
||||
}.bind(this)
|
||||
).fail(
|
||||
function () {
|
||||
$submit.removeClass('disabled');
|
||||
}.bind(this)
|
||||
);
|
||||
this.$el.find('.msg-box')
|
||||
.removeClass('hidden')
|
||||
.html('<span class="text-success">' + msg + '</span>');
|
||||
})
|
||||
.catch(() =>
|
||||
$submit.removeClass('disabled')
|
||||
);
|
||||
},
|
||||
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -77,12 +77,21 @@ define('views/user/record/detail', 'views/record/detail', function (Dep) {
|
||||
}
|
||||
|
||||
if (
|
||||
this.getUser().isAdmin()
|
||||
&&
|
||||
(this.model.isRegular() || this.model.isAdmin() || this.model.isPortal())
|
||||
&&
|
||||
this.getUser().isAdmin() &&
|
||||
(
|
||||
this.model.isRegular() ||
|
||||
this.model.isAdmin() ||
|
||||
this.model.isPortal()
|
||||
) &&
|
||||
!this.model.isSuperAdmin()
|
||||
) {
|
||||
this.addDropdownItem({
|
||||
name: 'sendPasswordChangeLink',
|
||||
label: 'Send Password Change Link',
|
||||
action: 'sendPasswordChangeLink',
|
||||
hidden: !this.model.get('emailAddress'),
|
||||
});
|
||||
|
||||
this.addDropdownItem({
|
||||
name: 'generateNewPassword',
|
||||
label: 'Generate New Password',
|
||||
@@ -91,13 +100,15 @@ define('views/user/record/detail', 'views/record/detail', function (Dep) {
|
||||
});
|
||||
|
||||
if (!this.model.get('emailAddress')) {
|
||||
this.listenTo(this.model, 'sync', function () {
|
||||
this.listenTo(this.model, 'sync', () => {
|
||||
if (this.model.get('emailAddress')) {
|
||||
this.showActionItem('generateNewPassword');
|
||||
this.showActionItem('sendPasswordChangeLink');
|
||||
} else {
|
||||
this.hideActionItem('generateNewPassword');
|
||||
this.hideActionItem('sendPasswordChangeLink');
|
||||
}
|
||||
}, this);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -105,10 +116,10 @@ define('views/user/record/detail', 'views/record/detail', function (Dep) {
|
||||
this.hideActionItem('duplicate');
|
||||
}
|
||||
|
||||
if (this.model.id == this.getUser().id) {
|
||||
this.listenTo(this.model, 'after:save', function () {
|
||||
if (this.model.id === this.getUser().id) {
|
||||
this.listenTo(this.model, 'after:save', () => {
|
||||
this.getUser().set(this.model.toJSON());
|
||||
}.bind(this));
|
||||
});
|
||||
}
|
||||
|
||||
this.setupFieldAppearance();
|
||||
@@ -144,29 +155,27 @@ define('views/user/record/detail', 'views/record/detail', function (Dep) {
|
||||
'emailAddress',
|
||||
];
|
||||
|
||||
nonAdminReadOnlyFieldList = nonAdminReadOnlyFieldList.filter(
|
||||
function (item) {
|
||||
if (!this.model.hasField(item)) {
|
||||
return true;
|
||||
}
|
||||
nonAdminReadOnlyFieldList = nonAdminReadOnlyFieldList.filter(item => {
|
||||
if (!this.model.hasField(item)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
var aclDefs = this.getMetadata().get(['entityAcl', 'User', 'fields', item]);
|
||||
var aclDefs = this.getMetadata().get(['entityAcl', 'User', 'fields', item]);
|
||||
|
||||
if (!aclDefs) {
|
||||
return true;
|
||||
}
|
||||
if (!aclDefs) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (aclDefs.nonAdminReadOnly) {
|
||||
return true;
|
||||
}
|
||||
if (aclDefs.nonAdminReadOnly) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}.bind(this),
|
||||
);
|
||||
return false;
|
||||
});
|
||||
|
||||
nonAdminReadOnlyFieldList.forEach(function (field) {
|
||||
nonAdminReadOnlyFieldList.forEach((field) => {
|
||||
this.setFieldReadOnly(field, true);
|
||||
}, this);
|
||||
});
|
||||
|
||||
if (!this.getAcl().checkScope('Team')) {
|
||||
this.setFieldReadOnly('defaultTeam', true);
|
||||
@@ -174,11 +183,11 @@ define('views/user/record/detail', 'views/record/detail', function (Dep) {
|
||||
},
|
||||
|
||||
setupFieldAppearance: function () {
|
||||
|
||||
this.controlFieldAppearance();
|
||||
this.listenTo(this.model, 'change', function () {
|
||||
|
||||
this.listenTo(this.model, 'change', () => {
|
||||
this.controlFieldAppearance();
|
||||
}, this);
|
||||
});
|
||||
},
|
||||
|
||||
controlFieldAppearance: function () {
|
||||
@@ -223,7 +232,7 @@ define('views/user/record/detail', 'views/record/detail', function (Dep) {
|
||||
if (this.model.id === this.getUser().id) {
|
||||
this.setFieldReadOnly('type');
|
||||
} else {
|
||||
if (this.model.get('type') == 'admin' || this.model.get('type') == 'regular') {
|
||||
if (this.model.get('type') === 'admin' || this.model.get('type') === 'regular') {
|
||||
this.setFieldNotReadOnly('type');
|
||||
this.setFieldOptionList('type', ['regular', 'admin']);
|
||||
} else {
|
||||
@@ -245,17 +254,16 @@ define('views/user/record/detail', 'views/record/detail', function (Dep) {
|
||||
|
||||
this.createView('changePassword', 'views/modals/change-password', {
|
||||
userId: this.model.id
|
||||
}, function (view) {
|
||||
}, (view) => {
|
||||
view.render();
|
||||
this.notify(false);
|
||||
|
||||
this.listenToOnce(view, 'changed', function () {
|
||||
setTimeout(function () {
|
||||
this.listenToOnce(view, 'changed', () => {
|
||||
setTimeout(() => {
|
||||
this.getBaseController().logout();
|
||||
}.bind(this), 2000);
|
||||
}, this);
|
||||
|
||||
}.bind(this));
|
||||
}, 2000);
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
actionPreferences: function () {
|
||||
@@ -279,19 +287,23 @@ define('views/user/record/detail', 'views/record/detail', function (Dep) {
|
||||
data: {
|
||||
id: this.model.id,
|
||||
}
|
||||
}).done(function (aclData) {
|
||||
}).then((aclData) => {
|
||||
this.createView('access', 'views/user/modals/access', {
|
||||
aclData: aclData,
|
||||
model: this.model,
|
||||
}, function (view) {
|
||||
}, (view) => {
|
||||
this.notify(false);
|
||||
|
||||
view.render();
|
||||
}.bind(this));
|
||||
}.bind(this));
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
getGridLayout: function (callback) {
|
||||
this._helper.layoutManager.get(this.model.name, this.options.layoutName || this.layoutName, function (simpleLayout) {
|
||||
this._helper
|
||||
.layoutManager
|
||||
.get(this.model.name, this.options.layoutName || this.layoutName, (simpleLayout) => {
|
||||
|
||||
var layout = Espo.Utils.cloneDeep(simpleLayout);
|
||||
|
||||
if (!this.getUser().isPortal()) {
|
||||
@@ -342,42 +354,55 @@ define('views/user/record/detail', 'views/record/detail', function (Dep) {
|
||||
};
|
||||
|
||||
callback(gridLayout);
|
||||
}.bind(this));
|
||||
});
|
||||
},
|
||||
|
||||
actionGenerateNewApiKey: function () {
|
||||
this.confirm(this.translate('confirmation', 'messages'), function () {
|
||||
this.confirm(this.translate('confirmation', 'messages'), () => {
|
||||
this.ajaxPostRequest('User/action/generateNewApiKey', {
|
||||
id: this.model.id
|
||||
}).then(function (data) {
|
||||
id: this.model.id,
|
||||
}).then((data) => {
|
||||
this.model.set(data);
|
||||
}.bind(this));
|
||||
}.bind(this));
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
actionViewSecurity: function () {
|
||||
this.createView('dialog', 'views/user/modals/security', {
|
||||
userModel: this.model,
|
||||
}, function (view) {
|
||||
}, (view) => {
|
||||
view.render();
|
||||
}, this);
|
||||
});
|
||||
},
|
||||
|
||||
actionSendPasswordChangeLink: function () {
|
||||
this.confirm({
|
||||
message: this.translate('sendPasswordChangeLinkConfirmation', 'messages', 'User'),
|
||||
confirmText: this.translate('Send', 'labels', 'Email'),
|
||||
})
|
||||
.then(() => {
|
||||
Espo.Ui.notify(this.translate('pleaseWait', 'messages'));
|
||||
|
||||
Espo.Ajax.postRequest('User/action/sendPasswordChangeLink', {
|
||||
id: this.model.id,
|
||||
}).then(() => {
|
||||
Espo.Ui.success(this.translate('Done'));
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
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)
|
||||
);
|
||||
).then(() => {
|
||||
Espo.Ui.notify(this.translate('pleaseWait', 'messages'));
|
||||
|
||||
Espo.Ajax.postRequest('User/action/generateNewPassword', {
|
||||
id: this.model.id,
|
||||
}).then(() => {
|
||||
Espo.Ui.success(this.translate('Done'));
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
});
|
||||
|
||||
@@ -37,66 +37,84 @@ define('views/user/record/edit', ['views/record/edit', 'views/user/record/detail
|
||||
|
||||
this.setupNonAdminFieldsAccess();
|
||||
|
||||
if (this.model.id == this.getUser().id) {
|
||||
this.listenTo(this.model, 'after:save', function () {
|
||||
if (this.model.id === this.getUser().id) {
|
||||
this.listenTo(this.model, 'after:save', () => {
|
||||
this.getUser().set(this.model.toJSON());
|
||||
}, this);
|
||||
});
|
||||
}
|
||||
|
||||
this.hideField('sendAccessInfo');
|
||||
|
||||
this.passwordInfoMessage = this.getPasswordSendingMessage();
|
||||
|
||||
if (!this.passwordInfoMessage) {
|
||||
this.hideField('passwordInfo');
|
||||
}
|
||||
|
||||
var passwordChanged = false;
|
||||
let passwordChanged = false;
|
||||
|
||||
this.listenToOnce(this.model, 'change:password', function (model) {
|
||||
this.listenToOnce(this.model, 'change:password', (model) => {
|
||||
passwordChanged = true;
|
||||
if (this.isPasswordSendable()) {
|
||||
this.showField('sendAccessInfo');
|
||||
this.model.set('sendAccessInfo', true);
|
||||
}
|
||||
}, this);
|
||||
|
||||
this.listenTo(this.model, 'change', function (model) {
|
||||
if (!passwordChanged) return;
|
||||
if (!model.hasChanged('emailAddress') && !model.hasChanged('portalsIds')) return;
|
||||
this.controlSendAccessInfoField();
|
||||
});
|
||||
|
||||
if (this.isPasswordSendable()) {
|
||||
this.showField('sendAccessInfo');
|
||||
this.model.set('sendAccessInfo', true);
|
||||
} else {
|
||||
this.hideField('sendAccessInfo');
|
||||
this.model.set('sendAccessInfo', false);
|
||||
this.listenTo(this.model, 'change', (model) => {
|
||||
if (!this.model.isNew() && !passwordChanged) {
|
||||
return;
|
||||
}
|
||||
}, this);
|
||||
|
||||
if (
|
||||
!model.hasChanged('emailAddress') &&
|
||||
!model.hasChanged('portalsIds')
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.controlSendAccessInfoField();
|
||||
});
|
||||
|
||||
Detail.prototype.setupFieldAppearance.call(this);
|
||||
|
||||
this.hideField('passwordPreview');
|
||||
this.listenTo(this.model, 'change:passwordPreview', function (model, value) {
|
||||
|
||||
this.listenTo(this.model, 'change:passwordPreview', (model, value) => {
|
||||
value = value || '';
|
||||
|
||||
if (value.length) {
|
||||
this.showField('passwordPreview');
|
||||
} else {
|
||||
this.hideField('passwordPreview');
|
||||
}
|
||||
}, this);
|
||||
});
|
||||
|
||||
|
||||
this.listenTo(this.model, 'after:save', function () {
|
||||
this.listenTo(this.model, 'after:save', () => {
|
||||
this.model.unset('password', {silent: true});
|
||||
this.model.unset('passwordConfirm', {silent: true});
|
||||
}, this);
|
||||
});
|
||||
},
|
||||
|
||||
controlSendAccessInfoField: function () {
|
||||
if (this.isPasswordSendable()) {
|
||||
this.showField('sendAccessInfo');
|
||||
this.model.set('sendAccessInfo', true);
|
||||
} else {
|
||||
this.hideField('sendAccessInfo');
|
||||
this.model.set('sendAccessInfo', false);
|
||||
}
|
||||
},
|
||||
|
||||
isPasswordSendable: function () {
|
||||
if (this.model.isPortal()) {
|
||||
if (!(this.model.get('portalsIds') || []).length) return false;
|
||||
if (!(this.model.get('portalsIds') || []).length) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (!this.model.get('emailAddress')) {
|
||||
return false;
|
||||
}
|
||||
if (!this.model.get('emailAddress')) return false;
|
||||
|
||||
return true;
|
||||
},
|
||||
@@ -150,7 +168,7 @@ define('views/user/record/edit', ['views/record/edit', 'views/user/record/detail
|
||||
name: 'password',
|
||||
type: 'password',
|
||||
params: {
|
||||
required: this.isNew,
|
||||
required: false,
|
||||
readyToChange: true,
|
||||
},
|
||||
view: 'views/user/fields/password',
|
||||
@@ -166,7 +184,7 @@ define('views/user/record/edit', ['views/record/edit', 'views/user/record/detail
|
||||
name: 'passwordConfirm',
|
||||
type: 'password',
|
||||
params: {
|
||||
required: this.isNew,
|
||||
required: false,
|
||||
readyToChange: true
|
||||
}
|
||||
},
|
||||
@@ -216,9 +234,11 @@ define('views/user/record/edit', ['views/record/edit', 'views/user/record/detail
|
||||
if (this.getConfig().get('smtpServer') && this.getConfig().get('smtpServer') !== '') {
|
||||
return '';
|
||||
}
|
||||
|
||||
var msg = this.translate('setupSmtpBefore', 'messages', 'User').replace('{url}', '#Admin/outboundEmails');
|
||||
|
||||
msg = this.getHelper().transfromMarkdownInlineText(msg);
|
||||
|
||||
return msg;
|
||||
},
|
||||
|
||||
@@ -241,7 +261,7 @@ define('views/user/record/edit', ['views/record/edit', 'views/user/record/detail
|
||||
|
||||
errorHandlerUserNameExists: function () {
|
||||
Espo.Ui.error(this.translate('userNameExists', 'messages', 'User'))
|
||||
}
|
||||
},
|
||||
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user