my bad I completly forgot to put the heart of the code
! I gave the method without the population of the table!
I created a Doctype called “LockOut User”, which contains a field ‘User’ and a DateTime field.
To populate this table, I’ve created a scheduler that runs every 4 minutes and fetch all the lockout accounts to put them in my LockOut User table. So at this point, I have all the user who have been lockout with the time at which they were lockout. When this script runs, it also removes the account who are no longer blocked (after the time have been reached).
To make it work, I created a schedule in the hook.py ‘all’ that calls ‘max_attempt_reach()’:
from __future__ import unicode_literals
import frappe
import datetime
from datetime import timedelta
from frappe.utils import now_datetime
from frappe.utils import cint
from frappe.email.doctype.email_template.email_template import get_email_template
def max_attempt_reach():
allow_consecutive_attempts = frappe.db.get_value('System Settings', 'System Setting', 'allow_consecutive_attempts')
allow_login_after_fail= frappe.db.get_value('System Settings', 'System Setting', 'allow_login_after_fail')
accounts = frappe.db.get_all('User', None)
for account in accounts:
if cint(frappe.cache().hget('login_failed_count', account['name'])) == int(allow_consecutive_attempts ):
#Get current time
_now = now_datetime()
account_info = frappe.db.get_all('Lock Out User', filters={'name':account['name']}, fields=['*'])
if not account_info:
lock_account(account['name'])
else:
account_info = account_info[0]
diff_time = _now - account_info['lock_out_time'] #difference in time between current time and lock out time
days, seconds = diff_time.days, diff_time.seconds
diff_hour = (days * 24 * 60 * 60) + (seconds) #convert time to seconds
#if diff_hour > allow_login_after_fail, the number of attempt have been reset : ie account in unlock
if diff_hour > allow_login_after_fail:
unlock_account(account['name'])
def lock_account(user):
frappe.new_doc('Lock Out User').update({
"user": user,
"lock_out_time": now_datetime()
}).save()
frappe.db.commit()
recipient = get_recipient(user)
lockout_doc = frappe.get_doc('Lock Out User', user)
frappe.sendmail(
recipients = [recipient],
**get_email_template('User Lock Out', {'doc': lockout_doc})
)
def get_recipient(user):
users = frappe.db.get_all('User', None)
#We want to send the email to a Super Admin which account is unlock
for u in users:
user_email = frappe.db.get_value('User', u['name'], 'email')
user_roles = frappe.get_roles(user_email)
if 'System Manager' in user_roles:
lock_out_email = frappe.db.get_value('Lock Out User', user_email, 'user')
if not lock_out_email:
return user_email
def unlock_account(user):
# reset the number of attempt to 0.
# By default, even if an account is unlock, the number of attempt will remains at the maximum attempt (allow_consecutive_attempts)
frappe.cache().hset('login_failed_count', user, 0)
delete = frappe.delete_doc('Lock Out User', user)
frappe.db.commit()
Then, if the admin wants to unlock a user, they must go in ‘LockOut User’ doctype, click on a button (that I created as a field in the doctype JS)
frappe.ui.form.on('Lock Out User', {
unlock_user_btn: function(frm) {
frappe.call({
method: "unlock_user",
doc: frm.doc,
args: {
},
callback: function() {
}
});
},
});
that calls the methods in your_app.your_app.doctype.lockout_user.lockout_user.py
class LockOutUser(Document):
def unlock_user(self):
#reset the number of attempt to 0
user_roles = frappe.get_roles(frappe.session.user)
frappe.cache().hset('login_failed_count', self.user, 0)
delete = frappe.delete_doc('Lock Out User', self.user)
frappe.db.commit()
frappe.msgprint("The user have been unlocked")
The code can be clean up a bit, I did it a few months ago without knowing much of erpnext. I thought it could be a good idea to add it here.
Ideally, it could be done automatically as soon as the user is lockout and the button to unlock it could be in the ‘User’ doctype. I didn’t feel like modifying the core, so I did it this way.