diff --git a/Controller/Adminhtml/Activity.php b/Controller/Adminhtml/Activity.php
new file mode 100644
index 0000000..69d61ab
--- /dev/null
+++ b/Controller/Adminhtml/Activity.php
@@ -0,0 +1,21 @@
+resultPageFactory = $resultPageFactory;
+ }
+
+ public function execute()
+ {
+ $resultPage = $this->resultPageFactory->create();
+ $resultPage->getConfig()->getTitle()->prepend((__('User Activity')));
+
+ return $resultPage;
+ }
+}
diff --git a/Controller/Adminhtml/Activity/MassDisable.php b/Controller/Adminhtml/Activity/MassDisable.php
new file mode 100644
index 0000000..f90e11f
--- /dev/null
+++ b/Controller/Adminhtml/Activity/MassDisable.php
@@ -0,0 +1,66 @@
+resultPageFactory = $resultPageFactory;
+ $this->activityModel = $activityModel;
+
+ parent::__construct($context);
+ }
+
+ public function execute()
+ {
+ try {
+ $userIds = $this->getRequest()->getPost('selected');
+
+ if (is_array($userIds)) {
+ $this->activityModel->updateUserStatus($userIds, 0);
+
+ $this->messageManager->addSuccess(
+ __(
+ 'Disabled %1 user(s).',
+ count($userIds)
+ )
+ );
+ }
+ } catch (\Exception $e) {
+ $this->messageManager->addError($e->getMessage());
+ }
+
+ $this->_redirect('*/*/index');
+ }
+}
diff --git a/Controller/Adminhtml/Activity/MassEnable.php b/Controller/Adminhtml/Activity/MassEnable.php
new file mode 100644
index 0000000..62245bd
--- /dev/null
+++ b/Controller/Adminhtml/Activity/MassEnable.php
@@ -0,0 +1,66 @@
+resultPageFactory = $resultPageFactory;
+ $this->activityModel = $activityModel;
+
+ parent::__construct($context);
+ }
+
+ public function execute()
+ {
+ try {
+ $userIds = $this->getRequest()->getPost('selected');
+
+ if (is_array($userIds)) {
+ $this->activityModel->updateUserStatus($userIds, 1);
+
+ $this->messageManager->addSuccess(
+ __(
+ 'Enabled %1 user(s).',
+ count($userIds)
+ )
+ );
+ }
+ } catch (\Exception $e) {
+ $this->messageManager->addError($e->getMessage());
+ }
+
+ $this->_redirect('*/*/index');
+ }
+}
diff --git a/Helper/Data.php b/Helper/Data.php
new file mode 100644
index 0000000..f560f4b
--- /dev/null
+++ b/Helper/Data.php
@@ -0,0 +1,68 @@
+dateFactory->create()->gmtDate();
+ }
+
+ /**
+ * Get difference between two dates. Return the number of days.
+ *
+ * @param string $dateFrom
+ * @param $dateTo
+ * @return float
+ */
+ public function getDateDiff($dateTo, $dateFrom = 'now')
+ {
+ if ($dateFrom == 'now') {
+ $dateFrom = $this->getNow();
+ }
+
+ return round(abs(strtotime($dateFrom) - strtotime($dateTo))/86400);
+ }
+
+ /**
+ * Get module configuration values from core_config_data
+ *
+ * @param $setting
+ * @return mixed
+ */
+ public function getConfig($setting)
+ {
+ return $this->scopeConfig->getValue(
+ $this->tab . '/' . $setting,
+ \Magento\Store\Model\ScopeInterface::SCOPE_STORE
+ );
+ }
+}
diff --git a/Model/Activity.php b/Model/Activity.php
new file mode 100755
index 0000000..14c5a76
--- /dev/null
+++ b/Model/Activity.php
@@ -0,0 +1,26 @@
+_init(\Magenizr\AdminUser\Model\ResourceModel\Activity::class);
+ }
+ // @codingStandardsIgnoreEnd
+}
diff --git a/Model/ResourceModel/Activity.php b/Model/ResourceModel/Activity.php
new file mode 100755
index 0000000..aef9d57
--- /dev/null
+++ b/Model/ResourceModel/Activity.php
@@ -0,0 +1,47 @@
+_init(Helper::TABLE_USER, 'user_id');
+ }
+ // @codingStandardsIgnoreEnd
+
+ /**
+ * Update the user status
+ *
+ * @param $userIds
+ * @param $status
+ * @return mixed
+ */
+ public function updateUserStatus($userIds, $status)
+ {
+ if (!is_array($userIds)) {
+ $userIds = [$userIds];
+ }
+
+ return $this->getConnection()->update(
+ $this->getMainTable(),
+ ['is_active' => $status],
+ $this->getIdFieldName() . ' IN (' . $this->getConnection()->quote($userIds) . ')'
+ );
+ }
+}
diff --git a/Model/ResourceModel/Activity/Collection.php b/Model/ResourceModel/Activity/Collection.php
new file mode 100755
index 0000000..98f3a57
--- /dev/null
+++ b/Model/ResourceModel/Activity/Collection.php
@@ -0,0 +1,26 @@
+_init(\Magenizr\AdminUser\Model\Activity::class, \Magenizr\AdminUser\Model\ResourceModel\Activity::class);
+ }
+ // @codingStandardsIgnoreEnd
+}
diff --git a/Model/ResourceModel/Activity/Grid/Collection.php b/Model/ResourceModel/Activity/Grid/Collection.php
new file mode 100644
index 0000000..872bd2a
--- /dev/null
+++ b/Model/ResourceModel/Activity/Grid/Collection.php
@@ -0,0 +1,141 @@
+aggregations;
+ }
+
+ /**
+ * @param AggregationInterface $aggregations
+ * @return $this
+ */
+ public function setAggregations($aggregations)
+ {
+ $this->aggregations = $aggregations;
+ }
+
+ /**
+ * Get search criteria.
+ *
+ * @return \Magento\Framework\Api\SearchCriteriaInterface|null
+ */
+ public function getSearchCriteria()
+ {
+ return null;
+ }
+
+ /**
+ * Set search criteria.
+ *
+ * @param \Magento\Framework\Api\SearchCriteriaInterface $searchCriteria
+ * @return $this
+ * @SuppressWarnings(PHPMD.UnusedFormalParameter)
+ */
+ public function setSearchCriteria(\Magento\Framework\Api\SearchCriteriaInterface $searchCriteria = null)
+ {
+ return $this;
+ }
+
+ /**
+ * Get total count.
+ *
+ * @return int
+ */
+ public function getTotalCount()
+ {
+ return $this->getSize();
+ }
+
+ /**
+ * Set total count.
+ *
+ * @param int $totalCount
+ * @return $this
+ * @SuppressWarnings(PHPMD.UnusedFormalParameter)
+ */
+ public function setTotalCount($totalCount)
+ {
+ return $this;
+ }
+
+ /**
+ * Set items list.
+ *
+ * @param \Magento\Framework\Api\ExtensibleDataInterface[] $items
+ * @return $this
+ * @SuppressWarnings(PHPMD.UnusedFormalParameter)
+ */
+ public function setItems(array $items = null)
+ {
+ return $this;
+ }
+}
diff --git a/README.md b/README.md
new file mode 100755
index 0000000..29e7f0c
--- /dev/null
+++ b/README.md
@@ -0,0 +1,67 @@
+This module will help you to identify rarely used admin accounts and allow you to disable or delete them directly.
+
+![Magenizr AdminUser - Backend](https://images2.imgbox.com/da/8f/wcLj1jC4_o.gif)
+
+## Business Value
+The more admin user accounts are enabled in Magento®, the higher is the risk of getting compromised.
+
+* Disabling rarely used admin accounts will lower the risk of getting compromised.
+* It helps your team and client to keep admin user accounts under control.
+
+## Features
+* It shows rarely used admin user accounts at the beginning on the list.
+* Bulk `enable` or `disable` admin accounts.
+
+## Usage
+Go to `System > Permissions > User Activity` to see all admin users sorted by the last login date.
+
+## System Requirements
+- Magento 2.3.x, 2.4.x
+- PHP 5.6.x, 7.x
+
+## Installation (Composer)
+
+1. Update your composer.json `composer require "magenizr/magento2-adminuser":"1.0.0" --no-update`
+2. Install dependencies and update your composer.lock `composer update --lock`
+
+```
+./composer.json has been updated
+Loading composer repositories with package information
+Updating dependencies (including require-dev)
+Package operations: 1 install, 0 updates, 0 removals
+ - Installing magenizr/magento2-adminuser (1.0.0): Downloading (100%)
+Writing lock file
+Generating autoload files
+```
+
+3. Enable the module and clear static content.
+
+```
+php bin/magento module:enable Magenizr_AdminUser --clear-static-content
+php bin/magento setup:upgrade
+```
+
+## Installation (Manually)
+
+1. Download the code.
+2. Extract the downloaded tar.gz file. Example: `tar -xzf Magenizr_AdminUser_1.0.0.tar.gz`.
+3. Copy the code into `./app/code/Magenizr/AdminUser/`.
+4. Enable the module and clear static content.
+
+```
+php bin/magento module:enable Magenizr_AdminUser --clear-static-content
+php bin/magento setup:upgrade
+```
+
+## Support
+If you have any issues with this extension, open an issue on [GitHub](https://github.com/magenizr/Magenizr_AdminUser/issues).
+
+## Contact
+Follow us on [GitHub](https://github.com/magenizr), [Twitter](https://twitter.com/magenizr) and [Facebook](https://www.facebook.com/magenizr).
+
+## History
+===== 1.0.0 =====
+* Stable version
+
+## License
+[OSL - Open Software Licence 3.0](https://opensource.org/licenses/osl-3.0.php)
diff --git a/Ui/Component/Listing/Column/Highlight.php b/Ui/Component/Listing/Column/Highlight.php
new file mode 100644
index 0000000..4b9dcdf
--- /dev/null
+++ b/Ui/Component/Listing/Column/Highlight.php
@@ -0,0 +1,91 @@
+urlBuilder = $urlBuilder;
+ $this->helper = $helper;
+ parent::__construct($context, $uiComponentFactory, $components, $data);
+ }
+
+ /**
+ * @param array $dataSource
+ * @return array
+ */
+ public function prepareDataSource(array $dataSource)
+ {
+ if (isset($dataSource['data']['items'])) {
+ foreach ($dataSource['data']['items'] as & $item) {
+ if (isset($item['user_id'])) {
+ $logDate = $item['logdate'];
+
+ if (!empty($item['logdate'])) {
+ // Highlight row after X days
+ $daysHighlight = (int)$this->helper->getConfig('highlight_users_after_days');
+ $daysDiff = (int)$this->helper->getDateDiff('now', $logDate);
+
+ if ($daysDiff >= $daysHighlight) {
+ $item['highlight'] = 'warning';
+ }
+ } else {
+ // Highlight row if logdate is empty
+ $item['highlight'] = 'warning';
+ }
+
+ // Highlight row if failed logins were registered
+ if ($item['failures_num'] > 0) {
+ $item['highlight'] = 'danger';
+ }
+ }
+ }
+ }
+
+ return $dataSource;
+ }
+}
diff --git a/Ui/Component/Listing/Column/Status.php b/Ui/Component/Listing/Column/Status.php
new file mode 100755
index 0000000..4df5831
--- /dev/null
+++ b/Ui/Component/Listing/Column/Status.php
@@ -0,0 +1,30 @@
+ 0, 'label' => __('Disabled')],
+ ['value' => 1, 'label' => __('Enabled')]
+ ];
+ }
+}
diff --git a/Ui/Component/Listing/Column/Username.php b/Ui/Component/Listing/Column/Username.php
new file mode 100644
index 0000000..30f8fe4
--- /dev/null
+++ b/Ui/Component/Listing/Column/Username.php
@@ -0,0 +1,79 @@
+urlBuilder = $urlBuilder;
+ parent::__construct($context, $uiComponentFactory, $components, $data);
+ }
+
+ /**
+ * @param array $dataSource
+ * @return array
+ */
+ public function prepareDataSource(array $dataSource)
+ {
+ $template = '%s';
+
+ if (isset($dataSource['data']['items'])) {
+ foreach ($dataSource['data']['items'] as & $item) {
+ if (isset($item['user_id'])) {
+ $userId = $item['user_id'];
+ $url = $this->context->getUrl('adminhtml/user/edit/user_id', ['user_id' => $userId]);
+
+ $username = $item['username'];
+
+ $item['username'] = sprintf($template, $url, $username);
+ }
+ }
+ }
+
+ return $dataSource;
+ }
+}
diff --git a/composer.json b/composer.json
new file mode 100755
index 0000000..815517f
--- /dev/null
+++ b/composer.json
@@ -0,0 +1,37 @@
+{
+ "name": "magenizr/magento2-adminuser",
+ "description": "Identify rarely used admin accounts. It shows you the date and time of the last login as well as failed login attempts.",
+ "type": "magento2-module",
+ "version": "1.0.0",
+ "license": [
+ "OSL-3.0"
+ ],
+ "authors": [
+ {
+ "name": "Magenizr",
+ "email": "modules@magenizr.com",
+ "homepage": "https://agency.magenizr.com"
+ }
+ ],
+ "repositories": [
+ {
+ "type": "composer",
+ "url": "https://repo.magento.com/"
+ }
+ ],
+ "keywords": [
+ "Magento 2",
+ "Admin",
+ "Backend",
+ "User",
+ "Activity"
+ ],
+ "autoload": {
+ "files": [
+ "registration.php"
+ ],
+ "psr-4": {
+ "Magenizr\\AdminUser\\": ""
+ }
+ }
+}
diff --git a/etc/acl.xml b/etc/acl.xml
new file mode 100755
index 0000000..459a3c9
--- /dev/null
+++ b/etc/acl.xml
@@ -0,0 +1,25 @@
+
+
+
+
Stores > Configuration > Advanced > Admin for module settings.', $this->getUrl('adminhtml/system_config/edit/section/admin')); ?>
++ |
+ |