debug(f'{params_kw=}') ns = { "username":params_kw.username, "password":password_encode(params_kw.password) } info(f'{ns=}') db = DBPools() dbname = get_module_dbname('rbac') async with db.sqlorContext(dbname) as sor: # Check lockout atomically in SQL r = await sor.sqlExe("""select * from users where username=${username}$ and not ( login_fail_count >= 3 and last_login_fail is not null and last_login_fail > DATE_SUB(NOW(), INTERVAL 300 SECOND) )""", ns.copy()) if len(r) == 0: # User not found or locked out r2 = await sor.sqlExe('select username from users where username=${username}$', ns.copy()) if len(r2) == 0: msg = "user name or password error" else: msg = "Account locked due to too many failed login attempts. Please try again in 5 minutes." return { "widgettype":"Error", "options":{ "timeout":5, "title":"Login Error", "message": msg } } user = r[0] # Verify password r = await sor.sqlExe('select * from users where username=${username}$ and password=${password}$', ns.copy()) if len(r) == 0: # Atomically increment fail count now_str = curDateString('%Y-%m-%d %H:%M:%S') await sor.sqlExe(""" UPDATE users SET login_fail_count = login_fail_count + 1, last_login_fail = ${now}$ WHERE id = ${id}$ """, {'id': user.id, 'now': now_str}) new_fail_count = (getattr(user, 'login_fail_count', 0) or 0) + 1 if new_fail_count >= 3: msg = "Too many failed attempts. Account locked for 5 minutes." else: msg = f"user name or password error ({3 - new_fail_count} attempts remaining)" return { "widgettype":"Error", "options":{ "timeout":3, "title":"Login Error", "message": msg } } # Success - atomically reset counters and update last_login await sor.sqlExe(""" UPDATE users SET login_fail_count = 0, last_login_fail = NULL, last_login = NOW() WHERE id = ${id}$ """, {'id': user.id}) await remember_user(r[0].id, username=r[0].username, userorgid=r[0].orgid) return { "widgettype":"Message", "options":{ "timeout":3, "auto_open":True, "title":"Login", "message":f"{r[0].username} Welcome back" }, "binds":[ { "wid":"self", "event":"dismissed", "actiontype":"urlwidget", "target":"window.user_container", "options":{ "url":entire_url('/rbac/user/userinfo.ui') } }, { "wid":"self", "event":"dismissed", "actiontype":"script", "target": f'body.login_window', "script":"this.destroy()" } ] } return { "widgettype":"Error", "options":{ "timeout":3, "title":"Login Error", "message":"system error" } }