diff --git a/wwwroot/gen_sms_code.dspy b/wwwroot/gen_sms_code.dspy index 94c606f..48898a8 100644 --- a/wwwroot/gen_sms_code.dspy +++ b/wwwroot/gen_sms_code.dspy @@ -12,7 +12,7 @@ if xx is None: return { "status": "error", "data": { - "message": "发送验证码出错" + "message": "发送验证码出错,请检查短信服务配置" } } id, code = xx diff --git a/wwwroot/phone_login.dspy b/wwwroot/phone_login.dspy index 8445ab6..cbb8406 100644 --- a/wwwroot/phone_login.dspy +++ b/wwwroot/phone_login.dspy @@ -21,7 +21,12 @@ if params_kw.key is None: "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: return { "status": "error", @@ -44,7 +49,9 @@ try: if recs: if len(recs) == 1: 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() await sor.sqlExe(""" UPDATE users @@ -53,7 +60,6 @@ try: WHERE id = ${id}$ """, {'id': r.id, 'now': now_str}) await remember_user(r.id, username=r.username, userorgid=r.orgid) - debug(f'here') return { "status": "ok", "data":{ @@ -71,7 +77,6 @@ try: WHERE id = ${id}$ """, {'id': r.id, 'now': now_str}) await remember_user(r.id, username=r.username, userorgid=r.orgid) - debug(f'here') return { "status": "ok", "data":{ @@ -79,17 +84,16 @@ try: } } else: - debug(f'here') return { "status": "choose", "data": { + "key": params_kw.key, "users": recs } } d = await register_user(sor, udata) if d['status'] == 'error': - debug(f'here, {d}') return d try: ownerid = await get_owner_orgid(sor, orgid) @@ -97,9 +101,11 @@ try: except Exception as 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'] await remember_user(r.id, username=r.username, userorgid=r.orgid) - debug(f'here') return { "status": "ok", "data":{ diff --git a/wwwroot/phone_login.js b/wwwroot/phone_login.js new file mode 100644 index 0000000..103d70d --- /dev/null +++ b/wwwroot/phone_login.js @@ -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); + } +}; diff --git a/wwwroot/user/login.ui b/wwwroot/user/login.ui index a0bed6f..b3cad7b 100644 --- a/wwwroot/user/login.ui +++ b/wwwroot/user/login.ui @@ -1,4 +1,5 @@ {% set sms_code_url = entire_url('/rbac/gen_sms_code.dspy') %} +{% set phone_login_url = entire_url('/rbac/phone_login.dspy') %} { "id": "login_window", "widgettype": "PopupWindow", @@ -12,6 +13,7 @@ "subwidgets": [ { "widgettype": "TabPanel", + "id": "login_tabs", "options": { "tab_wide": "auto", "height": "100%", @@ -53,16 +55,21 @@ } }, { - "name": "checkcode", + "name": "phonecode", "label": "手机验证码", "content": { "widgettype": "VBox", + "options": { + "padding": "8px", + "gap": "8px" + }, "subwidgets": [ { "widgettype": "Form", "id": "phone_form", "options": { - "description": "限中国国内手机", + "description": "限中国国内手机号", + "cols": 1, "fields": [ { "name": "cell_no", @@ -71,7 +78,7 @@ }, { "name": "codeid", - "uitype": "hide", + "uitype": "hidden", "value": "" }, { @@ -80,33 +87,46 @@ "uitype": "str" } ] - }, - "binds": [ - { - "wid": "self", - "event": "submit", - "actiontype": "urlwidget", - "target": "self", - "options": { - "method": "POST", - "url": "{{entire_url('code_login.dspy')}}" - } - } - ] + } }, { - "widgettype": "Button", - "id": "gen_code_btn", + "widgettype": "HBox", "options": { - "label": "发送验证码" + "gap": "8px" }, - "binds": [ + "subwidgets": [ { - "wid": "self", - "event": "click", - "actiontype": "script", - "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('发送验证码')}})" + "widgettype": "Button", + "id": "gen_code_btn", + "options": { + "label": "发送验证码" + }, + "binds": [ + { + "wid": "self", + "event": "click", + "actiontype": "script", + "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||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='

该手机号关联多个账号,请选择:

';for(var i=0;i'+users[i].username+''}html+='
';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)})" + } + ] } ] }