Add option "Maximum Login Attempts" for maintenance login

This commit is contained in:
Abhishek Kumar
2024-05-23 12:25:43 +05:30
parent fb5d0a32b8
commit 8dfa5e94dc
7 changed files with 180 additions and 104 deletions

View File

@@ -19,23 +19,15 @@
* @license https://store.webkul.com/license.html
*/
class MaintenanceAccessCore extends ObjectModel
class MaintenanceAccessCore extends ObjectModel
{
public $id_maintenance_access;
public $ip_address;
public $email;
public $date_add;
const USERNAME_ATTEMPTS_PER_QUARTER_HOUR = 3;
const IP_ATTEMPTS_QUARTER_HOUR = 12;
const USERNAME_ATTEMPTS_PER_HOUR = 6;
const IP_ATTEMPTS_PER_HOUR = 24;
const QUARTER_HOUR = 60*15;
const HOUR = 60*60;
/**
* @see ObjectModel::$definition
*/
const LOGIN_ATTEMPTS_WINDOW = 30; /* in minutes */
public static $definition = array(
'table' => 'maintenance_access',
'primary' => 'id_maintenance_access',
@@ -46,51 +38,32 @@ class MaintenanceAccessCore extends ObjectModel
),
);
public function cleanData()
public function removeFailedAttempts($email, $ipAddress)
{
$time = (time() - (self::HOUR*2));
Db::getInstance()->execute(
'DELETE FROM `' . _DB_PREFIX_ .'maintenance_access`
WHERE `date_add` < "' . pSQL(date("Y-m-d H:i:s", $time)) . '"'
'DELETE FROM `' . _DB_PREFIX_ .'maintenance_access` ma
WHERE (ma.`email` = "'.$email.'" OR ma.`ip_address` = "'.$ipAddress.'")
AND ma.`date_add` > "'.date('Y-m-d H:i:s', strtotime('-'.MaintenanceAccess::LOGIN_ATTEMPTS_WINDOW.' minutes')).'"'
);
}
public function getUserFailedCount($time, $email=false, $ip_address=false, $attempts)
public function getAttemptsCount($email, $ipAddress)
{
$sql = "SELECT COUNT(`id_maintenance_access`) FROM `". _DB_PREFIX_ ."maintenance_access` WHERE `date_add` > '"
. pSQL(date('Y-m-d H:i:s', $time)) ."'";
if ($email) {
$sql .= " AND `email` = '".pSQL($email)."'";
}
if ($ip_address) {
$sql .= " AND `ip_address` = '".pSQL($ip_address)."'";
}
return Db::getInstance()->getValue($sql) >= $attempts;
return Db::getInstance()->getValue(
'SELECT COUNT(ma.`id_maintenance_access`)
FROM `'._DB_PREFIX_.'maintenance_access` ma
WHERE (ma.`email` = "'.$email.'" OR ma.`ip_address` = "'.$ipAddress.'")
AND ma.`date_add` > "'.date('Y-m-d H:i:s', strtotime('-'.MaintenanceAccess::LOGIN_ATTEMPTS_WINDOW.' minutes')).'"'
);
}
public function checkLimit($email)
public function getLastAttempt($email, $ipAddress)
{
$has_error = false;
$minutes_ago = time() - self::QUARTER_HOUR;
$hours_ago = time() - self::HOUR;
$ip_address = Tools::getRemoteAddr();
$has_error |= $this->getUserFailedCount(
$minutes_ago, $email, false, self::USERNAME_ATTEMPTS_PER_QUARTER_HOUR
)? true : $has_error;
$has_error |= $this->getUserFailedCount(
$minutes_ago, false, $ip_address, self::IP_ATTEMPTS_QUARTER_HOUR
)? true : $has_error;
$has_error |= $this->getUserFailedCount(
$hours_ago, $email, false, self::USERNAME_ATTEMPTS_PER_HOUR
)? true : $has_error;
$has_error |= $this->getUserFailedCount(
$hours_ago, false, $ip_address, self::IP_ATTEMPTS_PER_HOUR
)? true : $has_error;
return $has_error;
return Db::getInstance()->getRow(
'SELECT *
FROM `'._DB_PREFIX_.'maintenance_access` ma
WHERE (ma.`email` = "'.$email.'" OR ma.`ip_address` = "'.$ipAddress.'")
ORDER BY ma.`date_add` DESC'
);
}
}
}

