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}} +
+ +
{{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()); + }, }); });