diff --git a/application/Espo/Core/Authentication/Authentication.php b/application/Espo/Core/Authentication/Authentication.php
index 962d1d6fcd..7c7d0c1be8 100644
--- a/application/Espo/Core/Authentication/Authentication.php
+++ b/application/Espo/Core/Authentication/Authentication.php
@@ -259,7 +259,17 @@ class Authentication
$user->set('ipAddress', $request->getServerParam('REMOTE_ADDR') ?? null);
- $this->applicationUser->setUser($user);
+ [$loggedUser, $anotherUserFailReason] = $this->getLoggedUser($request, $user);
+
+ if (!$loggedUser) {
+ return $this->processFail(
+ Result::fail($anotherUserFailReason ?? FailReason::ANOTHER_USER_NOT_FOUND),
+ $data,
+ $request
+ );
+ }
+
+ $this->applicationUser->setUser($loggedUser);
if (
!$result->isSecondStepRequired() &&
@@ -291,8 +301,8 @@ class Authentication
$authTokenId = $authToken->id ?? null;
}
- $user->set('token', $authToken->getToken());
- $user->set('authTokenId', $authTokenId);
+ $loggedUser->set('token', $authToken->getToken());
+ $loggedUser->set('authTokenId', $authTokenId);
if ($authLogRecord) {
$authLogRecord->set('authTokenId', $authTokenId);
@@ -315,7 +325,7 @@ class Authentication
}
if ($authLogRecord) {
- $user->set('authLogRecordId', $authLogRecord->getId());
+ $loggedUser->set('authLogRecordId', $authLogRecord->getId());
}
if ($result->isSuccess()) {
@@ -436,13 +446,13 @@ class Authentication
private function processTwoFactor(Result $result, Request $request): Result
{
- $loggedUser = $result->getLoggedUser();
+ $user = $result->getUser();
- if (!$loggedUser) {
- throw new RuntimeException("No logged-user.");
+ if (!$user) {
+ throw new RuntimeException("No user.");
}
- $method = $this->getUser2FAMethod($loggedUser);
+ $method = $this->getUser2FAMethod($user);
if (!$method) {
return $result;
@@ -709,4 +719,43 @@ class Authentication
return $user->getUserName();
}
+
+ /**
+ * @return array{?User, (FailReason::*)|null}
+ */
+ private function getLoggedUser(Request $request, User $user): array
+ {
+ $username = $request->getHeader('X-Another-User');
+
+ if (!$username) {
+ return [$user, null];
+ }
+
+ if ($this->configDataProvider->isAnotherUserDisabled()) {
+ return [null, FailReason::ANOTHER_USER_NOT_ALLOWED];
+ }
+
+ // Important check.
+ if (!$user->isAdmin()) {
+ return [null, FailReason::ANOTHER_USER_NOT_ALLOWED];
+ }
+
+ /** @var ?User $loggedUser */
+ $loggedUser = $this->entityManager
+ ->getRDBRepository(User::ENTITY_TYPE)
+ ->where([
+ 'userName' => $username,
+ ])
+ ->findOne();
+
+ if (!$loggedUser) {
+ return [null, FailReason::ANOTHER_USER_NOT_FOUND];
+ }
+
+ if (!$loggedUser->isRegular()) {
+ return [null, FailReason::ANOTHER_USER_NOT_ALLOWED];
+ }
+
+ return [$loggedUser, null];
+ }
}
diff --git a/application/Espo/Core/Authentication/ConfigDataProvider.php b/application/Espo/Core/Authentication/ConfigDataProvider.php
index fc766aef72..6b47130060 100644
--- a/application/Espo/Core/Authentication/ConfigDataProvider.php
+++ b/application/Espo/Core/Authentication/ConfigDataProvider.php
@@ -119,4 +119,9 @@ class ConfigDataProvider
{
return (bool) $this->metadata->get(['authenticationMethods', $authenticationMethod, 'api']);
}
+
+ public function isAnotherUserDisabled(): bool
+ {
+ return (bool) $this->config->get('authAnotherUserDisabled');
+ }
}
diff --git a/application/Espo/Core/Authentication/Result.php b/application/Espo/Core/Authentication/Result.php
index 48af2c20fe..354a88a675 100644
--- a/application/Espo/Core/Authentication/Result.php
+++ b/application/Espo/Core/Authentication/Result.php
@@ -49,7 +49,6 @@ class Result
private ?string $message = null;
private ?string $token = null;
private ?string $view = null;
- private ?User $loggedUser = null;
private ?string $failReason = null;
private ?Data $data = null;
@@ -64,7 +63,6 @@ class Result
$this->message = $data->getMessage();
$this->token = $data->getToken();
$this->view = $data->getView();
- $this->loggedUser = $data->getLoggedUser();
$this->failReason = $data->getFailReason();
}
}
@@ -130,12 +128,11 @@ class Result
}
/**
- * Get a logged user. Considered that an admin user can log in as another user.
- * The logged user will be an admin user.
+ * @deprecated Use `getUser`.
*/
public function getLoggedUser(): ?User
{
- return $this->loggedUser ?? $this->user;
+ return $this->user;
}
/**
diff --git a/application/Espo/Core/Authentication/Result/Data.php b/application/Espo/Core/Authentication/Result/Data.php
index 1d54de4e37..f669bea4f0 100644
--- a/application/Espo/Core/Authentication/Result/Data.php
+++ b/application/Espo/Core/Authentication/Result/Data.php
@@ -38,7 +38,6 @@ class Data
private ?string $message = null;
private ?string $token = null;
private ?string $view = null;
- private ?User $loggedUser = null;
private ?string $failReason = null;
/**
@@ -50,14 +49,12 @@ class Data
?string $message = null,
?string $failReason = null,
?string $token = null,
- ?string $view = null,
- ?User $loggedUser = null
+ ?string $view = null
) {
$this->message = $message;
$this->failReason = $failReason;
$this->token = $token;
$this->view = $view;
- $this->loggedUser = $loggedUser;
}
public static function create(): self
@@ -75,11 +72,6 @@ class Data
return new self($message);
}
- public function getLoggedUser(): ?User
- {
- return $this->loggedUser;
- }
-
public function getView(): ?string
{
return $this->view;
@@ -137,14 +129,6 @@ class Data
return $obj;
}
- public function withLoggedUser(?User $loggedUser): self
- {
- $obj = clone $this;
- $obj->loggedUser = $loggedUser;
-
- return $obj;
- }
-
/**
* @param mixed $value
*/
diff --git a/application/Espo/Core/Authentication/Result/FailReason.php b/application/Espo/Core/Authentication/Result/FailReason.php
index e9d23496ed..50a2dab2d3 100644
--- a/application/Espo/Core/Authentication/Result/FailReason.php
+++ b/application/Espo/Core/Authentication/Result/FailReason.php
@@ -42,4 +42,6 @@ class FailReason
public const HASH_NOT_MATCHED = 'Hash not matched';
public const METHOD_NOT_ALLOWED = 'Not allowed authentication method';
public const DISCREPANT_DATA = 'Discrepant authentication data';
+ public const ANOTHER_USER_NOT_FOUND = 'Another user not found';
+ public const ANOTHER_USER_NOT_ALLOWED = 'Another user not allowed';
}
diff --git a/application/Espo/Core/Authentication/TwoFactor/Email/EmailLogin.php b/application/Espo/Core/Authentication/TwoFactor/Email/EmailLogin.php
index 4466f7a026..f1c11cf886 100644
--- a/application/Espo/Core/Authentication/TwoFactor/Email/EmailLogin.php
+++ b/application/Espo/Core/Authentication/TwoFactor/Email/EmailLogin.php
@@ -64,25 +64,19 @@ class EmailLogin implements Login
{
$code = $request->getHeader('Espo-Authorization-Code');
- $loggedUser = $result->getLoggedUser();
+ $user = $result->getUser();
- if (!$loggedUser) {
- throw new RuntimeException("No logged-user.");
+ if (!$user) {
+ throw new RuntimeException("No user.");
}
if (!$code) {
- $user = $result->getUser();
-
- if (!$user) {
- throw new RuntimeException("No user.");
- }
-
- $this->util->sendCode($loggedUser);
+ $this->util->sendCode($user);
return Result::secondStepRequired($user, $this->getResultData());
}
- if ($this->verifyCode($loggedUser, $code)) {
+ if ($this->verifyCode($user, $code)) {
return $result;
}
diff --git a/application/Espo/Core/Authentication/TwoFactor/Sms/SmsLogin.php b/application/Espo/Core/Authentication/TwoFactor/Sms/SmsLogin.php
index dbc9bb3a36..5ac39e06a5 100644
--- a/application/Espo/Core/Authentication/TwoFactor/Sms/SmsLogin.php
+++ b/application/Espo/Core/Authentication/TwoFactor/Sms/SmsLogin.php
@@ -63,10 +63,10 @@ class SmsLogin implements Login
{
$code = $request->getHeader('Espo-Authorization-Code');
- $loggedUser = $result->getLoggedUser();
+ $user = $result->getUser();
- if (!$loggedUser) {
- throw new RuntimeException("No logged-user.");
+ if (!$user) {
+ throw new RuntimeException("No user.");
}
if (!$code) {
@@ -76,12 +76,12 @@ class SmsLogin implements Login
throw new RuntimeException("No user.");
}
- $this->util->sendCode($loggedUser);
+ $this->util->sendCode($user);
return Result::secondStepRequired($user, $this->getResultData());
}
- if ($this->verifyCode($loggedUser, $code)) {
+ if ($this->verifyCode($user, $code)) {
return $result;
}
diff --git a/application/Espo/Core/Authentication/TwoFactor/Totp/TotpLogin.php b/application/Espo/Core/Authentication/TwoFactor/Totp/TotpLogin.php
index 961039add0..3217fe556b 100644
--- a/application/Espo/Core/Authentication/TwoFactor/Totp/TotpLogin.php
+++ b/application/Espo/Core/Authentication/TwoFactor/Totp/TotpLogin.php
@@ -64,23 +64,17 @@ class TotpLogin implements Login
{
$code = $request->getHeader('Espo-Authorization-Code');
+ $user = $result->getUser();
+
+ if (!$user) {
+ throw new RuntimeException("No user.");
+ }
+
if (!$code) {
- $user = $result->getUser();
-
- if (!$user) {
- throw new RuntimeException("No user.");
- }
-
return Result::secondStepRequired($user, $this->getResultData());
}
- $loggedUser = $result->getLoggedUser();
-
- if (!$loggedUser) {
- throw new RuntimeException("No logged-user.");
- }
-
- if ($this->verifyCode($loggedUser, $code)) {
+ if ($this->verifyCode($user, $code)) {
return $result;
}
diff --git a/application/Espo/Core/Utils/Client/ActionRenderer.php b/application/Espo/Core/Utils/Client/ActionRenderer.php
index 98fe02b2cb..3d3ccb0dac 100644
--- a/application/Espo/Core/Utils/Client/ActionRenderer.php
+++ b/application/Espo/Core/Utils/Client/ActionRenderer.php
@@ -65,13 +65,14 @@ class ActionRenderer
{
$encodedData = Json::encode($data);
- $script = "
- app.doAction({
- controllerClassName: '{$controller}',
- action: '{$action}',
- options: {$encodedData},
- });
- ";
+ $script =
+ "
+ app.doAction({
+ controllerClassName: '{$controller}',
+ action: '{$action}',
+ options: {$encodedData},
+ });
+ ";
return $this->clientManager->render($script);
}
diff --git a/application/Espo/EntryPoints/LoginAs.php b/application/Espo/EntryPoints/LoginAs.php
new file mode 100644
index 0000000000..01d156f4e7
--- /dev/null
+++ b/application/Espo/EntryPoints/LoginAs.php
@@ -0,0 +1,82 @@
+clientManager = $clientManager;
+ }
+
+ /**
+ * @throws BadRequest
+ */
+ public function run(Request $request, Response $response): void
+ {
+ $anotherUser = $request->getQueryParam('anotherUser');
+
+ if (!$anotherUser) {
+ throw new BadRequest("No anotherUser.");
+ }
+
+ $data = [
+ 'anotherUser' => $anotherUser,
+ 'username' => $request->getQueryParam('username'),
+ ];
+
+ $encodedData = Json::encode($data);
+
+ $script =
+ "
+ app.initAuth();
+ app.doAction({
+ controllerClassName: 'controllers/login-as',
+ action: 'login',
+ options: {$encodedData},
+ });
+ ";
+
+ $this->clientManager->writeHeaders($response);
+ $response->writeBody($this->clientManager->render($script)); ;
+ }
+}
diff --git a/application/Espo/Resources/defaults/systemConfig.php b/application/Espo/Resources/defaults/systemConfig.php
index 714027514a..68d726949f 100644
--- a/application/Espo/Resources/defaults/systemConfig.php
+++ b/application/Espo/Resources/defaults/systemConfig.php
@@ -101,6 +101,7 @@ return [
'clientSecurityHeadersDisabled',
'clientCspDisabled',
'clientCspScriptSourceList',
+ 'authAnotherUserDisabled',
],
'adminItems' => [
'devMode',
diff --git a/application/Espo/Resources/i18n/en_US/Global.json b/application/Espo/Resources/i18n/en_US/Global.json
index cb100656b1..7ac7b95670 100644
--- a/application/Espo/Resources/i18n/en_US/Global.json
+++ b/application/Espo/Resources/i18n/en_US/Global.json
@@ -176,6 +176,8 @@
"Password": "Password",
"Login": "Login",
"Log Out": "Log Out",
+ "Log in": "Log in",
+ "Log in as": "Log in as",
"Preferences": "Preferences",
"State": "State",
"Street": "Street",
diff --git a/application/Espo/Resources/i18n/en_US/User.json b/application/Espo/Resources/i18n/en_US/User.json
index 9cead3e2f4..8346dea1a8 100644
--- a/application/Espo/Resources/i18n/en_US/User.json
+++ b/application/Espo/Resources/i18n/en_US/User.json
@@ -82,7 +82,8 @@
"Reset 2FA": "Reset 2FA",
"Code": "Code",
"Secret": "Secret",
- "Send Code": "Send Code"
+ "Send Code": "Send Code",
+ "Login Link": "Login Link"
},
"tooltips": {
"defaultTeam": "All records created by this user will be related to this team by default.",
@@ -95,6 +96,7 @@
"portals": "Portals which this user has access to."
},
"messages": {
+ "loginAs": "Open the login link in an incognito window to preserve your current session. Use your admin credentials to log in.",
"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.",
diff --git a/client/res/templates/login.tpl b/client/res/templates/login.tpl
index df5262b35e..694baff19e 100644
--- a/client/res/templates/login.tpl
+++ b/client/res/templates/login.tpl
@@ -35,6 +35,12 @@
maxlength="255"
>
+ {{#if anotherUser}}
+
+ {{/if}}
{{#if showForgotPassword}}
{
- this.onAuth.call(this);
- });
+
+ return;
}
- this.on('auth', this.onAuth, this);
+ this.initUserData(null, () => this.onAuth.call(this));
},
/**
@@ -497,6 +508,11 @@ function (
this.loadStylesheet();
}
+ if (this.anotherUser) {
+ this.viewHelper.webSocketManager = null;
+ this.webSocketManager = null;
+ }
+
if (this.webSocketManager) {
this.webSocketManager.connect(this.auth, this.user.id);
}
@@ -668,6 +684,7 @@ function (
metadata: this.metadata,
dateTime: this.dateTime,
broadcastChannel: this.broadcastChannel,
+ baseController: this.baseController,
};
},
@@ -716,8 +733,6 @@ function (
controllerClass => {
let injections = this.getControllerInjection();
- injections.baseController = this.baseController;
-
let controller = new controllerClass(this.baseController.params, injections);
controller.name = name;
@@ -871,17 +886,22 @@ function (
},
/**
- * @private
+ * @public
*/
initAuth: function () {
+ this.on('auth', this.onAuth, this);
+
this.auth = this.storage.get('user', 'auth') || null;
+ this.anotherUser = this.storage.get('user', 'anotherUser') || null;
this.baseController.on('login', data => {
let userId = data.user.id;
let userName = data.auth.userName;
let token = data.auth.token;
+ let anotherUser = data.auth.anotherUser || null;
this.auth = Base64.encode(userName + ':' + token);
+ this.anotherUser = anotherUser;
let lastUserId = this.storage.get('user', 'lastUserId');
@@ -892,6 +912,7 @@ function (
this.storage.set('user', 'auth', this.auth);
this.storage.set('user', 'lastUserId', userId);
+ this.storage.set('user', 'anotherUser', this.anotherUser);
this.setCookieAuth(userName, token);
@@ -920,6 +941,7 @@ function (
}
this.auth = null;
+ this.anotherUser = null;
this.user.clear();
this.preferences.clear();
@@ -927,6 +949,7 @@ function (
this.acl.clear();
this.storage.clear('user', 'auth');
+ this.storage.clear('user', 'anotherUser');
this.doAction({action: 'login'});
@@ -1084,6 +1107,9 @@ function (
xhr.setRequestHeader('Espo-Authorization-By-Token', 'true');
}
+ if (this.anotherUser !== null) {
+ xhr.setRequestHeader('X-Another-User', this.anotherUser);
+ }
},
dataType: 'json',
timeout: this.ajaxTimeout,
diff --git a/client/src/controller.js b/client/src/controller.js
index f0b765a219..6bbe5808ce 100644
--- a/client/src/controller.js
+++ b/client/src/controller.js
@@ -136,6 +136,8 @@ define('controller', [], function () {
*/
setRouter: function (router) {
this._router = router;
+
+ this.trigger('router-set', router);
},
/**
diff --git a/client/src/controllers/base.js b/client/src/controllers/base.js
index dd19e31365..909a931b67 100644
--- a/client/src/controllers/base.js
+++ b/client/src/controllers/base.js
@@ -41,10 +41,18 @@ define('controllers/base', ['controller'], function (Dep) {
/**
* Log in.
*/
- login: function () {
- var viewName = this.getConfig().get('loginView') || 'views/login';
+ login: function (options) {
+ let viewName = this.getConfig().get('loginView') || 'views/login';
- this.entire(viewName, {}, loginView => {
+ let anotherUser = (options || {}).anotherUser;
+ let prefilledUsername = (options || {}).username;
+
+ let viewOptions = {
+ anotherUser: anotherUser,
+ prefilledUsername: prefilledUsername,
+ };
+
+ this.entire(viewName, viewOptions, loginView => {
loginView.render();
loginView.on('login', (userName, data) => {
@@ -58,6 +66,7 @@ define('controllers/base', ['controller'], function (Dep) {
loginData: data,
userName: userName,
password: password,
+ anotherUser: anotherUser,
}, secondStepView => {
secondStepView.render();
@@ -83,6 +92,7 @@ define('controllers/base', ['controller'], function (Dep) {
auth: {
userName: userName,
token: data.token,
+ anotherUser: data.anotherUser,
},
user: data.user,
preferences: data.preferences,
diff --git a/client/src/controllers/login-as.js b/client/src/controllers/login-as.js
new file mode 100644
index 0000000000..fdd8245bed
--- /dev/null
+++ b/client/src/controllers/login-as.js
@@ -0,0 +1,54 @@
+/************************************************************************
+ * This file is part of EspoCRM.
+ *
+ * EspoCRM - Open Source CRM application.
+ * Copyright (C) 2014-2022 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('controllers/login-as', ['controller'], function (Dep) {
+
+ return Dep.extend({
+
+ actionLogin: function (options) {
+ let anotherUser = options.anotherUser;
+ let username = options.username;
+
+ if (!anotherUser) {
+ throw new Error("No anotherUser.");
+ }
+
+ this.baseController.login({
+ anotherUser: anotherUser,
+ username: username,
+ });
+
+ this.listenToOnce(this.baseController, 'login', () => {
+ this.baseController.once('router-set', () => {
+ let url = window.location.href.split('?')[0];
+ window.location.replace(url);
+ })
+ });
+ },
+ });
+});
diff --git a/client/src/view-helper.js b/client/src/view-helper.js
index 5c5190c205..00c1d47bb3 100644
--- a/client/src/view-helper.js
+++ b/client/src/view-helper.js
@@ -202,9 +202,9 @@ function (marked, DOMPurify, /** typeof Handlebars */Handlebars) {
themeManager: null,
/**
- * A web-socket manager
+ * A web-socket manager. Null if not enabled.
*
- * @type {module:web-socket-manager.Class}
+ * @type {?module:web-socket-manager.Class}
*/
webSocketManager: null,
diff --git a/client/src/views/login-second-step.js b/client/src/views/login-second-step.js
index 878b79d53d..2444a29d3f 100644
--- a/client/src/views/login-second-step.js
+++ b/client/src/views/login-second-step.js
@@ -39,6 +39,11 @@ define('views/login-second-step', ['view'], function (Dep) {
},
},
+ /**
+ * @type {?string}
+ */
+ anotherUser: null,
+
events: {
'submit #login-form': function (e) {
e.preventDefault();
@@ -65,6 +70,7 @@ define('views/login-second-step', ['view'], function (Dep) {
setup: function () {
this.message = this.translate(this.options.loginData.message, 'messages', 'User');
+ this.anotherUser = this.options.anotherUser || null;
},
send: function () {
@@ -117,18 +123,29 @@ define('views/login-second-step', ['view'], function (Dep) {
let authString = Base64.encode(userName + ':' + password);
+ let headers = {
+ 'Authorization': 'Basic ' + authString,
+ 'Espo-Authorization': authString,
+ 'Espo-Authorization-Code': code,
+ 'Espo-Authorization-Create-Token-Secret': true,
+ };
+
+ if (this.anotherUser !== null) {
+ headers['X-Another-User'] = this.anotherUser;
+ }
+
Espo.Ajax
.getRequest('App/user', {code: code}, {
login: true,
- headers: {
- 'Authorization': 'Basic ' + authString,
- 'Espo-Authorization': authString,
- 'Espo-Authorization-Code': code,
- 'Espo-Authorization-Create-Token-Secret': true,
- },
+ headers: headers,
})
.then(data => {
this.notify(false);
+
+ if (this.anotherUser) {
+ data.anotherUser = this.anotherUser;
+ }
+
this.trigger('login', userName, data);
})
.catch(xhr => {
diff --git a/client/src/views/login.js b/client/src/views/login.js
index 296b2b2420..8a397ef38b 100644
--- a/client/src/views/login.js
+++ b/client/src/views/login.js
@@ -39,6 +39,11 @@ define('views/login', ['view'], function (Dep) {
},
},
+ /**
+ * @type {?string}
+ */
+ anotherUser: null,
+
events: {
'submit #login-form': function (e) {
e.preventDefault();
@@ -61,9 +66,14 @@ define('views/login', ['view'], function (Dep) {
return {
logoSrc: this.getLogoSrc(),
showForgotPassword: this.getConfig().get('passwordRecoveryEnabled'),
+ anotherUser: this.anotherUser,
};
},
+ setup: function () {
+ this.anotherUser = this.options.anotherUser || null;
+ },
+
getLogoSrc: function () {
var companyLogoId = this.getConfig().get('companyLogoId');
@@ -76,19 +86,25 @@ define('views/login', ['view'], function (Dep) {
afterRender: function () {
this.$submit = this.$el.find('#btn-login');
+ this.$username = this.$el.find('#field-userName');
+ this.$password = this.$el.find('#field-password');
+
+ if (this.options.prefilledUsername) {
+ this.$username.val(this.options.prefilledUsername);
+ }
},
login: function () {
- let userName = $('#field-userName').val();
+ let userName = this.$username.val();
let trimmedUserName = userName.trim();
if (trimmedUserName !== userName) {
- $('#field-userName').val(trimmedUserName);
+ this.$username.val(trimmedUserName);
userName = trimmedUserName;
}
- let password = $('#field-password').val();
+ let password = this.$password.val();
if (userName === '') {
this.isPopoverDestroyed = false;
@@ -140,19 +156,29 @@ define('views/login', ['view'], function (Dep) {
throw e;
}
+ let headers = {
+ 'Authorization': 'Basic ' + authString,
+ 'Espo-Authorization': authString,
+ 'Espo-Authorization-By-Token': false,
+ 'Espo-Authorization-Create-Token-Secret': true,
+ };
+
+ if (this.anotherUser !== null) {
+ headers['X-Another-User'] = this.anotherUser;
+ }
+
Espo.Ajax
.getRequest('App/user', null, {
login: true,
- headers: {
- 'Authorization': 'Basic ' + authString,
- 'Espo-Authorization': authString,
- 'Espo-Authorization-By-Token': false,
- 'Espo-Authorization-Create-Token-Secret': true,
- },
+ headers: headers,
})
.then(data => {
this.notify(false);
+ if (this.anotherUser) {
+ data.anotherUser = this.anotherUser;
+ }
+
this.trigger('login', userName, data);
})
.catch(xhr => {
diff --git a/client/src/views/notification/badge.js b/client/src/views/notification/badge.js
index 21d3bad965..5b26a3397b 100644
--- a/client/src/views/notification/badge.js
+++ b/client/src/views/notification/badge.js
@@ -51,7 +51,7 @@ define('views/notification/badge', ['view'], function (Dep) {
this.notificationSoundsDisabled = true;
- this.useWebSocket = this.getConfig().get('useWebSocket');
+ this.useWebSocket = !!this.getHelper().webSocketManager;
this.once('remove', () => {
if (this.timeout) {
diff --git a/client/src/views/record/detail.js b/client/src/views/record/detail.js
index bcf1176649..b52f2ac211 100644
--- a/client/src/views/record/detail.js
+++ b/client/src/views/record/detail.js
@@ -1856,7 +1856,7 @@ function (Dep, ViewRecordHelper, ActionItemSetup) {
if (
!this.isNew &&
- this.getConfig().get('useWebSocket') &&
+ !!this.getHelper().webSocketManager &&
this.getMetadata().get(['scopes', this.entityType, 'object'])
) {
this.subscribeToWebSocket();
diff --git a/client/src/views/stream/panel.js b/client/src/views/stream/panel.js
index d06155ae0b..8cc09bec93 100644
--- a/client/src/views/stream/panel.js
+++ b/client/src/views/stream/panel.js
@@ -280,7 +280,7 @@ define('views/stream/panel', ['views/record/panels/relationship', 'lib!Textcompl
},
subscribeToWebSocket: function () {
- if (!this.getConfig().get('useWebSocket')) {
+ if (!this.getHelper().webSocketManager) {
return;
}
diff --git a/client/src/views/user/modals/login-as.js b/client/src/views/user/modals/login-as.js
new file mode 100644
index 0000000000..73b9472212
--- /dev/null
+++ b/client/src/views/user/modals/login-as.js
@@ -0,0 +1,56 @@
+/************************************************************************
+ * This file is part of EspoCRM.
+ *
+ * EspoCRM - Open Source CRM application.
+ * Copyright (C) 2014-2022 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/modals/login-as', ['views/modal'], function (Dep) {
+
+ return Dep.extend({
+
+ backdrop: true,
+
+ templateContent: `
+
+ {{translate 'loginAs' category='messages' scope='User'}}
+
+ {{translate 'Login Link' scope='User'}}
+ `,
+
+ setup: function () {
+ this.$header = $('
')
+ .append(
+ $('').text(this.model.get('name')),
+ ' ',
+ $('').addClass('chevron-right'),
+ ' ',
+ $('').text(this.translate('Login')),
+ );
+
+ this.url = `?entryPoint=loginAs` +
+ `&anotherUser=${this.options.anotherUser}&username=${this.options.username}`;
+ },
+ });
+});
diff --git a/client/src/views/user/record/detail.js b/client/src/views/user/record/detail.js
index d3141bafbc..8ae9e16550 100644
--- a/client/src/views/user/record/detail.js
+++ b/client/src/views/user/record/detail.js
@@ -122,6 +122,14 @@ define('views/user/record/detail', 'views/record/detail', function (Dep) {
});
}
+ if (this.getUser().isAdmin() && this.model.isRegular()) {
+ this.addDropdownItem({
+ label: 'Log in',
+ name: 'login',
+ action: 'login',
+ });
+ }
+
this.setupFieldAppearance();
},
@@ -416,5 +424,16 @@ define('views/user/record/detail', 'views/record/detail', function (Dep) {
});
},
+ actionLogin: function () {
+ let anotherUser = this.model.get('userName');
+ let username = this.getUser().get('userName');
+
+ this.createView('dialog', 'views/user/modals/login-as', {
+ model: this.model,
+ anotherUser: anotherUser,
+ username: username,
+ })
+ .then(view => view.render());
+ },
});
});