feat: 手机验证码登录对接 + 注册tab + user_logined事件派发
- 新增 code_login.dspy: 接收前端表单(cell_no/codeid/check_code) 映射到sms_engine验证,返回UI widget含自动登录binds - 修复 login.ui 手机验证码tab: gen_code按钮改用script调用 gen_sms_code.dspy并回写key到隐藏字段,submit指向code_login.dspy - login.ui 新增注册tab: 用户名/手机号/密码/确认密码表单 - register.dspy: 注册成功后自动remember_user并返回含binds的 Message widget(加载userinfo、销毁登录窗、派发user_logined) - up_login.dspy: 补充user_logined事件派发bind - load_path.py: code_login.dspy加入any权限,gen_sms_code.dspy 从logined移至any(验证码发送在登录前)
This commit is contained in:
parent
54b0f3d7b6
commit
de21b9fd38
@ -41,8 +41,10 @@ MOD = "rbac"
|
|||||||
# any — 无需登录(菜单、登录页等)
|
# any — 无需登录(菜单、登录页等)
|
||||||
PATHS_ANY = [
|
PATHS_ANY = [
|
||||||
f"/rbac/admin_menu.ui",
|
f"/rbac/admin_menu.ui",
|
||||||
|
f"/rbac/gen_sms_code.dspy",
|
||||||
f"/rbac/phone_login.dspy",
|
f"/rbac/phone_login.dspy",
|
||||||
f"/rbac/qr_scan.ui",
|
f"/rbac/qr_scan.ui",
|
||||||
|
f"/rbac/user/code_login.dspy",
|
||||||
f"/rbac/user/login.ui",
|
f"/rbac/user/login.ui",
|
||||||
f"/rbac/user/logout.dspy",
|
f"/rbac/user/logout.dspy",
|
||||||
f"/rbac/user/register.dspy",
|
f"/rbac/user/register.dspy",
|
||||||
@ -64,7 +66,6 @@ PATHS_LOGINED = [
|
|||||||
f"/rbac/add_reseller.dspy",
|
f"/rbac/add_reseller.dspy",
|
||||||
f"/rbac/add_superuser.dspy",
|
f"/rbac/add_superuser.dspy",
|
||||||
f"/rbac/find_unauth_files.dspy",
|
f"/rbac/find_unauth_files.dspy",
|
||||||
f"/rbac/gen_sms_code.dspy",
|
|
||||||
f"/rbac/get_all_roles.dspy",
|
f"/rbac/get_all_roles.dspy",
|
||||||
f"/rbac/get_normal_roles.dspy",
|
f"/rbac/get_normal_roles.dspy",
|
||||||
f"/rbac/get_provider.dspy",
|
f"/rbac/get_provider.dspy",
|
||||||
|
|||||||
257
wwwroot/user/code_login.dspy
Normal file
257
wwwroot/user/code_login.dspy
Normal file
@ -0,0 +1,257 @@
|
|||||||
|
# 手机验证码登录 - 接收前端表单参数(cell_no, codeid, check_code)
|
||||||
|
# 调用sms_engine验证后完成登录或自动注册
|
||||||
|
debug(f'code_login.dspy: {params_kw=}')
|
||||||
|
|
||||||
|
cellphone = params_kw.cell_no
|
||||||
|
key = params_kw.codeid
|
||||||
|
sms_code = params_kw.check_code
|
||||||
|
|
||||||
|
if not cellphone:
|
||||||
|
return {
|
||||||
|
"widgettype": "Error",
|
||||||
|
"options": {
|
||||||
|
"timeout": 3,
|
||||||
|
"title": "错误",
|
||||||
|
"message": "需输入手机号"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if not sms_code:
|
||||||
|
return {
|
||||||
|
"widgettype": "Error",
|
||||||
|
"options": {
|
||||||
|
"timeout": 3,
|
||||||
|
"title": "错误",
|
||||||
|
"message": "需输入验证码"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if not key:
|
||||||
|
return {
|
||||||
|
"widgettype": "Error",
|
||||||
|
"options": {
|
||||||
|
"timeout": 3,
|
||||||
|
"title": "错误",
|
||||||
|
"message": "需要短信验证key"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# 验证短信码
|
||||||
|
ok = await sms_engine.check_sms_code(key, sms_code)
|
||||||
|
if not ok:
|
||||||
|
return {
|
||||||
|
"widgettype": "Error",
|
||||||
|
"options": {
|
||||||
|
"timeout": 3,
|
||||||
|
"title": "验证失败",
|
||||||
|
"message": "手机短信验证码出错"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# 验证通过,查找或注册用户
|
||||||
|
ns = {
|
||||||
|
"username": cellphone,
|
||||||
|
"password": "^&%UHI",
|
||||||
|
"cfm_password": "^&%UHI",
|
||||||
|
"mobile": cellphone,
|
||||||
|
"user_status": "0"
|
||||||
|
}
|
||||||
|
udata = DictObject(**ns)
|
||||||
|
|
||||||
|
try:
|
||||||
|
async with get_sor_context(request._run_ns, 'rbac') as sor:
|
||||||
|
recs = await sor.R('users', {'mobile': cellphone})
|
||||||
|
if recs:
|
||||||
|
if len(recs) == 1:
|
||||||
|
r = recs[0]
|
||||||
|
now_str = timestampstr()
|
||||||
|
await sor.sqlExe("""
|
||||||
|
UPDATE users
|
||||||
|
SET last_login = ${now}$, login_fail_count = 0,
|
||||||
|
last_login_fail = NULL
|
||||||
|
WHERE id = ${id}$
|
||||||
|
""", {'id': r.id, 'now': now_str})
|
||||||
|
await remember_user(r.id, username=r.username, userorgid=r.orgid)
|
||||||
|
return {
|
||||||
|
"widgettype": "Message",
|
||||||
|
"options": {
|
||||||
|
"timeout": 3,
|
||||||
|
"auto_open": True,
|
||||||
|
"title": "登录成功",
|
||||||
|
"message": f"{r.username} 欢迎回来"
|
||||||
|
},
|
||||||
|
"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": "body.login_window",
|
||||||
|
"script": "this.destroy()"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"wid": "self",
|
||||||
|
"event": "dismissed",
|
||||||
|
"actiontype": "script",
|
||||||
|
"target": "self",
|
||||||
|
"script": "if(bricks.app && bricks.app.dispatch) bricks.app.dispatch('user_logined')"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
# 多个用户绑定同一手机号
|
||||||
|
if params_kw.selected_id:
|
||||||
|
for r in recs:
|
||||||
|
if r.id == params_kw.selected_id:
|
||||||
|
now_str = timestampstr()
|
||||||
|
await sor.sqlExe("""
|
||||||
|
UPDATE users
|
||||||
|
SET last_login = ${now}$, login_fail_count = 0,
|
||||||
|
last_login_fail = NULL
|
||||||
|
WHERE id = ${id}$
|
||||||
|
""", {'id': r.id, 'now': now_str})
|
||||||
|
await remember_user(r.id, username=r.username, userorgid=r.orgid)
|
||||||
|
return {
|
||||||
|
"widgettype": "Message",
|
||||||
|
"options": {
|
||||||
|
"timeout": 3,
|
||||||
|
"auto_open": True,
|
||||||
|
"title": "登录成功",
|
||||||
|
"message": f"{r.username} 欢迎回来"
|
||||||
|
},
|
||||||
|
"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": "body.login_window",
|
||||||
|
"script": "this.destroy()"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"wid": "self",
|
||||||
|
"event": "dismissed",
|
||||||
|
"actiontype": "script",
|
||||||
|
"target": "self",
|
||||||
|
"script": "if(bricks.app && bricks.app.dispatch) bricks.app.dispatch('user_logined')"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
else:
|
||||||
|
# 返回用户选择列表
|
||||||
|
buttons = []
|
||||||
|
for r in recs:
|
||||||
|
buttons.append({
|
||||||
|
"widgettype": "Button",
|
||||||
|
"options": {
|
||||||
|
"label": f"{r.username} ({r.id})",
|
||||||
|
"width": "100%",
|
||||||
|
"margin": "4px 0"
|
||||||
|
},
|
||||||
|
"binds": [{
|
||||||
|
"wid": "self",
|
||||||
|
"event": "click",
|
||||||
|
"actiontype": "urlwidget",
|
||||||
|
"target": "self",
|
||||||
|
"options": {
|
||||||
|
"url": entire_url('/rbac/user/code_login.dspy'),
|
||||||
|
"params": {
|
||||||
|
"cell_no": cellphone,
|
||||||
|
"codeid": key,
|
||||||
|
"check_code": sms_code,
|
||||||
|
"selected_id": r.id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}]
|
||||||
|
})
|
||||||
|
return {
|
||||||
|
"widgettype": "VBox",
|
||||||
|
"options": {"padding": "12px"},
|
||||||
|
"subwidgets": [
|
||||||
|
{
|
||||||
|
"widgettype": "Text",
|
||||||
|
"options": {
|
||||||
|
"text": "该手机号关联多个账号,请选择:",
|
||||||
|
"fontSize": "14px",
|
||||||
|
"margin": "0 0 8px 0"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
] + buttons
|
||||||
|
}
|
||||||
|
|
||||||
|
# 新用户自动注册
|
||||||
|
d = await register_user(sor, udata)
|
||||||
|
if d['status'] == 'error':
|
||||||
|
return {
|
||||||
|
"widgettype": "Error",
|
||||||
|
"options": {
|
||||||
|
"timeout": 5,
|
||||||
|
"title": "注册失败",
|
||||||
|
"message": d['data']['message']
|
||||||
|
}
|
||||||
|
}
|
||||||
|
try:
|
||||||
|
ownerid = await get_owner_orgid(sor, orgid)
|
||||||
|
await openCustomerAccounts(sor, ownerid, orgid)
|
||||||
|
except Exception as e:
|
||||||
|
exception(f'{e}')
|
||||||
|
|
||||||
|
r = d['data']['user']
|
||||||
|
await remember_user(r.id, username=r.username, userorgid=r.orgid)
|
||||||
|
return {
|
||||||
|
"widgettype": "Message",
|
||||||
|
"options": {
|
||||||
|
"timeout": 3,
|
||||||
|
"auto_open": True,
|
||||||
|
"title": "登录成功",
|
||||||
|
"message": f"{r.username} 欢迎"
|
||||||
|
},
|
||||||
|
"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": "body.login_window",
|
||||||
|
"script": "this.destroy()"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"wid": "self",
|
||||||
|
"event": "dismissed",
|
||||||
|
"actiontype": "script",
|
||||||
|
"target": "self",
|
||||||
|
"script": "if(bricks.app && bricks.app.dispatch) bricks.app.dispatch('user_logined')"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
except Exception as e:
|
||||||
|
exception(f'code_login error: {e}')
|
||||||
|
return {
|
||||||
|
"widgettype": "Error",
|
||||||
|
"options": {
|
||||||
|
"timeout": 5,
|
||||||
|
"title": "系统错误",
|
||||||
|
"message": f"{e}"
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -6,7 +6,7 @@
|
|||||||
"auto_open": true,
|
"auto_open": true,
|
||||||
"anthor": "cc",
|
"anthor": "cc",
|
||||||
"cwidth": 22,
|
"cwidth": 22,
|
||||||
"cheight": 19
|
"cheight": 22
|
||||||
},
|
},
|
||||||
"subwidgets": [
|
"subwidgets": [
|
||||||
{
|
{
|
||||||
@ -57,14 +57,6 @@
|
|||||||
"content": {
|
"content": {
|
||||||
"widgettype": "Form",
|
"widgettype": "Form",
|
||||||
"options": {
|
"options": {
|
||||||
"toolbar": {
|
|
||||||
"tools": [
|
|
||||||
{
|
|
||||||
"name": "gen_code",
|
|
||||||
"label": "发送验证码"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"description": "限中国国内手机",
|
"description": "限中国国内手机",
|
||||||
"fields": [
|
"fields": [
|
||||||
{
|
{
|
||||||
@ -75,25 +67,30 @@
|
|||||||
{
|
{
|
||||||
"name": "codeid",
|
"name": "codeid",
|
||||||
"uitype": "hide",
|
"uitype": "hide",
|
||||||
"value": "{{uuid()}}"
|
"value": ""
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "check_code",
|
"name": "check_code",
|
||||||
|
"label": "验证码",
|
||||||
"uitype": "str"
|
"uitype": "str"
|
||||||
}
|
}
|
||||||
]
|
],
|
||||||
|
"toolbar": {
|
||||||
|
"tools": [
|
||||||
|
{
|
||||||
|
"name": "gen_code",
|
||||||
|
"label": "发送验证码"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"binds": [
|
"binds": [
|
||||||
{
|
{
|
||||||
"wid": "self",
|
"wid": "self",
|
||||||
"event": "gen_code",
|
"event": "gen_code",
|
||||||
"actiontype": "urlwidget",
|
"actiontype": "script",
|
||||||
"datawidget": "self",
|
|
||||||
"datamethod": "getValue",
|
|
||||||
"target": "self",
|
"target": "self",
|
||||||
"options": {
|
"script": "var form=this;var vals=form.getValue();if(!vals.cell_no){alert('请输入手机号');return;}fetch(form.app.baseUrl+'/rbac/gen_sms_code.dspy?cellphone='+encodeURIComponent(vals.cell_no)).then(function(r){return r.json()}).then(function(d){if(d.status==='ok'){form.setValue({codeid:d.data.key});alert('验证码已发送')}else{alert(d.data.message)}}).catch(function(e){alert('发送失败:'+e)})"
|
||||||
"url": "{{entire_url('../gen_sms_code.dspy')}}"
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"wid": "self",
|
"wid": "self",
|
||||||
@ -101,11 +98,56 @@
|
|||||||
"actiontype": "urlwidget",
|
"actiontype": "urlwidget",
|
||||||
"target": "self",
|
"target": "self",
|
||||||
"options": {
|
"options": {
|
||||||
|
"method": "POST",
|
||||||
"url": "{{entire_url('code_login.dspy')}}"
|
"url": "{{entire_url('code_login.dspy')}}"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "register",
|
||||||
|
"label": "注册",
|
||||||
|
"content": {
|
||||||
|
"widgettype": "Form",
|
||||||
|
"options": {
|
||||||
|
"cols": 1,
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"name": "username",
|
||||||
|
"label": "用户名",
|
||||||
|
"uitype": "str"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "mobile",
|
||||||
|
"label": "手机号",
|
||||||
|
"uitype": "str"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "password",
|
||||||
|
"label": "密码",
|
||||||
|
"uitype": "password"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "cfm_password",
|
||||||
|
"label": "确认密码",
|
||||||
|
"uitype": "password"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"binds": [
|
||||||
|
{
|
||||||
|
"wid": "self",
|
||||||
|
"event": "submit",
|
||||||
|
"actiontype": "urlwidget",
|
||||||
|
"target": "self",
|
||||||
|
"options": {
|
||||||
|
"method": "POST",
|
||||||
|
"url": "{{entire_url('register.dspy')}}"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,18 +1,69 @@
|
|||||||
debug(f'{params_kw=}')
|
debug(f'register.dspy: {params_kw=}')
|
||||||
db = DBPools()
|
db = DBPools()
|
||||||
dbname = get_module_dbname('rbac')
|
dbname = get_module_dbname('rbac')
|
||||||
async with db.sqlorContext(dbname) as sor:
|
async with db.sqlorContext(dbname) as sor:
|
||||||
data = await register_user(sor, params_kw)
|
data = await register_user(sor, params_kw)
|
||||||
data = DictObject(**data)
|
data = DictObject(**data)
|
||||||
|
if data.status == 'error':
|
||||||
|
debug(f"register error: {data.data.message}")
|
||||||
|
return {
|
||||||
|
"widgettype": "Error",
|
||||||
|
"options": {
|
||||||
|
"timeout": 5,
|
||||||
|
"title": "注册失败",
|
||||||
|
"message": data.data.message
|
||||||
|
}
|
||||||
|
}
|
||||||
|
user = data.data.user
|
||||||
|
orgid = user.orgid
|
||||||
try:
|
try:
|
||||||
if data['status'] == 'error':
|
|
||||||
debug(f"{data.data.message}")
|
|
||||||
return UiError(title='Error', message=data.data.message)
|
|
||||||
orgid = data.data.user.orgid
|
|
||||||
await openCustomerAccounts(sor, '0', orgid)
|
await openCustomerAccounts(sor, '0', orgid)
|
||||||
debug(f'{orgid} accounts opened')
|
debug(f'{orgid} accounts opened')
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
exception(f'{e},{orgid=}')
|
exception(f'{e},{orgid=}')
|
||||||
|
|
||||||
return UiMessage(title="Success", message=f"register success {orgid}")
|
# 注册成功后自动登录
|
||||||
return UiError(title='Error', message="register failed")
|
await remember_user(user.id, username=user.username, userorgid=user.orgid)
|
||||||
|
return {
|
||||||
|
"widgettype": "Message",
|
||||||
|
"options": {
|
||||||
|
"timeout": 3,
|
||||||
|
"auto_open": True,
|
||||||
|
"title": "注册成功",
|
||||||
|
"message": f"{user.username} 注册成功,已自动登录"
|
||||||
|
},
|
||||||
|
"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": "body.login_window",
|
||||||
|
"script": "if(this.destroy) this.destroy()"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"wid": "self",
|
||||||
|
"event": "dismissed",
|
||||||
|
"actiontype": "script",
|
||||||
|
"target": "self",
|
||||||
|
"script": "if(bricks.app && bricks.app.dispatch) bricks.app.dispatch('user_logined')"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
"widgettype": "Error",
|
||||||
|
"options": {
|
||||||
|
"timeout": 5,
|
||||||
|
"title": "注册失败",
|
||||||
|
"message": "系统错误,请稍后重试"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@ -1,4 +1,3 @@
|
|||||||
|
|
||||||
debug(f'{params_kw=}')
|
debug(f'{params_kw=}')
|
||||||
ns = {
|
ns = {
|
||||||
"username":params_kw.username,
|
"username":params_kw.username,
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user