- login.ui 密码/手机登录透传 login_source - up_login 条件化 /index.ui 跳转(仅主动登录刷新) - code_login 三处登录分支收敛 _login_msg,auto 时不 dispatch
162 lines
4.6 KiB
Plaintext
162 lines
4.6 KiB
Plaintext
debug(f'{params_kw=}')
|
||
ns = {
|
||
"username":params_kw.username,
|
||
"password":password_encode(params_kw.password)
|
||
}
|
||
|
||
async def _audit_login(sor, user_id, username, action, result='ok'):
|
||
"""登录审计:检查系统是否有审计模块,有则写入。"""
|
||
try:
|
||
from app_audit import audit_log
|
||
except ImportError:
|
||
return
|
||
try:
|
||
ip = request.get('client_ip', '') if hasattr(request, 'get') else ''
|
||
await audit_log(sor, user_id, username, action, result=result, client_ip=ip)
|
||
except Exception:
|
||
pass
|
||
|
||
info(f'{ns=}')
|
||
db = DBPools()
|
||
dbname = get_module_dbname('rbac')
|
||
async with db.sqlorContext(dbname) as sor:
|
||
# Get user record with lockout fields (standard SQL)
|
||
r = await sor.sqlExe('select * from users where username=${username}$', ns.copy())
|
||
if len(r) == 0:
|
||
await _audit_login(sor, None, ns['username'], 'login_fail', 'fail')
|
||
return {
|
||
"status": "error",
|
||
"data": {
|
||
"message": "user name or password error"
|
||
},
|
||
"widgettype":"Error",
|
||
"options":{
|
||
"timeout":3,
|
||
"title":"Login Error",
|
||
"message":"user name or password error"
|
||
}
|
||
}
|
||
user = r[0]
|
||
|
||
# Lockout check in Python (DB-agnostic)
|
||
fail_count = getattr(user, 'login_fail_count', 0) or 0
|
||
last_fail = getattr(user, 'last_login_fail', None)
|
||
if fail_count >= 3 and last_fail:
|
||
try:
|
||
stored = datetime.datetime.strptime(str(last_fail), '%Y-%m-%d %H:%M:%S')
|
||
elapsed = (datetime.datetime.now() - stored).total_seconds()
|
||
if elapsed < 300:
|
||
await _audit_login(sor, user.id, ns['username'], 'login_fail', 'locked')
|
||
return {
|
||
"status": "error",
|
||
"data": {
|
||
"message": "Account locked due to too many failed login attempts. Please try again in 5 minutes."
|
||
},
|
||
"widgettype":"Error",
|
||
"options":{
|
||
"timeout":5,
|
||
"title":"Account Locked",
|
||
"message":"Account locked due to too many failed login attempts. Please try again in 5 minutes."
|
||
}
|
||
}
|
||
else:
|
||
# Lockout expired, reset in background (best-effort)
|
||
await sor.sqlExe("""
|
||
UPDATE users SET login_fail_count = 0, last_login_fail = NULL
|
||
WHERE id = ${id}$
|
||
""", {'id': user.id})
|
||
except (ValueError, TypeError):
|
||
pass
|
||
|
||
# Verify password
|
||
r = await sor.sqlExe('select * from users where username=${username}$ and password=${password}$', ns.copy())
|
||
if len(r) == 0:
|
||
# 5分钟窗口:超过5分钟前的失败次数默认为0(原子 SQL)
|
||
now_str = datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')
|
||
stale_before = (datetime.datetime.now() - datetime.timedelta(seconds=300)).strftime('%Y-%m-%d %H:%M:%S')
|
||
await sor.sqlExe("""
|
||
UPDATE users
|
||
SET login_fail_count = CASE WHEN last_login_fail IS NULL OR last_login_fail < ${stale}$ THEN 1 ELSE login_fail_count + 1 END,
|
||
last_login_fail = ${now}$
|
||
WHERE id = ${id}$
|
||
""", {'id': user.id, 'now': now_str, 'stale': stale_before})
|
||
await _audit_login(sor, user.id, ns['username'], 'login_fail', 'fail')
|
||
|
||
new_fail_count = fail_count + 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 {
|
||
"status": "error",
|
||
"data": {
|
||
"message": msg
|
||
},
|
||
"widgettype":"Error",
|
||
"options":{
|
||
"timeout":3,
|
||
"title":"Login Error",
|
||
"message": msg
|
||
}
|
||
}
|
||
# Success -- atomic reset
|
||
now_str = datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')
|
||
await sor.sqlExe("""
|
||
UPDATE users
|
||
SET login_fail_count = 0, last_login_fail = NULL,
|
||
last_login = ${now}$
|
||
WHERE id = ${id}$
|
||
""", {'id': user.id, 'now': now_str})
|
||
await remember_user(r[0].id, username=r[0].username, userorgid=r[0].orgid)
|
||
await _audit_login(sor, r[0].id, r[0].username, 'login', 'ok')
|
||
login_source = params_kw.get('login_source', 'manual')
|
||
msg = {
|
||
"status": "ok",
|
||
"data": { "user": r[0]},
|
||
"widgettype":"Message",
|
||
"options":{
|
||
"timeout":3,
|
||
"anchor": "cc",
|
||
"auto_open":True,
|
||
"title":"Login",
|
||
"message":f"{r[0].username} Welcome back"
|
||
},
|
||
"binds":[
|
||
{
|
||
"wid":"self",
|
||
"event":"opened",
|
||
"actiontype":"script",
|
||
"target":"window.login_window",
|
||
"script":"this.destroy()"
|
||
}
|
||
]
|
||
}
|
||
if login_source == 'auto':
|
||
# 非用户主动点击登录(如401自动弹窗):不刷新页面,标记不刷新
|
||
msg["options"]["refresh"] = False
|
||
else:
|
||
# 用户主动点击登录:登录成功后刷新页面(跳转/index.ui)
|
||
msg["binds"].insert(0, {
|
||
"wid":"self",
|
||
"event":"dismissed",
|
||
"actiontype":"urlwidget",
|
||
"target":"window",
|
||
"options":{
|
||
"url":entire_url('/index.ui')
|
||
}
|
||
})
|
||
return msg
|
||
return {
|
||
"status": "error",
|
||
"data": {
|
||
"message": "system error"
|
||
},
|
||
|
||
"widgettype":"Error",
|
||
"options":{
|
||
"timeout":3,
|
||
"title":"Login Error",
|
||
"message":"system error"
|
||
}
|
||
}
|