diff --git a/application/Espo/Core/Utils/EntityManager.php b/application/Espo/Core/Utils/EntityManager.php index 4b7fb1ac95..3b379bb492 100644 --- a/application/Espo/Core/Utils/EntityManager.php +++ b/application/Espo/Core/Utils/EntityManager.php @@ -54,6 +54,8 @@ class EntityManager private $linkForbiddenNameList = ['posts', 'stream', 'subscription']; + private $forbiddenEntityTypeNameList = ['PortalUser', 'ApiUser']; + public function __construct(Metadata $metadata, Language $language, File\Manager $fileManager, Config $config, Container $container = null) { $this->metadata = $metadata; @@ -192,6 +194,10 @@ class EntityManager throw new Conflict('Entity name \''.$name.'\' is not allowed.'); } + if (in_array($name, $this->forbiddenEntityTypeNameList)) { + throw new Conflict('Entity name \''.$name.'\' is not allowed.'); + } + if (in_array(strtolower($name), $this->reservedWordList)) { throw new Conflict('Entity name \''.$name.'\' is not allowed.'); } diff --git a/application/Espo/Entities/User.php b/application/Espo/Entities/User.php index fe27ded2eb..007b7f3a53 100644 --- a/application/Espo/Entities/User.php +++ b/application/Espo/Entities/User.php @@ -51,6 +51,11 @@ class User extends \Espo\Core\Entities\Person return $this->isPortal(); } + public function isRegular() + { + return $this->get('type') === 'regular'; + } + public function isApi() { return $this->get('type') === 'api'; @@ -84,4 +89,25 @@ class User extends \Espo\Core\Entities\Person } } } + + protected function _getName() + { + if (!array_key_exists('name', $this->valuesContainer) || !$this->valuesContainer['name']) { + if ($this->get('userName')) { + return $this->get('userName'); + } + } + return $this->valuesContainer['name']; + } + + protected function _hasName() + { + if (array_key_exists('name', $this->valuesContainer)) { + return true; + } + if ($this->has('userName')) { + return true; + } + return false; + } } diff --git a/application/Espo/Hooks/Common/Stream.php b/application/Espo/Hooks/Common/Stream.php index 20403526b7..21c75d0377 100644 --- a/application/Espo/Hooks/Common/Stream.php +++ b/application/Espo/Hooks/Common/Stream.php @@ -203,6 +203,8 @@ class Stream extends \Espo\Core\Hooks\Base if ( !$this->getUser()->isSystem() && + !$this->getUser()->isApi() + && $createdById && $createdById === $this->getUser()->id diff --git a/application/Espo/Repositories/User.php b/application/Espo/Repositories/User.php index ced81e4ee2..98c2946eb9 100644 --- a/application/Espo/Repositories/User.php +++ b/application/Espo/Repositories/User.php @@ -133,4 +133,19 @@ class User extends \Espo\Core\ORM\Repositories\RDB } return false; } + + public function handleSelectParams(&$params) + { + parent::handleSelectParams($params); + if (array_key_exists('select', $params)) { + if (in_array('name', $params['select'])) { + $additionalAttributeList = ['userName']; + foreach ($additionalAttributeList as $attribute) { + if (!in_array($attribute, $params['select'])) { + $params['select'][] = $attribute; + } + } + } + } + } } diff --git a/application/Espo/Resources/i18n/en_US/ApiUser.json b/application/Espo/Resources/i18n/en_US/ApiUser.json new file mode 100644 index 0000000000..58070d2ed1 --- /dev/null +++ b/application/Espo/Resources/i18n/en_US/ApiUser.json @@ -0,0 +1,5 @@ +{ + "labels": { + "Create ApiUser": "Create API User" + } +} diff --git a/application/Espo/Resources/i18n/en_US/Global.json b/application/Espo/Resources/i18n/en_US/Global.json index 3603f44ff3..f3659773e8 100644 --- a/application/Espo/Resources/i18n/en_US/Global.json +++ b/application/Espo/Resources/i18n/en_US/Global.json @@ -24,6 +24,7 @@ "Attachment": "Attachment", "EmailFolder": "Email Folder", "PortalUser": "Portal User", + "ApiUser": "API User", "ScheduledJobLogRecord": "Scheduled Job Log Record", "PasswordChangeRequest": "Password Change Request", "ActionHistoryRecord": "Action History Record", @@ -74,6 +75,7 @@ "Attachment": "Attachments", "EmailFolder": "Email Folders", "PortalUser": "Portal Users", + "ApiUser": "API Users", "ScheduledJobLogRecord": "Scheduled Job Log Records", "PasswordChangeRequest": "Password Change Requests", "ActionHistoryRecord": "Action History", diff --git a/application/Espo/Resources/i18n/en_US/User.json b/application/Espo/Resources/i18n/en_US/User.json index 70c5b5f12e..64a79646fa 100644 --- a/application/Espo/Resources/i18n/en_US/User.json +++ b/application/Espo/Resources/i18n/en_US/User.json @@ -30,7 +30,8 @@ "ipAddress": "IP Address", "passwordPreview": "Password Preview", "isSuperAdmin": "Is Super Admin", - "lastAccess": "Last Access" + "lastAccess": "Last Access", + "apiKey": "API Key" }, "links": { "teams": "Teams", diff --git a/application/Espo/Resources/layouts/User/filters.json b/application/Espo/Resources/layouts/User/filters.json index e0c5637e6b..262fbd2678 100644 --- a/application/Espo/Resources/layouts/User/filters.json +++ b/application/Espo/Resources/layouts/User/filters.json @@ -4,7 +4,7 @@ "createdBy", "emailAddress", "title", - "isAdmin", + "type", "isActive", "position", "portals", diff --git a/application/Espo/Resources/metadata/clientDefs/ApiUser.json b/application/Espo/Resources/metadata/clientDefs/ApiUser.json new file mode 100644 index 0000000000..7f274b0a31 --- /dev/null +++ b/application/Espo/Resources/metadata/clientDefs/ApiUser.json @@ -0,0 +1,34 @@ +{ + "controller": "controllers/api-user", + "views": { + "detail": "views/user/detail", + "list": "views/api-user/list" + }, + "recordViews": { + "list": "views/user/record/list", + "detail": "views/user/record/detail", + "edit":"views/user/record/edit", + "detailSmall":"views/user/record/detail-quick", + "editSmall":"views/user/record/edit-quick" + }, + "defaultSidePanelFieldLists": { + "detail": [ + "avatar", + "createdAt", + "lastAccess" + ], + "detailSmall": [ + "avatar", + "createdAt" + ], + "edit": [ + "avatar" + ], + "editSmall": [ + "avatar" + ] + }, + "filterList": [ + ], + "boolFilterList": [] +} \ No newline at end of file diff --git a/application/Espo/Resources/metadata/entityDefs/User.json b/application/Espo/Resources/metadata/entityDefs/User.json index 2e33cc1c46..d59542fb8e 100644 --- a/application/Espo/Resources/metadata/entityDefs/User.json +++ b/application/Espo/Resources/metadata/entityDefs/User.json @@ -37,6 +37,15 @@ "disabled": true, "notStorable": true }, + "apiKey": { + "type": "varchar", + "maxLength": 36, + "readOnly": true, + "layoutMassUpdateDisabled": true, + "layoutDetailDisabled": true, + "layoutFiltersDisabled": true, + "layoutListDisabled": true + }, "salutationName": { "type": "enum", "options": ["", "Mr.", "Ms.", "Mrs.", "Dr."] diff --git a/application/Espo/Services/Note.php b/application/Espo/Services/Note.php index 7491857934..1faa44913d 100644 --- a/application/Espo/Services/Note.php +++ b/application/Espo/Services/Note.php @@ -69,7 +69,7 @@ class Note extends Record if ($preferences && $preferences->get('followEntityOnStreamPost')) { if ($this->getMetadata()->get(['scopes', $entity->get('parentType'), 'stream'])) { $parent = $this->getEntityManager()->getEntity($entity->get('parentType'), $entity->get('parentId')); - if ($parent) { + if ($parent && !$this->getUser()->isSystem() && !$this->getUser()->isApi()) { $this->getServiceFactory()->create('Stream')->followEntity($parent, $this->getUser()->id); } } diff --git a/application/Espo/Services/Stream.php b/application/Espo/Services/Stream.php index 3623a8ddb5..245e3162e1 100644 --- a/application/Espo/Services/Stream.php +++ b/application/Espo/Services/Stream.php @@ -545,7 +545,7 @@ class Stream extends \Espo\Core\Services\Base 'order' => 'DESC' ]; - if (!$user->isPortal() || $user->isAdmin()) { + if ((!$user->isPortal() || $user->isAdmin()) && !$user->isApi()) { $selectParamsList[] = [ 'select' => $select, 'leftJoins' => ['createdBy'], diff --git a/application/Espo/Services/User.php b/application/Espo/Services/User.php index 3cc7b841d6..eea066b81c 100644 --- a/application/Espo/Services/User.php +++ b/application/Espo/Services/User.php @@ -61,7 +61,8 @@ class User extends Record 'portalRolesIds', 'contactId', 'accountsIds', - 'type' + 'type', + 'apiKey' ]; protected $mandatorySelectAttributeList = [ diff --git a/client/src/controllers/api-user.js b/client/src/controllers/api-user.js new file mode 100644 index 0000000000..4dd365dcd8 --- /dev/null +++ b/client/src/controllers/api-user.js @@ -0,0 +1,63 @@ +/************************************************************************ + * This file is part of EspoCRM. + * + * EspoCRM - Open Source CRM application. + * Copyright (C) 2014-2018 Yuri Kuznetsov, Taras Machyshyn, Oleksiy Avramenko + * Website: http://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. + ************************************************************************/ + +Espo.define('controllers/api-user', 'controllers/record', function (Dep) { + + return Dep.extend({ + + entityType: 'User', + + getCollection: function (callback, context, usePreviouslyFetched) { + context = context || this; + Dep.prototype.getCollection.call(this, function (collection) { + collection.data.filterList = ['api']; + callback.call(context, collection); + }, context, usePreviouslyFetched); + }, + + createViewView: function (options, model) { + if (!model.isApi()) { + if (model.isPortal()) { + this.getRouter().dispatch('PortalUser', 'view', {id: model.id, model: model}); + return; + } + this.getRouter().dispatch('User', 'view', {id: model.id, model: model}); + return; + } + Dep.prototype.createViewView.call(this, options, model); + }, + + create: function (options) { + options = options || {}; + options.attributes = options.attributes || {}; + options.attributes.type = 'api'; + Dep.prototype.create.call(this, options); + } + + }); +}); diff --git a/client/src/controllers/portal-user.js b/client/src/controllers/portal-user.js index 936332f7ca..2752d23bc7 100644 --- a/client/src/controllers/portal-user.js +++ b/client/src/controllers/portal-user.js @@ -42,10 +42,21 @@ Espo.define('controllers/portal-user', 'controllers/record', function (Dep) { createViewView: function (options, model) { if (!model.isPortal()) { + if (model.isApi()) { + this.getRouter().dispatch('ApiUser', 'view', {id: model.id, model: model}); + return; + } this.getRouter().dispatch('User', 'view', {id: model.id, model: model}); return; } Dep.prototype.createViewView.call(this, options, model); + }, + + create: function (options) { + options = options || {}; + options.attributes = options.attributes || {}; + options.attributes.type = 'portal'; + Dep.prototype.create.call(this, options); } }); diff --git a/client/src/controllers/user.js b/client/src/controllers/user.js index b02a5a333f..f147c8ad4f 100644 --- a/client/src/controllers/user.js +++ b/client/src/controllers/user.js @@ -43,6 +43,10 @@ Espo.define('controllers/user', 'controllers/record', function (Dep) { this.getRouter().dispatch('PortalUser', 'view', {id: model.id, model: model}); return; } + if (model.isApi()) { + this.getRouter().dispatch('ApiUser', 'view', {id: model.id, model: model}); + return; + } Dep.prototype.createViewView.call(this, options, model); } diff --git a/client/src/models/user.js b/client/src/models/user.js index 1de5d91884..d0303f7d7d 100644 --- a/client/src/models/user.js +++ b/client/src/models/user.js @@ -41,6 +41,10 @@ Espo.define('models/user', 'model', function (Dep) { isApi: function () { return this.get('type') == 'api'; + }, + + isRegular: function () { + return this.get('type') == 'regular'; } }); }); diff --git a/client/src/views/api-user/list.js b/client/src/views/api-user/list.js new file mode 100644 index 0000000000..440c37a3a2 --- /dev/null +++ b/client/src/views/api-user/list.js @@ -0,0 +1,51 @@ +/************************************************************************ + * This file is part of EspoCRM. + * + * EspoCRM - Open Source CRM application. + * Copyright (C) 2014-2018 Yuri Kuznetsov, Taras Machyshyn, Oleksiy Avramenko + * Website: http://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. + ************************************************************************/ + +Espo.define('views/api-user/list', 'views/list', function (Dep) { + + return Dep.extend({ + + setup: function () { + Dep.prototype.setup.call(this); + }, + + actionCreate: function () { + var attributes = { + type: 'api' + }; + + var router = this.getRouter(); + var url = '#' + this.scope + '/create'; + router.dispatch(this.scope, 'create', { + attributes: attributes + }); + router.navigate(url, {trigger: false}); + } + + }); +}); diff --git a/client/src/views/modals/detail.js b/client/src/views/modals/detail.js index 48dcba882e..d5504f5fb6 100644 --- a/client/src/views/modals/detail.js +++ b/client/src/views/modals/detail.js @@ -370,6 +370,7 @@ Espo.define('views/modals/detail', 'views/modal', function (Dep) { var viewName = this.getMetadata().get(['clientDefs', this.scope, 'modalViews', 'edit']) || 'views/modals/edit'; this.createView('quickEdit', viewName, { scope: this.scope, + entityType: this.model.entityType, id: this.id, fullFormDisabled: this.fullFormDisabled }, function (view) { diff --git a/client/src/views/modals/edit.js b/client/src/views/modals/edit.js index e3f76b44dc..d4468c04a3 100644 --- a/client/src/views/modals/edit.js +++ b/client/src/views/modals/edit.js @@ -89,6 +89,9 @@ Espo.define('views/modals/edit', 'views/modal', function (Dep) { }); this.scope = this.scope || this.options.scope; + + this.entityType = this.options.entityType || this.scope; + this.id = this.options.id; if (!this.id) { @@ -113,7 +116,7 @@ Espo.define('views/modals/edit', 'views/modal', function (Dep) { this.waitForView('edit'); - this.getModelFactory().create(this.scope, function (model) { + this.getModelFactory().create(this.entityType, function (model) { if (this.id) { if (this.sourceModel) { model = this.model = this.sourceModel.clone(); diff --git a/client/src/views/user/detail.js b/client/src/views/user/detail.js index f5670d30f6..c0676c0e9d 100644 --- a/client/src/views/user/detail.js +++ b/client/src/views/user/detail.js @@ -101,4 +101,3 @@ Espo.define('views/user/detail', 'views/detail', function (Dep) { }, }); }); - diff --git a/client/src/views/user/record/detail-quick.js b/client/src/views/user/record/detail-quick.js index ccec01efc8..6d4c4479a8 100644 --- a/client/src/views/user/record/detail-quick.js +++ b/client/src/views/user/record/detail-quick.js @@ -26,7 +26,7 @@ * these Appropriate Legal Notices must retain the display of the "EspoCRM" word. ************************************************************************/ -Espo.define('views/user/record/detail-quick', 'views/record/detail-small', function (Dep) { +Espo.define('views/user/record/detail-quick', ['views/record/detail-small', 'views/user/record/detail'], function (Dep, Detail) { return Dep.extend({ @@ -38,9 +38,14 @@ Espo.define('views/user/record/detail-quick', 'views/record/detail-small', funct setup: function () { Dep.prototype.setup.call(this); + Detail.prototype.setupNonAdminFieldsAccess.call(this); + Detail.prototype.setupFieldAppearance.call(this); + }, + + controlFieldAppearance: function () { + Detail.prototype.controlFieldAppearance.call(this); } }); }); - diff --git a/client/src/views/user/record/detail.js b/client/src/views/user/record/detail.js index d8729ece6b..ee0910e927 100644 --- a/client/src/views/user/record/detail.js +++ b/client/src/views/user/record/detail.js @@ -50,7 +50,7 @@ Espo.define('views/user/record/detail', 'views/record/detail', function (Dep) { }); } - if (this.model.id == this.getUser().id) { + if (this.model.id == this.getUser().id && !this.model.isApi()) { this.dropdownItemList.push({ name: 'changePassword', label: 'Change Password', @@ -59,6 +59,10 @@ Espo.define('views/user/record/detail', 'views/record/detail', function (Dep) { } } + if (this.model.isPortal() || this.model.isApi()) { + this.hideActionItem('duplicate'); + } + if (this.model.id == this.getUser().id) { this.listenTo(this.model, 'after:save', function () { this.getUser().set(this.model.toJSON()); @@ -124,6 +128,11 @@ Espo.define('views/user/record/detail', 'views/record/detail', function (Dep) { if (this.model.get('type') === 'api') { this.hideField('title'); + this.hideField('emailAddress'); + this.hideField('phoneNumber'); + this.hideField('name'); + this.hideField('gender'); + } else { this.showField('title'); } diff --git a/client/src/views/user/record/edit.js b/client/src/views/user/record/edit.js index 9f790db65f..ba56073eb8 100644 --- a/client/src/views/user/record/edit.js +++ b/client/src/views/user/record/edit.js @@ -110,7 +110,7 @@ Espo.define('views/user/record/edit', ['views/record/edit', 'views/user/record/d ] }); - if (this.type == 'edit' && this.getUser().isAdmin()) { + if (this.type == 'edit' && this.getUser().isAdmin() && !this.model.isApi()) { layout.push({ label: 'Password', rows: [