feat: rewrite SMS login UI with fetch-based flow matching phone_login.dspy API
- login.ui: SMS tab now uses fetch for gen_sms_code.dspy and phone_login.dspy - Added _webbricks_=1 to fetch URLs (prevents HTML wrapping) - Added 60s countdown timer on send-code button - Added multi-account selection UI (status=choose response) - Fixed uitype 'hide' -> 'hidden' for codeid field - Dispatches user_logined event after successful phone login - gen_sms_code.dspy: improved error message for SMS service config issues - phone_login.dspy: added mark_used parameter for multi-account flow - phone_login.js: sageSelectAccount handler for account selection
This commit is contained in:
parent
cfd3810a0a
commit
567513789e
@ -12,7 +12,7 @@ if xx is None:
|
|||||||
return {
|
return {
|
||||||
"status": "error",
|
"status": "error",
|
||||||
"data": {
|
"data": {
|
||||||
"message": "发送验证码出错"
|
"message": "发送验证码出错,请检查短信服务配置"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
id, code = xx
|
id, code = xx
|
||||||
|
|||||||
@ -21,7 +21,12 @@ if params_kw.key is None:
|
|||||||
"message": "需要短信验证key"
|
"message": "需要短信验证key"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
f = await sms_engine.check_sms_code(params_kw.key, params_kw.sms_code)
|
|
||||||
|
# First check (no selected_id): verify code but don't consume it yet
|
||||||
|
# (multi-account flow needs the code to remain valid for the second call)
|
||||||
|
# Second check (with selected_id): verify and consume the code
|
||||||
|
mark_used = bool(params_kw.selected_id)
|
||||||
|
f = await sms_engine.check_sms_code(params_kw.key, params_kw.sms_code, mark_used=mark_used)
|
||||||
if not f:
|
if not f:
|
||||||
return {
|
return {
|
||||||
"status": "error",
|
"status": "error",
|
||||||
@ -44,7 +49,9 @@ try:
|
|||||||
if recs:
|
if recs:
|
||||||
if len(recs) == 1:
|
if len(recs) == 1:
|
||||||
r = recs[0]
|
r = recs[0]
|
||||||
# Update last_login atomically (standard SQL, no DB-specific functions)
|
# Single account: code already verified, now mark as used
|
||||||
|
if not mark_used:
|
||||||
|
await sms_engine.check_sms_code(params_kw.key, params_kw.sms_code, mark_used=True)
|
||||||
now_str = timestampstr()
|
now_str = timestampstr()
|
||||||
await sor.sqlExe("""
|
await sor.sqlExe("""
|
||||||
UPDATE users
|
UPDATE users
|
||||||
@ -53,7 +60,6 @@ try:
|
|||||||
WHERE id = ${id}$
|
WHERE id = ${id}$
|
||||||
""", {'id': r.id, 'now': now_str})
|
""", {'id': r.id, 'now': now_str})
|
||||||
await remember_user(r.id, username=r.username, userorgid=r.orgid)
|
await remember_user(r.id, username=r.username, userorgid=r.orgid)
|
||||||
debug(f'here')
|
|
||||||
return {
|
return {
|
||||||
"status": "ok",
|
"status": "ok",
|
||||||
"data":{
|
"data":{
|
||||||
@ -71,7 +77,6 @@ try:
|
|||||||
WHERE id = ${id}$
|
WHERE id = ${id}$
|
||||||
""", {'id': r.id, 'now': now_str})
|
""", {'id': r.id, 'now': now_str})
|
||||||
await remember_user(r.id, username=r.username, userorgid=r.orgid)
|
await remember_user(r.id, username=r.username, userorgid=r.orgid)
|
||||||
debug(f'here')
|
|
||||||
return {
|
return {
|
||||||
"status": "ok",
|
"status": "ok",
|
||||||
"data":{
|
"data":{
|
||||||
@ -79,17 +84,16 @@ try:
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
else:
|
else:
|
||||||
debug(f'here')
|
|
||||||
return {
|
return {
|
||||||
"status": "choose",
|
"status": "choose",
|
||||||
"data": {
|
"data": {
|
||||||
|
"key": params_kw.key,
|
||||||
"users": recs
|
"users": recs
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
d = await register_user(sor, udata)
|
d = await register_user(sor, udata)
|
||||||
if d['status'] == 'error':
|
if d['status'] == 'error':
|
||||||
debug(f'here, {d}')
|
|
||||||
return d
|
return d
|
||||||
try:
|
try:
|
||||||
ownerid = await get_owner_orgid(sor, orgid)
|
ownerid = await get_owner_orgid(sor, orgid)
|
||||||
@ -97,9 +101,11 @@ try:
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
exception(f'{e}')
|
exception(f'{e}')
|
||||||
|
|
||||||
|
# New user registered: code already verified, mark as used
|
||||||
|
if not mark_used:
|
||||||
|
await sms_engine.check_sms_code(params_kw.key, params_kw.sms_code, mark_used=True)
|
||||||
r = d['data']['user']
|
r = d['data']['user']
|
||||||
await remember_user(r.id, username=r.username, userorgid=r.orgid)
|
await remember_user(r.id, username=r.username, userorgid=r.orgid)
|
||||||
debug(f'here')
|
|
||||||
return {
|
return {
|
||||||
"status": "ok",
|
"status": "ok",
|
||||||
"data":{
|
"data":{
|
||||||
|
|||||||
53
wwwroot/phone_login.js
Normal file
53
wwwroot/phone_login.js
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
/* Sage Phone Login - Multi-account Selection Handler
|
||||||
|
Called when user selects an account from the multi-account list
|
||||||
|
*/
|
||||||
|
window.sageSelectAccount = async function(selectedId) {
|
||||||
|
var form = bricks.getWidgetById('phone_form', bricks.app);
|
||||||
|
if (!form) return;
|
||||||
|
var vals = form._getValue();
|
||||||
|
|
||||||
|
var btn = bricks.getWidgetById('phone_login_btn', bricks.app);
|
||||||
|
if (btn) { btn.disabled = true; if (btn.text_w) btn.text_w.set_otext('登录中...'); }
|
||||||
|
|
||||||
|
var body = 'cellphone=' + encodeURIComponent(vals.cell_no)
|
||||||
|
+ '&key=' + encodeURIComponent(vals.codeid)
|
||||||
|
+ '&sms_code=' + encodeURIComponent(vals.check_code)
|
||||||
|
+ '&selected_id=' + encodeURIComponent(selectedId);
|
||||||
|
|
||||||
|
// Get phone_login URL from the page context
|
||||||
|
var baseUrl = bricks.app.baseUrl || '';
|
||||||
|
var loginUrl = baseUrl + '/rbac/phone_login.dspy?_webbricks_=1';
|
||||||
|
|
||||||
|
try {
|
||||||
|
var resp = await fetch(loginUrl, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
||||||
|
body: body
|
||||||
|
});
|
||||||
|
var d = await resp.json();
|
||||||
|
|
||||||
|
if (btn) { btn.disabled = false; if (btn.text_w) btn.text_w.set_otext('登录'); }
|
||||||
|
|
||||||
|
if (d.status === 'ok') {
|
||||||
|
var u = d.data.user;
|
||||||
|
var nick = u.nick_name || u.username;
|
||||||
|
var msgW = {
|
||||||
|
widgettype: 'Message',
|
||||||
|
options: { timeout: 3, auto_open: true, title: '登录成功', message: nick + ' 欢迎' },
|
||||||
|
binds: [
|
||||||
|
{ wid: 'self', event: 'dismissed', actiontype: 'script', target: 'self',
|
||||||
|
script: 'if(bricks.app&&bricks.app.dispatch)bricks.app.dispatch("user_logined")' },
|
||||||
|
{ wid: 'self', event: 'dismissed', actiontype: 'script', target: 'body.login_window',
|
||||||
|
script: 'this.destroy()' }
|
||||||
|
]
|
||||||
|
};
|
||||||
|
var w = await bricks.widgetBuild(msgW, bricks.app);
|
||||||
|
if (w) bricks.app.add_widget(w);
|
||||||
|
} else {
|
||||||
|
alert(d.data.message || '登录失败');
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
if (btn) { btn.disabled = false; if (btn.text_w) btn.text_w.set_otext('登录'); }
|
||||||
|
alert('网络错误: ' + e);
|
||||||
|
}
|
||||||
|
};
|
||||||
@ -1,4 +1,5 @@
|
|||||||
{% set sms_code_url = entire_url('/rbac/gen_sms_code.dspy') %}
|
{% set sms_code_url = entire_url('/rbac/gen_sms_code.dspy') %}
|
||||||
|
{% set phone_login_url = entire_url('/rbac/phone_login.dspy') %}
|
||||||
{
|
{
|
||||||
"id": "login_window",
|
"id": "login_window",
|
||||||
"widgettype": "PopupWindow",
|
"widgettype": "PopupWindow",
|
||||||
@ -12,6 +13,7 @@
|
|||||||
"subwidgets": [
|
"subwidgets": [
|
||||||
{
|
{
|
||||||
"widgettype": "TabPanel",
|
"widgettype": "TabPanel",
|
||||||
|
"id": "login_tabs",
|
||||||
"options": {
|
"options": {
|
||||||
"tab_wide": "auto",
|
"tab_wide": "auto",
|
||||||
"height": "100%",
|
"height": "100%",
|
||||||
@ -53,16 +55,21 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "checkcode",
|
"name": "phonecode",
|
||||||
"label": "手机验证码",
|
"label": "手机验证码",
|
||||||
"content": {
|
"content": {
|
||||||
"widgettype": "VBox",
|
"widgettype": "VBox",
|
||||||
|
"options": {
|
||||||
|
"padding": "8px",
|
||||||
|
"gap": "8px"
|
||||||
|
},
|
||||||
"subwidgets": [
|
"subwidgets": [
|
||||||
{
|
{
|
||||||
"widgettype": "Form",
|
"widgettype": "Form",
|
||||||
"id": "phone_form",
|
"id": "phone_form",
|
||||||
"options": {
|
"options": {
|
||||||
"description": "限中国国内手机",
|
"description": "限中国国内手机号",
|
||||||
|
"cols": 1,
|
||||||
"fields": [
|
"fields": [
|
||||||
{
|
{
|
||||||
"name": "cell_no",
|
"name": "cell_no",
|
||||||
@ -71,7 +78,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "codeid",
|
"name": "codeid",
|
||||||
"uitype": "hide",
|
"uitype": "hidden",
|
||||||
"value": ""
|
"value": ""
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -80,20 +87,14 @@
|
|||||||
"uitype": "str"
|
"uitype": "str"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"binds": [
|
|
||||||
{
|
{
|
||||||
"wid": "self",
|
"widgettype": "HBox",
|
||||||
"event": "submit",
|
|
||||||
"actiontype": "urlwidget",
|
|
||||||
"target": "self",
|
|
||||||
"options": {
|
"options": {
|
||||||
"method": "POST",
|
"gap": "8px"
|
||||||
"url": "{{entire_url('code_login.dspy')}}"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
|
"subwidgets": [
|
||||||
{
|
{
|
||||||
"widgettype": "Button",
|
"widgettype": "Button",
|
||||||
"id": "gen_code_btn",
|
"id": "gen_code_btn",
|
||||||
@ -106,7 +107,26 @@
|
|||||||
"event": "click",
|
"event": "click",
|
||||||
"actiontype": "script",
|
"actiontype": "script",
|
||||||
"target": "self",
|
"target": "self",
|
||||||
"script": "var form=bricks.getWidgetById('phone_form',bricks.app);if(!form){alert('form not found');return;}var vals=form._getValue();var cell=vals.cell_no;if(!cell){alert('请输入手机号');return;}var btn=bricks.getWidgetById('gen_code_btn',bricks.app);if(btn&&btn.text_w){btn.text_w.set_otext('发送中...')}fetch('{{sms_code_url}}?cellphone='+encodeURIComponent(cell)).then(function(r){return r.json()}).then(function(d){if(d.status==='ok'){form.setValue({codeid:d.data.key});if(btn&&btn.text_w){btn.text_w.set_otext('已发送('+cell+')')}}else{alert(d.data.message);if(btn&&btn.text_w){btn.text_w.set_otext('发送验证码')}}}).catch(function(e){alert('发送失败:'+e);if(btn&&btn.text_w){btn.text_w.set_otext('发送验证码')}})"
|
"script": "var form=bricks.getWidgetById('phone_form',bricks.app);if(!form){alert('form not found');return;}var vals=form._getValue();var cell=vals.cell_no;if(!cell||cell.length<11){alert('请输入正确的手机号');return;}var btn=bricks.getWidgetById('gen_code_btn',bricks.app);btn.disabled=true;if(btn.text_w)btn.text_w.set_otext('发送中...');fetch('{{sms_code_url}}?_webbricks_=1&cellphone='+encodeURIComponent(cell)).then(function(r){return r.json()}).then(function(d){if(d.status==='ok'){form.setValue({codeid:d.data.key});if(btn.text_w)btn.text_w.set_otext('已发送');var sec=60;var tmr=setInterval(function(){sec--;if(sec<=0){clearInterval(tmr);btn.disabled=false;if(btn.text_w)btn.text_w.set_otext('重新发送')}else{if(btn.text_w)btn.text_w.set_otext(sec+'s')}},1000)}else{alert(d.data.message||'发送验证码出错');btn.disabled=false;if(btn.text_w)btn.text_w.set_otext('发送验证码')}}).catch(function(e){alert('网络错误: '+e);btn.disabled=false;if(btn.text_w)btn.text_w.set_otext('发送验证码')})"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"widgettype": "Button",
|
||||||
|
"id": "phone_login_btn",
|
||||||
|
"options": {
|
||||||
|
"label": "登录",
|
||||||
|
"css": "sage-btn-primary"
|
||||||
|
},
|
||||||
|
"binds": [
|
||||||
|
{
|
||||||
|
"wid": "self",
|
||||||
|
"event": "click",
|
||||||
|
"actiontype": "script",
|
||||||
|
"target": "self",
|
||||||
|
"script": "var form=bricks.getWidgetById('phone_form',bricks.app);if(!form)return;var vals=form._getValue();if(!vals.cell_no){alert('请输入手机号');return;}if(!vals.check_code){alert('请输入验证码');return;}if(!vals.codeid){alert('请先发送验证码');return;}var btn=bricks.getWidgetById('phone_login_btn',bricks.app);btn.disabled=true;if(btn.text_w)btn.text_w.set_otext('登录中...');var body='cellphone='+encodeURIComponent(vals.cell_no)+'&key='+encodeURIComponent(vals.codeid)+'&sms_code='+encodeURIComponent(vals.check_code);fetch('{{phone_login_url}}?_webbricks_=1',{method:'POST',headers:{'Content-Type':'application/x-www-form-urlencoded'},body:body}).then(function(r){return r.json()}).then(function(d){btn.disabled=false;if(btn.text_w)btn.text_w.set_otext('登录');if(d.status==='ok'){var u=d.data.user;var nick=u.nick_name||u.username;var msgW={widgettype:'Message',options:{timeout:3,auto_open:true,title:'登录成功',message:nick+' 欢迎'},binds:[{wid:'self',event:'dismissed',actiontype:'script',target:'self',script:'if(bricks.app&&bricks.app.dispatch)bricks.app.dispatch(\"user_logined\")'},{wid:'self',event:'dismissed',actiontype:'script',target:'body.login_window',script:'this.destroy()'}]};bricks.widgetBuild(msgW,bricks.app).then(function(w){if(w)bricks.app.add_widget(w)})}else if(d.status==='choose'){var users=d.data.users;var html='<div style=\"padding:12px\"><p style=\"margin:0 0 12px\">该手机号关联多个账号,请选择:</p>';for(var i=0;i<users.length;i++){html+='<button onclick=\"sageSelectAccount(\\x27'+users[i].id+'\\x27)\" style=\"display:block;width:100%;padding:10px;margin:4px 0;cursor:pointer;border:1px solid #ccc;border-radius:4px;background:#f5f5f5;text-align:left;font-size:14px\">'+users[i].username+'</button>'}html+='</div>';var cw=bricks.getWidgetById('choose_container',bricks.app);if(cw){cw.dom_element.innerHTML=html;cw.dom_element.style.display=''}else{var div=document.createElement('div');div.id='choose_container';div.innerHTML=html;form.dom_element.parentElement.appendChild(div)}form.dom_element.style.display='none'}else{alert(d.data.message||'登录失败')}}).catch(function(e){btn.disabled=false;if(btn.text_w)btn.text_w.set_otext('登录');alert('网络错误: '+e)})"
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user