diff --git a/application/Espo/Acl/ActionHistoryRecord.php b/application/Espo/Acl/ActionHistoryRecord.php new file mode 100644 index 0000000000..4e76352bf3 --- /dev/null +++ b/application/Espo/Acl/ActionHistoryRecord.php @@ -0,0 +1,42 @@ +get('userId') === $user->id; + } +} + diff --git a/application/Espo/Controllers/ActionHistoryRecord.php b/application/Espo/Controllers/ActionHistoryRecord.php new file mode 100644 index 0000000000..8d64f1f5b0 --- /dev/null +++ b/application/Espo/Controllers/ActionHistoryRecord.php @@ -0,0 +1,71 @@ +$item = $this->getConfig()->get($item); } + unset($userData['authTokenId']); + unset($userData['password']); + return array( 'user' => $userData, 'acl' => $this->getAcl()->getMap(), diff --git a/application/Espo/Controllers/AuthToken.php b/application/Espo/Controllers/AuthToken.php index ad059a727c..c2fb15e510 100644 --- a/application/Espo/Controllers/AuthToken.php +++ b/application/Espo/Controllers/AuthToken.php @@ -42,20 +42,37 @@ class AuthToken extends \Espo\Core\Controllers\Record public function actionUpdate($params, $data, $request) { - throw new Forbidden(); - } - - public function actionCreate($params, $data, $request) - { - throw new Forbidden(); - } - - public function actionListLinked($params, $data, $request) - { + if ( + is_array($data) && + array_key_exists('isActive', $data) && + $data['isActive'] === false && + count(array_keys($data)) === 1) + { + return parent::actionUpdate($params, $data, $request); + } throw new Forbidden(); } public function actionMassUpdate($params, $data, $request) + { + if (empty($data['attributes'])) { + throw new BadRequest(); + } + + $attributes = $data['attributes']; + + if ( + is_object($attributes) && + isset($attributes->isActive) && + $attributes->isActive === false && + count(array_keys(get_object_vars($attributes))) === 1 + ) { + return parent::actionMassUpdate($params, $data, $request); + } + throw new Forbidden(); + } + + public function actionCreate($params, $data, $request) { throw new Forbidden(); } diff --git a/application/Espo/Controllers/LastViewed.php b/application/Espo/Controllers/LastViewed.php new file mode 100644 index 0000000000..36050c4fb1 --- /dev/null +++ b/application/Espo/Controllers/LastViewed.php @@ -0,0 +1,46 @@ +getServiceFactory()->create('LastViewed')->get(); + + return [ + 'total' => $result['total'], + 'list' => isset($result['collection']) ? $result['collection']->toArray() : $result['list'] + ]; + } +} + diff --git a/application/Espo/Core/Controllers/Record.php b/application/Espo/Core/Controllers/Record.php index 2bd7a30e39..be0fba0c55 100644 --- a/application/Espo/Core/Controllers/Record.php +++ b/application/Espo/Core/Controllers/Record.php @@ -67,7 +67,7 @@ class Record extends Base public function actionRead($params, $data, $request) { $id = $params['id']; - $entity = $this->getRecordService()->getEntity($id); + $entity = $this->getRecordService()->readEntity($id); if (empty($entity)) { throw new NotFound(); diff --git a/application/Espo/Core/Utils/Auth.php b/application/Espo/Core/Utils/Auth.php index fec033e405..9f607251dc 100644 --- a/application/Espo/Core/Utils/Auth.php +++ b/application/Espo/Core/Utils/Auth.php @@ -109,6 +109,7 @@ class Auth } $user->set('isAdmin', $isAdmin); + $user->set('ipAddress', $_SERVER['REMOTE_ADDR']); $entityManager->setUser($user); $this->getContainer()->setUser($user); @@ -116,7 +117,10 @@ class Auth public function login($username, $password) { - $authToken = $this->getEntityManager()->getRepository('AuthToken')->where(array('token' => $password))->findOne(); + $authToken = $this->getEntityManager()->getRepository('AuthToken')->where(array( + 'token' => $password, + 'isActive' => true + ))->findOne(); if ($authToken) { if (!$this->allowAnyAccess) { @@ -167,6 +171,8 @@ class Auth $user->loadLinkMultipleField('teams'); } + $user->set('ipAddress', $_SERVER['REMOTE_ADDR']); + $this->getEntityManager()->setUser($user); $this->getContainer()->setUser($user); @@ -186,6 +192,7 @@ class Auth $this->getEntityManager()->saveEntity($authToken); $user->set('token', $authToken->get('token')); + $user->set('authTokenId', $authToken->id); } return true; @@ -201,7 +208,8 @@ class Auth { $authToken = $this->getEntityManager()->getRepository('AuthToken')->where(array('token' => $token))->findOne(); if ($authToken) { - $this->getEntityManager()->removeEntity($authToken); + $authToken->set('isActive', false); + $this->getEntityManager()->saveEntity($authToken); return true; } } diff --git a/application/Espo/Core/Utils/Database/Orm/Relations/BelongsTo.php b/application/Espo/Core/Utils/Database/Orm/Relations/BelongsTo.php index 2d758c2d39..a29662fb4a 100644 --- a/application/Espo/Core/Utils/Database/Orm/Relations/BelongsTo.php +++ b/application/Espo/Core/Utils/Database/Orm/Relations/BelongsTo.php @@ -38,6 +38,18 @@ class BelongsTo extends Base $foreignEntityName = $this->getForeignEntityName(); $foreignLinkName = $this->getForeignLinkName(); + + $noForeignName = false; + if (!empty($linkParams['noForeignName'])) { + $noForeignName = true; + } else { + if (!empty($linkParams['foreignName'])) { + $foreign = $linkParams['foreignName']; + } else { + $foreign = $this->getForeignField('name', $foreignEntityName); + } + } + if (!empty($linkParams['noJoin'])) { $fieldNameDefs = array( 'type' => 'varchar', @@ -49,15 +61,14 @@ class BelongsTo extends Base $fieldNameDefs = array( 'type' => 'foreign', 'relation' => $linkName, - 'foreign' => $this->getForeignField('name', $foreignEntityName), + 'foreign' => $foreign, 'notStorable' => false ); } - return array ( + $data = array ( $entityName => array ( 'fields' => array( - $linkName.'Name' => $fieldNameDefs, $linkName.'Id' => array( 'type' => 'foreignId', 'index' => true @@ -74,6 +85,12 @@ class BelongsTo extends Base ) ) ); + + if (!$noForeignName) { + $data[$entityName]['fields'][$linkName.'Name'] = $fieldNameDefs; + } + + return $data; } } \ No newline at end of file diff --git a/application/Espo/Core/Utils/Database/Schema/tables/actionHistoryRecord.php b/application/Espo/Core/Utils/Database/Schema/tables/actionHistoryRecord.php new file mode 100644 index 0000000000..37bf88ff3e --- /dev/null +++ b/application/Espo/Core/Utils/Database/Schema/tables/actionHistoryRecord.php @@ -0,0 +1,39 @@ + array( + 'params' => array( + 'engine' => 'InnoDB' + ) + ) + +); + diff --git a/application/Espo/Core/defaults/config.php b/application/Espo/Core/defaults/config.php index bbc0236f66..0ea459e425 100644 --- a/application/Espo/Core/defaults/config.php +++ b/application/Espo/Core/defaults/config.php @@ -156,6 +156,7 @@ return array ( "calendarEntityList" => ["Meeting", "Call", "Task"], "activitiesEntityList" => ["Meeting", "Call"], "historyEntityList" => ["Meeting", "Call", "Email"], - 'isInstalled' => false, + "lastViewedCount" => 20, + 'isInstalled' => false ); diff --git a/application/Espo/Entities/ActionHistoryRecord.php b/application/Espo/Entities/ActionHistoryRecord.php new file mode 100644 index 0000000000..4686bfdfc3 --- /dev/null +++ b/application/Espo/Entities/ActionHistoryRecord.php @@ -0,0 +1,36 @@ + true + ); if ($authTokenLifetime) { $dt = new \DateTime(); @@ -60,10 +62,11 @@ class AuthTokenControl extends \Espo\Core\Jobs\Base $whereClause['lastAccess<'] = $authTokenMaxIdleTimeThreshold; } - $tokenList = $this->getEntityManager()->getRepository('AuthToken')->where($whereClause)->limit(0, 100)->find(); + $tokenList = $this->getEntityManager()->getRepository('AuthToken')->where($whereClause)->limit(0, 500)->find(); foreach ($tokenList as $token) { - $this->getEntityManager()->removeEntity($token); + $token->set('isActive', false); + $this->getEntityManager()->saveEntity($token); } } } diff --git a/application/Espo/ORM/DB/Mapper.php b/application/Espo/ORM/DB/Mapper.php index afc1f2c3b0..42d3218f5c 100644 --- a/application/Espo/ORM/DB/Mapper.php +++ b/application/Espo/ORM/DB/Mapper.php @@ -66,9 +66,8 @@ abstract class Mapper implements IMapper } $params['whereClause']['id'] = $id; - $params['whereClause']['deleted'] = 0; - $sql = $this->query->createSelectQuery($entity->getEntityType(), $params); + $sql = $this->query->createSelectQuery($entity->getEntityType(), $params, !empty($params['withDeleted'])); $ps = $this->pdo->query($sql); @@ -103,7 +102,7 @@ abstract class Mapper implements IMapper public function select(IEntity $entity, $params = array()) { - $sql = $this->query->createSelectQuery($entity->getEntityType(), $params); + $sql = $this->query->createSelectQuery($entity->getEntityType(), $params, !empty($params['withDeleted'])); return $this->selectByQuery($entity, $sql); } diff --git a/application/Espo/ORM/DB/Query/Base.php b/application/Espo/ORM/DB/Query/Base.php index 6ffd95e972..c7fa949725 100644 --- a/application/Espo/ORM/DB/Query/Base.php +++ b/application/Espo/ORM/DB/Query/Base.php @@ -728,7 +728,11 @@ abstract class Base $alias = $this->getAlias($entity, $relationName); if ($alias) { - $leftPart = $alias . '.' . $this->toDb($fieldDefs['foreign']); + if (!is_array($fieldDefs['foreign'])) { + $leftPart = $alias . '.' . $this->toDb($fieldDefs['foreign']); + } else { + $leftPart = $this->getFieldPath($entity, $field); + } } } } diff --git a/application/Espo/ORM/Repositories/RDB.php b/application/Espo/ORM/Repositories/RDB.php index e4017d9a30..acd956da5b 100644 --- a/application/Espo/ORM/Repositories/RDB.php +++ b/application/Espo/ORM/Repositories/RDB.php @@ -511,6 +511,18 @@ class RDB extends \Espo\ORM\Repository return $this; } + public function select($select) + { + $this->listParams['select'] = $select; + return $this; + } + + public function groupBy($groupBy) + { + $this->listParams['groupBy'] = $groupBy; + return $this; + } + public function setListParams(array $params = array()) { $this->listParams = $params; diff --git a/application/Espo/Resources/i18n/en_US/ActionHistoryRecord.json b/application/Espo/Resources/i18n/en_US/ActionHistoryRecord.json new file mode 100644 index 0000000000..5a888a5b4f --- /dev/null +++ b/application/Espo/Resources/i18n/en_US/ActionHistoryRecord.json @@ -0,0 +1,28 @@ +{ + "fields": { + "user": "User", + "action": "Action", + "createdAt": "Date", + "user": "User", + "target": "Target", + "targetType": "Target Type", + "authToken": "Auth Token", + "ipAddress": "IP Address" + }, + "links": { + "authToken": "Auth Token", + "user": "User", + "target": "Target" + }, + "presetFilters": { + "onlyMy": "Only My" + }, + "options": { + "action": { + "read": "Read", + "update": "Update", + "delete": "Delete", + "create": "Create" + } + } +} diff --git a/application/Espo/Resources/i18n/en_US/Admin.json b/application/Espo/Resources/i18n/en_US/Admin.json index 1a99a1585d..da033ed4b8 100644 --- a/application/Espo/Resources/i18n/en_US/Admin.json +++ b/application/Espo/Resources/i18n/en_US/Admin.json @@ -54,7 +54,8 @@ "Notifications": "Notifications", "Jobs": "Jobs", "Reset to Default": "Reset to Default", - "Email Filters": "Email Filters" + "Email Filters": "Email Filters", + "Action History": "Action History" }, "layouts": { "list": "List", @@ -192,7 +193,8 @@ "integrations": "Integration with third-party services.", "notifications": "In-app and email notification settings.", "inboundEmails": "Settings for incoming emails.", - "emailFilters": "Emails messages that match specified filter won't be imported." + "emailFilters": "Emails messages that match specified filter won't be imported.", + "actionHistory": "Log of actions done by users." }, "options": { "previewSize": { diff --git a/application/Espo/Resources/i18n/en_US/AuthToken.json b/application/Espo/Resources/i18n/en_US/AuthToken.json index 28d25d7f9d..6df72baf5b 100644 --- a/application/Espo/Resources/i18n/en_US/AuthToken.json +++ b/application/Espo/Resources/i18n/en_US/AuthToken.json @@ -3,7 +3,21 @@ "user": "User", "ipAddress": "IP Address", "lastAccess": "Last Access Date", - "createdAt": "Login Date" - + "createdAt": "Login Date", + "isActive": "Is Active", + "portal": "Portal" + }, + "links": { + "actionHistoryRecords": "Action History" + }, + "presetFilters": { + "active": "Active", + "inactive": "Inactive" + }, + "labels": { + "Set Inactive": "Set Inactive" + }, + "massActions": { + "setInactive": "Set Inactive" } } diff --git a/application/Espo/Resources/i18n/en_US/Global.json b/application/Espo/Resources/i18n/en_US/Global.json index 25708b7d48..8a24989a1d 100644 --- a/application/Espo/Resources/i18n/en_US/Global.json +++ b/application/Espo/Resources/i18n/en_US/Global.json @@ -22,7 +22,13 @@ "PortalRole": "Portal Role", "Attachment": "Attachment", "EmailFolder": "Email Folder", - "PortalUser": "Portal User" + "PortalUser": "Portal User", + "ScheduledJobLogRecord": "Scheduled Job Log Record", + "PasswordChangeRequest": "Password Change Request", + "ActionHistoryRecord": "Action History Record", + "AuthToken": "Auth Token", + "UniqueId": "Unique ID", + "LastViewed": "Last Viewed" }, "scopeNamesPlural": { "Email": "Emails", @@ -47,7 +53,13 @@ "PortalRole": "Portal Roles", "Attachment": "Attachments", "EmailFolder": "Email Folders", - "PortalUser": "Portal Users" + "PortalUser": "Portal Users", + "ScheduledJobLogRecord": "Scheduled Job Log Records", + "PasswordChangeRequest": "Password Change Requests", + "ActionHistoryRecord": "Action History", + "AuthToken": "Auth Tokens", + "UniqueId": "Unique IDs", + "LastViewed": "Last Viewed" }, "labels": { "Misc": "Misc", diff --git a/application/Espo/Resources/i18n/en_US/Note.json b/application/Espo/Resources/i18n/en_US/Note.json index 13dbfc8102..4f441d8301 100644 --- a/application/Espo/Resources/i18n/en_US/Note.json +++ b/application/Espo/Resources/i18n/en_US/Note.json @@ -5,7 +5,8 @@ "targetType": "Target", "teams": "Teams", "users": "Users", - "portals": "Portals" + "portals": "Portals", + "type": "Type" }, "filters": { "all": "All", diff --git a/application/Espo/Resources/i18n/en_US/User.json b/application/Espo/Resources/i18n/en_US/User.json index 4971d60999..c5e18afcf1 100644 --- a/application/Espo/Resources/i18n/en_US/User.json +++ b/application/Espo/Resources/i18n/en_US/User.json @@ -25,7 +25,8 @@ "sendAccessInfo": "Send Email with Access Info to User", "portal": "Portal", "gender": "Gender", - "position": "Position in Team" + "position": "Position in Team", + "ipAddress": "IP Address" }, "links": { "teams": "Teams", diff --git a/application/Espo/Resources/layouts/ActionHistoryRecord/detail.json b/application/Espo/Resources/layouts/ActionHistoryRecord/detail.json new file mode 100644 index 0000000000..ae947287f5 --- /dev/null +++ b/application/Espo/Resources/layouts/ActionHistoryRecord/detail.json @@ -0,0 +1,24 @@ +[ + { + "label":"", + "rows": [ + [ + {"name": "user"}, + {"name": "authToken"} + ], + [ + {"name": "ipAddress", "fullWidth": true} + ], + [ + {"name": "action"}, + {"name": "createdAt"} + ], + [ + {"name": "targetType", "fullWidth": true} + ], + [ + {"name": "target", "fullWidth": true} + ] + ] + } +] \ No newline at end of file diff --git a/application/Espo/Resources/layouts/ActionHistoryRecord/detailSmall.json b/application/Espo/Resources/layouts/ActionHistoryRecord/detailSmall.json new file mode 100644 index 0000000000..1a5ca587a6 --- /dev/null +++ b/application/Espo/Resources/layouts/ActionHistoryRecord/detailSmall.json @@ -0,0 +1,22 @@ +[ + { + "label":"", + "rows": [ + [ + {"name": "user"}, + {"name": "authToken"} + ], + [ + {"name": "action"}, + {"name": "ipAddress"} + ], + [ + {"name": "targetType"}, + {"name": "createdAt"} + ], + [ + {"name": "target", "fullWidth": true} + ] + ] + } +] \ No newline at end of file diff --git a/application/Espo/Resources/layouts/ActionHistoryRecord/filters.json b/application/Espo/Resources/layouts/ActionHistoryRecord/filters.json new file mode 100644 index 0000000000..f89f25f5eb --- /dev/null +++ b/application/Espo/Resources/layouts/ActionHistoryRecord/filters.json @@ -0,0 +1,7 @@ +[ + "action", + "target", + "createdAt", + "ipAddress", + "user" +] \ No newline at end of file diff --git a/application/Espo/Resources/layouts/ActionHistoryRecord/list.json b/application/Espo/Resources/layouts/ActionHistoryRecord/list.json new file mode 100644 index 0000000000..1e8867bf47 --- /dev/null +++ b/application/Espo/Resources/layouts/ActionHistoryRecord/list.json @@ -0,0 +1,8 @@ +[ + {"name":"user", "notSortable": true}, + {"name":"action", "width": "12", "notSortable": true}, + {"name":"targetType", "width": "15", "notSortable": true}, + {"name":"target", "notSortable": true}, + {"name":"ipAddress", "width": "14", "notSortable": true}, + {"name":"createdAt", "width": "12", "align": "right", "notSortable": true} +] \ No newline at end of file diff --git a/application/Espo/Resources/layouts/ActionHistoryRecord/listLastViewed.json b/application/Espo/Resources/layouts/ActionHistoryRecord/listLastViewed.json new file mode 100644 index 0000000000..ef323fade8 --- /dev/null +++ b/application/Espo/Resources/layouts/ActionHistoryRecord/listLastViewed.json @@ -0,0 +1,4 @@ +[ + {"name":"targetType", "notSortable": true, "width": 22}, + {"name":"target", "notSortable": true} +] \ No newline at end of file diff --git a/application/Espo/Resources/layouts/ActionHistoryRecord/listSmall.json b/application/Espo/Resources/layouts/ActionHistoryRecord/listSmall.json new file mode 100644 index 0000000000..bcadcb596e --- /dev/null +++ b/application/Espo/Resources/layouts/ActionHistoryRecord/listSmall.json @@ -0,0 +1,6 @@ +[ + {"name":"action", "width": "18", "notSortable": true}, + {"name":"targetType", "width": "20", "notSortable": true}, + {"name":"target", "notSortable": true}, + {"name":"createdAt", "width": "18", "align": "right", "notSortable": true} +] \ No newline at end of file diff --git a/application/Espo/Resources/layouts/AuthToken/detail.json b/application/Espo/Resources/layouts/AuthToken/detail.json new file mode 100644 index 0000000000..8ee19d427d --- /dev/null +++ b/application/Espo/Resources/layouts/AuthToken/detail.json @@ -0,0 +1,10 @@ +[ + { + "label":"", + "rows":[ + [{"name":"user"}, {"name":"isActive"}], + [{"name":"ipAddress"}, {"name":"createdAt"}], + [{"name":"lastAccess"}, {"name":"portal"}] + ] + } +] diff --git a/application/Espo/Resources/layouts/AuthToken/detailSmall.json b/application/Espo/Resources/layouts/AuthToken/detailSmall.json new file mode 100644 index 0000000000..8ee19d427d --- /dev/null +++ b/application/Espo/Resources/layouts/AuthToken/detailSmall.json @@ -0,0 +1,10 @@ +[ + { + "label":"", + "rows":[ + [{"name":"user"}, {"name":"isActive"}], + [{"name":"ipAddress"}, {"name":"createdAt"}], + [{"name":"lastAccess"}, {"name":"portal"}] + ] + } +] diff --git a/application/Espo/Resources/layouts/AuthToken/filters.json b/application/Espo/Resources/layouts/AuthToken/filters.json new file mode 100644 index 0000000000..d4461baa86 --- /dev/null +++ b/application/Espo/Resources/layouts/AuthToken/filters.json @@ -0,0 +1,7 @@ +[ + "user", + "ipAddress", + "lastAccess", + "createdAt", + "portal" +] diff --git a/application/Espo/Resources/layouts/AuthToken/list.json b/application/Espo/Resources/layouts/AuthToken/list.json index ab0fa30608..e24851b115 100644 --- a/application/Espo/Resources/layouts/AuthToken/list.json +++ b/application/Espo/Resources/layouts/AuthToken/list.json @@ -1,6 +1,7 @@ [ - {"name":"user"}, - {"name":"ipAddress"}, - {"name":"lastAccess"}, - {"name":"createdAt"} + {"name":"user"}, + {"name":"isActive", "widthPx": "100"}, + {"name":"ipAddress", "width": "17"}, + {"name":"createdAt", "width": "19"}, + {"name":"lastAccess", "width": "19"} ] diff --git a/application/Espo/Resources/layouts/AuthToken/listSmall.json b/application/Espo/Resources/layouts/AuthToken/listSmall.json new file mode 100644 index 0000000000..e24851b115 --- /dev/null +++ b/application/Espo/Resources/layouts/AuthToken/listSmall.json @@ -0,0 +1,7 @@ +[ + {"name":"user"}, + {"name":"isActive", "widthPx": "100"}, + {"name":"ipAddress", "width": "17"}, + {"name":"createdAt", "width": "19"}, + {"name":"lastAccess", "width": "19"} +] diff --git a/application/Espo/Resources/layouts/AuthToken/relationships.json b/application/Espo/Resources/layouts/AuthToken/relationships.json new file mode 100644 index 0000000000..a046e51fba --- /dev/null +++ b/application/Espo/Resources/layouts/AuthToken/relationships.json @@ -0,0 +1,3 @@ +[ + "actionHistoryRecords" +] diff --git a/application/Espo/Resources/layouts/Note/listSmall.json b/application/Espo/Resources/layouts/Note/listSmall.json new file mode 100644 index 0000000000..d4d8eb29dd --- /dev/null +++ b/application/Espo/Resources/layouts/Note/listSmall.json @@ -0,0 +1,11 @@ +[ + { + "name": "type" + }, + { + "name": "createdBy" + }, + { + "name": "createdAt" + } +] \ No newline at end of file diff --git a/application/Espo/Resources/metadata/app/acl.json b/application/Espo/Resources/metadata/app/acl.json index f0f6a1b55e..332bae512c 100644 --- a/application/Espo/Resources/metadata/app/acl.json +++ b/application/Espo/Resources/metadata/app/acl.json @@ -49,6 +49,12 @@ "delete": "own", "create": "no" }, + "ActionHistoryRecord": { + "read": "own", + "edit": "no", + "delete": "no", + "create": "no" + }, "Role": false, "PortalRole": false }, diff --git a/application/Espo/Resources/metadata/app/aclPortal.json b/application/Espo/Resources/metadata/app/aclPortal.json index fc2a41fd21..481afc145b 100644 --- a/application/Espo/Resources/metadata/app/aclPortal.json +++ b/application/Espo/Resources/metadata/app/aclPortal.json @@ -36,6 +36,7 @@ "EmailFolder": false, "EmailAccount": false, "EmailTemplate": false, + "ActionHistoryRecord": false, "Preferences": { "read": "own", "edit": "own", diff --git a/application/Espo/Resources/metadata/app/adminPanel.json b/application/Espo/Resources/metadata/app/adminPanel.json index 5b25e4162e..79046a5383 100644 --- a/application/Espo/Resources/metadata/app/adminPanel.json +++ b/application/Espo/Resources/metadata/app/adminPanel.json @@ -76,6 +76,11 @@ "url":"#Admin/authTokens", "label":"Auth Tokens", "description":"authTokens" + }, + { + "url": "#ActionHistoryRecord", + "label": "Action History", + "description": "actionHistory" } ] }, diff --git a/application/Espo/Resources/metadata/clientDefs/ActionHistoryRecord.json b/application/Espo/Resources/metadata/clientDefs/ActionHistoryRecord.json new file mode 100644 index 0000000000..283e619bcf --- /dev/null +++ b/application/Espo/Resources/metadata/clientDefs/ActionHistoryRecord.json @@ -0,0 +1,10 @@ +{ + "controller": "controllers/record", + "createDisabled": true, + "recordViews": { + "list": "views/action-history-record/record/list" + }, + "modalViews": { + "detail": "views/action-history-record/modals/detail" + } +} diff --git a/application/Espo/Resources/metadata/clientDefs/AuthToken.json b/application/Espo/Resources/metadata/clientDefs/AuthToken.json index 70711a112d..ab367a4e0a 100644 --- a/application/Espo/Resources/metadata/clientDefs/AuthToken.json +++ b/application/Espo/Resources/metadata/clientDefs/AuthToken.json @@ -1,5 +1,23 @@ { - "recordViews":{ - "list":"Admin.AuthToken.Record.List" - } + "controller": "controllers/record", + "recordViews": { + "list": "views/admin/auth-token/record/list", + "detail": "views/admin/auth-token/record/detail", + "detailSmall": "views/admin/auth-token/record/detail-small" + }, + "modalViews": { + "detail": "views/admin/auth-token/modals/detail" + }, + "filterList": [ + "active", + "inactive" + ], + "createDisabled": true, + "relationshipPanels": { + "actionHistoryRecords": { + "create": false, + "select": false, + "rowActionsView": "views/record/row-actions/relationship-view-only" + } + } } diff --git a/application/Espo/Resources/metadata/clientDefs/LastViewed.json b/application/Espo/Resources/metadata/clientDefs/LastViewed.json new file mode 100644 index 0000000000..f382c222af --- /dev/null +++ b/application/Espo/Resources/metadata/clientDefs/LastViewed.json @@ -0,0 +1,9 @@ +{ + "controller": "controllers/last-viewed", + "views": { + "list": "views/last-viewed/list" + }, + "recordViews": { + "list": "views/last-viewed/record/list" + } +} diff --git a/application/Espo/Resources/metadata/entityDefs/ActionHistoryRecord.json b/application/Espo/Resources/metadata/entityDefs/ActionHistoryRecord.json new file mode 100644 index 0000000000..4f1216d871 --- /dev/null +++ b/application/Espo/Resources/metadata/entityDefs/ActionHistoryRecord.json @@ -0,0 +1,56 @@ +{ + "fields": { + "number": { + "type": "autoincrement", + "index": true + }, + "targetType": { + "view": "views/action-history-record/fields/target-type", + "translation": "Global.scopeNames" + }, + "target": { + "type": "linkParent", + "view": "views/action-history-record/fields/target" + }, + "data": { + "type": "jsonObject" + }, + "action": { + "type": "enum", + "options": ["read", "update", "create", "delete"] + }, + "createdAt": { + "type": "datetime" + }, + "user": { + "type": "link" + }, + "ipAddress": { + "type": "varchar", + "maxLength": "39" + }, + "authToken": { + "type": "link" + } + }, + "links": { + "user": { + "type": "belongsTo", + "entity": "User" + }, + "target": { + "type": "belongsToParent" + }, + "authToken": { + "type": "belongsTo", + "entity": "AuthToken", + "foreignName": "id", + "foreign": "actionHistoryRecords" + } + }, + "collection": { + "sortBy": "number", + "asc": false, + "textFilterFields": ["ipAddress", "userName"] + } +} diff --git a/application/Espo/Resources/metadata/entityDefs/AuthToken.json b/application/Espo/Resources/metadata/entityDefs/AuthToken.json index 6510ef6296..566e8cfff2 100644 --- a/application/Espo/Resources/metadata/entityDefs/AuthToken.json +++ b/application/Espo/Resources/metadata/entityDefs/AuthToken.json @@ -3,29 +3,40 @@ "token": { "type": "varchar", "maxLength": "36", - "index": true + "index": true, + "readOnly": true }, "hash": { "type": "varchar", "maxLength": 150, - "index": true + "index": true, + "readOnly": true }, "userId": { "type": "varchar", - "maxLength": "36" + "maxLength": "36", + "readOnly": true }, "user": { - "type": "link" + "type": "link", + "readOnly": true }, "portal": { - "type": "link" + "type": "link", + "readOnly": true }, "ipAddress": { "type": "varchar", - "maxLength": "36" + "maxLength": "36", + "readOnly": true + }, + "isActive": { + "type": "bool", + "default": true }, "lastAccess": { - "type": "datetime" + "type": "datetime", + "readOnly": true }, "createdAt": { "type": "datetime", @@ -44,11 +55,17 @@ "portal": { "type": "belongsTo", "entity": "Portal" + }, + "actionHistoryRecords": { + "type": "hasMany", + "entity": "ActionHistoryRecord", + "foreign": "authToken" } }, "collection": { "sortBy": "lastAccess", - "asc": false + "asc": false, + "textFilterFields": ["ipAddress", "userName"] }, "indexes": { "token": { diff --git a/application/Espo/Resources/metadata/entityDefs/Settings.json b/application/Espo/Resources/metadata/entityDefs/Settings.json index 25c8b1f708..0b01a9ca21 100644 --- a/application/Espo/Resources/metadata/entityDefs/Settings.json +++ b/application/Espo/Resources/metadata/entityDefs/Settings.json @@ -408,6 +408,13 @@ }, "massEmailDisableMandatoryOptOutLink": { "type": "bool" + }, + "lastViewedCount": { + "type": "int", + "min": 1, + "max": 200, + "default": 20, + "required": true } } } diff --git a/application/Espo/Resources/metadata/entityDefs/User.json b/application/Espo/Resources/metadata/entityDefs/User.json index 6cc8dde72a..ecebe3ac46 100644 --- a/application/Espo/Resources/metadata/entityDefs/User.json +++ b/application/Espo/Resources/metadata/entityDefs/User.json @@ -110,6 +110,16 @@ "notStorable": true, "disabled": true }, + "authTokenId": { + "type": "varchar", + "notStorable": true, + "disabled": true + }, + "ipAddress": { + "type": "varchar", + "notStorable": true, + "disabled": true + }, "defaultTeam": { "type": "link", "tooltip": true diff --git a/application/Espo/Resources/metadata/scopes/LastViewed.json b/application/Espo/Resources/metadata/scopes/LastViewed.json new file mode 100644 index 0000000000..5cebee1b86 --- /dev/null +++ b/application/Espo/Resources/metadata/scopes/LastViewed.json @@ -0,0 +1,7 @@ +{ + "entity": false, + "layouts": false, + "tab": false, + "acl": false, + "customizable": false +} diff --git a/application/Espo/SelectManagers/ActionHistoryRecord.php b/application/Espo/SelectManagers/ActionHistoryRecord.php new file mode 100644 index 0000000000..95e88c3b38 --- /dev/null +++ b/application/Espo/SelectManagers/ActionHistoryRecord.php @@ -0,0 +1,45 @@ +accessOnlyOwn($result); + } + + protected function accessOnlyOwn(&$result) + { + $result['whereClause'][] = array( + 'userId' => $this->getUser()->id + ); + } +} diff --git a/application/Espo/SelectManagers/AuthToken.php b/application/Espo/SelectManagers/AuthToken.php new file mode 100644 index 0000000000..b6853e2625 --- /dev/null +++ b/application/Espo/SelectManagers/AuthToken.php @@ -0,0 +1,48 @@ + true + ); + } + + protected function filterInactive(&$result) + { + $result['whereClause'][] = array( + 'isActive' => false + ); + } +} + diff --git a/application/Espo/Services/ActionHistoryRecord.php b/application/Espo/Services/ActionHistoryRecord.php new file mode 100644 index 0000000000..792e0ee312 --- /dev/null +++ b/application/Espo/Services/ActionHistoryRecord.php @@ -0,0 +1,59 @@ +get('targetId') && $entity->get('targetType')) { + $repository = $this->getEntityManager()->getRepository($entity->get('targetType')); + if ($repository) { + $target = $repository->where(array( + 'id' => $entity->get('targetId') + ))->findOne(array( + 'withDeleted' => true + )); + if ($target && $target->get('name')) { + $entity->set('targetName', $target->get('name')); + } + } + } + } +} + diff --git a/application/Espo/Services/AuthToken.php b/application/Espo/Services/AuthToken.php index 4337227684..d81ada5d15 100644 --- a/application/Espo/Services/AuthToken.php +++ b/application/Espo/Services/AuthToken.php @@ -36,5 +36,7 @@ use \Espo\Core\Exceptions\NotFound; class AuthToken extends Record { protected $internalAttributeList = ['hash', 'token']; + + protected $actionHistoryDisabled = true; } diff --git a/application/Espo/Services/LastViewed.php b/application/Espo/Services/LastViewed.php new file mode 100644 index 0000000000..1d8cc9c019 --- /dev/null +++ b/application/Espo/Services/LastViewed.php @@ -0,0 +1,74 @@ +addDependency('serviceFactory'); + $this->addDependency('metadata'); + } + + public function get() + { + $entityManager = $this->getInjection('entityManager'); + + $maxSize = $this->getConfig()->get('lastViewedCount', 20); + + $actionHistoryRecordService = $this->getInjection('serviceFactory')->create('ActionHistoryRecord'); + + $scopes = $this->getInjection('metadata')->get('scopes'); + + $targetTypeList = array_filter(array_keys($scopes), function ($item) use ($scopes) { + return !empty($scopes[$item]['object']); + }); + + $collection = $this->getEntityManager()->getRepository('ActionHistoryRecord')->where(array( + 'userId' => $this->getUser()->id, + 'action' => 'read', + 'targetType' => $targetTypeList, + ))->order('number', true)->limit(0, $maxSize)->select(['targetId', 'targetType'])->distinct()->find(); + + foreach ($collection as $i => $entity) { + $actionHistoryRecordService->loadParentNameFields($entity); + $entity->id = $i; + } + + return array( + 'total' => count($collection), + 'collection' => $collection + ); + } +} + diff --git a/application/Espo/Services/Notification.php b/application/Espo/Services/Notification.php index 446ee9bded..991fc73371 100644 --- a/application/Espo/Services/Notification.php +++ b/application/Espo/Services/Notification.php @@ -38,6 +38,8 @@ use Espo\Core\Utils\Json; class Notification extends \Espo\Services\Record { + protected $actionHistoryDisabled = true; + public function notifyAboutMentionInPost($userId, $noteId) { $notification = $this->getEntityManager()->getEntity('Notification'); diff --git a/application/Espo/Services/Record.php b/application/Espo/Services/Record.php index ce71d40891..320a381667 100644 --- a/application/Espo/Services/Record.php +++ b/application/Espo/Services/Record.php @@ -84,8 +84,12 @@ class Record extends \Espo\Core\Services\Base protected $checkForDuplicatesInUpdate = false; + protected $actionHistoryDisabled = false; + protected $duplicatingLinkList = []; + protected $listCountQueryDisabled = false; + const MAX_TEXT_COLUMN_LENGTH_FOR_LIST = 5000; const FOLLOWERS_LIMIT = 4; @@ -168,9 +172,40 @@ class Record extends \Espo\Core\Services\Base return $service; } - protected function prepareEntity($entity) + protected function processActionHistoryRecord($action, Entity $entity) { + if ($this->actionHistoryDisabled) return; + if ($this->getConfig()->get('actionHistoryDisabled')) return; + $historyRecord = $this->getEntityManager()->getEntity('ActionHistoryRecord'); + + $historyRecord->set('action', $action); + $historyRecord->set('userId', $this->getUser()->id); + $historyRecord->set('authTokenId', $this->getUser()->get('authTokenId')); + $historyRecord->set('ipAddress', $this->getUser()->get('ipAddress')); + + if ($entity) { + $historyRecord->set(array( + 'targetType' => $entity->getEntityType(), + 'targetId' => $entity->id + )); + } + + $this->getEntityManager()->saveEntity($historyRecord); + } + + public function readEntity($id) + { + if (empty($id)) { + throw new Error(); + } + $entity = $this->getEntity($id); + + if ($entity) { + $this->processActionHistoryRecord('read', $entity); + } + + return $entity; } public function getEntity($id = null) @@ -558,6 +593,9 @@ class Record extends \Espo\Core\Services\Base $this->afterCreate($entity, $data); $this->afterCreateProcessDuplicating($entity, $data); $this->prepareEntityForOutput($entity); + + $this->processActionHistoryRecord('create', $entity); + return $entity; } @@ -617,6 +655,9 @@ class Record extends \Espo\Core\Services\Base if ($this->storeEntity($entity)) { $this->afterUpdate($entity, $data); $this->prepareEntityForOutput($entity); + + $this->processActionHistoryRecord('update', $entity); + return $entity; } @@ -676,6 +717,9 @@ class Record extends \Espo\Core\Services\Base $result = $this->getRepository()->remove($entity); if ($result) { $this->afterDelete($entity); + + $this->processActionHistoryRecord('delete', $entity); + return $result; } } @@ -690,7 +734,11 @@ class Record extends \Espo\Core\Services\Base public function findEntities($params) { $disableCount = false; - if (in_array($this->entityType, $this->getConfig()->get('disabledCountQueryEntityList', array()))) { + if ( + $this->listCountQueryDisabled + || + in_array($this->entityType, $this->getConfig()->get('disabledCountQueryEntityList', [])) + ) { $disableCount = true; } @@ -752,7 +800,9 @@ class Record extends \Espo\Core\Services\Base } $disableCount = false; - if (in_array($foreignEntityName, $this->getConfig()->get('disabledCountQueryEntityList', array()))) { + if ( + in_array($this->entityType, $this->getConfig()->get('disabledCountQueryEntityList', [])) + ) { $disableCount = true; } @@ -959,6 +1009,8 @@ class Record extends \Espo\Core\Services\Base if ($repository->save($entity)) { $idsUpdated[] = $entity->id; $count++; + + $this->processActionHistoryRecord('update', $entity); } } } @@ -987,6 +1039,8 @@ class Record extends \Espo\Core\Services\Base if ($repository->save($entity)) { $idsUpdated[] = $entity->id; $count++; + + $this->processActionHistoryRecord('update', $entity); } } } @@ -1022,6 +1076,8 @@ class Record extends \Espo\Core\Services\Base if ($repository->remove($entity)) { $idsRemoved[] = $entity->id; $count++; + + $this->processActionHistoryRecord('delete', $entity); } } } @@ -1047,6 +1103,8 @@ class Record extends \Espo\Core\Services\Base if ($repository->remove($entity)) { $idsRemoved[] = $entity->id; $count++; + + $this->processActionHistoryRecord('delete', $entity); } } } diff --git a/client/res/layout-types/list-row.tpl b/client/res/layout-types/list-row.tpl index 1c7eeb2811..ae5efd03ba 100644 --- a/client/res/layout-types/list-row.tpl +++ b/client/res/layout-types/list-row.tpl @@ -1,13 +1,16 @@ <% _.each(layout, function (defs, key) { %> <% - var width = ''; - if (defs.options && defs.options.defs && defs.options.defs.params) { - width = defs.options.defs.params.width || ''; + var width = null; + if (defs.options && defs.options.defs && 'width' in defs.options.defs) { + width = (defs.options.defs.width + '%') || null; + } + if (defs.options && defs.options.defs && 'widthPx' in defs.options.defs) { + width = defs.options.defs.widthPx || null; } var align = false; - if (defs.options && defs.options.defs && defs.options.defs.params) { - align = defs.options.defs.params.align || false; + if (defs.options && defs.options.defs) { + align = defs.options.defs.align || false; } %>