View File

@@ -776,6 +776,7 @@ class FrontControllerCore extends Controller
}
if (Tools::isSubmit('SubmitLogin')
&& !Configuration::get('PS_SHOP_ENABLE')
&& Configuration::get('PS_ALLOW_EMP')
&& !Context::getContext()->cookie->enable_maintenance_view
) {
@@ -812,24 +813,31 @@ class FrontControllerCore extends Controller
protected function processLogin($email, $passwd)
{
$result = false;
$remoteIpAddress = Tools::getRemoteAddr();
$objMaintenanceAccess = new MaintenanceAccess();
$attemptsCount = $objMaintenanceAccess->getAttemptsCount($email, $remoteIpAddress); // does not include the current one
$maxAttempts = Configuration::get('PS_ALLOW_EMP_MAX_ATTEMPTS');
/* Check fields validity */
if (empty($email)) {
$this->errors[] = Tools::displayError('Email is empty.');
$this->errors[] = Tools::displayError('The Email address field is blank.');
} elseif (!Validate::isEmail($email)) {
$this->errors[] = Tools::displayError('Invalid email address.');
}
if (empty($passwd)) {
$this->errors[] = Tools::displayError('The password field is blank.');
$this->errors[] = Tools::displayError('The Password field is blank.');
} elseif (!Validate::isPasswd($passwd)) {
$this->errors[] = Tools::displayError('Invalid password.');
}
if ($objMaintenanceAccess->checkLimit($email)) {
$this->errors[] =Tools::displayError('You have exceeded the limit of login attempts, please try after some time');
if (!count($this->errors)) {
if ($attemptsCount >= $maxAttempts) {
if ($lastAttempt = $objMaintenanceAccess->getLastAttempt($email, $remoteIpAddress)) {
$minutes = round((time() - strtotime($lastAttempt['date_add'])) / 60);
$this->errors[] = Tools::displayError(sprintf('You have exceeded the limit of login attempts, please try after %d minutes.', MaintenanceAccess::LOGIN_ATTEMPTS_WINDOW - $minutes));
}
}
}
if (!count($this->errors)) {
@@ -837,6 +845,7 @@ class FrontControllerCore extends Controller
$this->context->employee = new Employee();
$is_employee_loaded = $this->context->employee->getByEmail($email, $passwd);
$employee_associated_shop = $this->context->employee->getAssociatedShops();
if (!$is_employee_loaded) {
$this->errors[] = Tools::displayError('The Employee does not exist, or the password provided is incorrect.');
$this->context->employee->logout();
@@ -847,21 +856,40 @@ class FrontControllerCore extends Controller
// Update cookie
$cookie = Context::getContext()->cookie;
$cookie->enable_maintenance_view = $this->context->employee->id;
$cookie->remote_addr = ip2long(Tools::getRemoteAddr());
$cookie->remote_addr = ip2long($remoteIpAddress);
$cookie->write();
// Reset attempts count on successful login
$objMaintenanceAccess->removeFailedAttempts($email, $remoteIpAddress);
}
if (count($this->errors)) {
$this->context->smarty->assign('errors', $this->errors);
$objMaintenanceAccess = new MaintenanceAccess();
$objMaintenanceAccess->email = $email;
$objMaintenanceAccess->ip_address = Tools::getRemoteAddr();
$objMaintenanceAccess->ip_address = $remoteIpAddress;
$objMaintenanceAccess->save();
if ($attemptsCount < $maxAttempts) {
$attemptsLeft = $maxAttempts - $attemptsCount - 1;
if ($attemptsLeft > 0) {
if ($attemptsLeft > 1) {
$this->errors[] = Tools::displayError(sprintf('%d attempts left.', ($maxAttempts - $attemptsCount - 1)));
} else {
$this->errors[] = Tools::displayError(sprintf('%d attempt left.', ($maxAttempts - $attemptsCount - 1)));
}
} else {
$this->errors[] = Tools::displayError(sprintf('You have reached the limit of login attempts, please try after %d minutes.', MaintenanceAccess::LOGIN_ATTEMPTS_WINDOW));
}
}
$this->context->smarty->assign('errors', $this->errors);
} else {
Tools::redirect('index.php');
}
} else {
$this->context->smarty->assign('errors', $this->errors);
}
return $result;
}
/**

View File

@@ -54,7 +54,16 @@ class AdminMaintenanceControllerCore extends AdminController
'validation' => 'isBool',
'cast' => 'intval',
'type' => 'bool',
'class' => "hello"
'form_group_class' => (Tools::getValue('PS_SHOP_ENABLE', Configuration::get('PS_SHOP_ENABLE'))) ? 'collapse' : '',
),
'PS_ALLOW_EMP_MAX_ATTEMPTS' => array(
'title' => $this->l('Maximum Login Attempts'),
'validation' => 'isUnsignedInt',
'cast' => 'intval',
'type' => 'text',
'class' => 'fixed-width-xl',
'desc' => $this->l('Set the number of maximum login attempts allowed in 30 minutes.'),
'form_group_class' => ((Tools::getValue('PS_SHOP_ENABLE', Configuration::get('PS_SHOP_ENABLE'))) || !(Tools::getValue('PS_ALLOW_EMP', Configuration::get('PS_ALLOW_EMP')))) ? ' collapse' : '',
),
'PS_MAINTENANCE_IP' => array(
'title' => $this->l('Maintenance IP'),
@@ -69,6 +78,41 @@ class AdminMaintenanceControllerCore extends AdminController
);
}
public function postProcess()
{
if (Tools::isSubmit('submitOptionsconfiguration')) {
$shopEnable = Tools::getValue('PS_SHOP_ENABLE');
$allowEmp = Tools::getValue('PS_ALLOW_EMP');
$allowEmpMaxTries = trim(Tools::getValue('PS_ALLOW_EMP_MAX_ATTEMPTS'));
$maintenanceIp = trim(Tools::getValue('PS_MAINTENANCE_IP'));
// validations
if (!$shopEnable && $allowEmp) {
if (!$allowEmpMaxTries) {
$this->errors[] = $this->l('Maximum Tries is a required field.');
} elseif (!Validate::isUnsignedInt($allowEmpMaxTries)) {
$this->errors[] = $this->l('Maximum Tries is invalid.');
}
}
// update values
if (!count($this->errors)) {
Configuration::updateValue('PS_SHOP_ENABLE', $shopEnable);
Configuration::updateValue('PS_MAINTENANCE_IP', $maintenanceIp);
if (!$shopEnable) {
Configuration::updateValue('PS_ALLOW_EMP', $allowEmp);
if ($allowEmp) {
Configuration::updateValue('PS_ALLOW_EMP_MAX_ATTEMPTS', $allowEmpMaxTries);
}
}
Tools::redirectAdmin(self::$currentIndex.'&token='.$this->token.'&conf=6');
}
}
}
public function setMedia()
{
parent::setMedia();

View File

@@ -92,6 +92,12 @@
<configuration id="PS_SHOP_ENABLE" name="PS_SHOP_ENABLE">
<value>1</value>
</configuration>
<configuration id="PS_ALLOW_EMP" name="PS_ALLOW_EMP">
<value>0</value>
</configuration>
<configuration id="PS_ALLOW_EMP_MAX_ATTEMPTS" name="PS_ALLOW_EMP_MAX_ATTEMPTS">
<value>3</value>
</configuration>
<configuration id="PS_NB_DAYS_NEW_PRODUCT" name="PS_NB_DAYS_NEW_PRODUCT">
<value>20</value>
</configuration>

View File

@@ -1,22 +1,52 @@
$(document).ready(function(){
/**
* Since 2010 Webkul.
*
* NOTICE OF LICENSE
*
* All right is reserved,
* Please go through this link for complete license : https://store.webkul.com/license.html
*
* DISCLAIMER
*
* Do not edit or add to this file if you wish to upgrade this module to newer
* versions in the future. If you wish to customize this module for your
* needs please refer to https://store.webkul.com/customisation-guidelines/ for more information.
*
* @author Webkul IN <support@webkul.com>
* @copyright Since 2010 Webkul IN
* @license https://store.webkul.com/license.html
*/
if ($("#PS_SHOP_ENABLE_on").is(':checked')) {
$('#conf_id_PS_ALLOW_EMP').hide();
}
if ($("#PS_SHOP_ENABLE_off").is(':checked')) {
$("#conf_id_PS_ALLOW_EMP").show();
function manageFieldMaximumAttempts(state) {
if (state) {
$('[name="PS_ALLOW_EMP_MAX_ATTEMPTS"]').closest('.form-group').show(200);
} else {
$('[name="PS_ALLOW_EMP_MAX_ATTEMPTS"]').closest('.form-group').hide(200);
}
}
$(".clicker.blue").on('click', function(){
$('.hiddendiv').show('slow');
$(document).ready(function() {
// manage fields visibility
$(document).on('change', '[name="PS_SHOP_ENABLE"]', function () {
if (parseInt($('[name="PS_SHOP_ENABLE"]:checked').val())) {
$('[name="PS_ALLOW_EMP"]').closest('.form-group').hide(200);
manageFieldMaximumAttempts(false);
} else {
$('[name="PS_ALLOW_EMP"]').closest('.form-group').show(200);
manageFieldMaximumAttempts(parseInt($('[name="PS_ALLOW_EMP"]:checked').val()));
}
});
$("#cancelLogin").on('click', function(){
$('.hiddendiv').hide('slow');
$(document).on('change', '[name="PS_ALLOW_EMP"]', function () {
manageFieldMaximumAttempts(parseInt($('[name="PS_ALLOW_EMP"]:checked').val()));
});
$("input[type='radio'][name='PS_SHOP_ENABLE']").change(function() {
$("#conf_id_PS_ALLOW_EMP").toggle('slow');
$('.clicker.blue').on('click', function() {
$('.login-form-wrap').toggle(200);
});
$('#cancelLogin').on('click', function() {
$('.login-form-wrap').hide(200);
});
// manage dropdowns

View File

@@ -39,8 +39,7 @@ p {
cursor: pointer;
}
.hiddendiv {
display: none;
.login-form-wrap {
width: 300px;
}

View File

@@ -98,37 +98,33 @@
<p>{l s='Thanks for your patience!'}</p>
{if isset($allowEmployee) && $allowEmployee}
<div>
<p class="clicker blue" tabindex="1">{l s='Are you member?'}</p>
<div class="hiddendiv">
<div class="allow-conatainer">
<form action="index.php" method="post">
<div class="form_content clearfix">
<div class="form-group form-ok">
<label class="" for="email">{l s='Email address'}</label>
<br>
<input class="form-control" placeholder="Email" type="email" id="email"
name="email" value="">
</div>
<div class="form-group form-ok">
<label class="" for="passwd">{l s='Password'}</label>
<br>
<input class="form-control" type="password" placeholder="Password" id="passwd"
name="passwd" value="">
</div>
<button type="submit" id="SubmitLogin" name="SubmitLogin" class="btn btn-primary">
<span>
{l s='Log in'}
</span>
</button>
<button type="button" id="cancelLogin" name="cancelLogin"
class="btn btn-primary cancel-login">
<span>
{l s='Cancel'}
</span>
</button>
<p class="clicker blue" tabindex="1">{l s='Are you a member?'}</p>
<div class="login-form-wrap" {if !isset($smarty.post.SubmitLogin)}style="display:none;"{/if}>
<form action="" method="post">
<div class="form_content clearfix">
<div class="form-group form-ok">
<label class="" for="email">{l s='Email address'}</label>
<br>
<input class="form-control" placeholder="{l s='Email'}" type="email" id="email" name="email" {if isset($smarty.post.SubmitLogin)}value="{$smarty.post.email|escape:'html':'UTF-8'}"{/if}>
</div>
</form>
</div>
<div class="form-group form-ok">
<label class="" for="passwd">{l s='Password'}</label>
<br>
<input class="form-control" type="password" placeholder="{l s='Password'}" id="passwd" name="passwd" value="">
</div>
<button type="submit" id="SubmitLogin" name="SubmitLogin" class="btn btn-primary">
<span>
{l s='Log in'}
</span>
</button>
<button type="button" id="cancelLogin" name="cancelLogin"
class="btn btn-primary cancel-login">
<span>
{l s='Cancel'}
</span>
</button>
</div>
</form>
</div>
</div>
{/if}