@koishi-ce/plugin-auth 1.0.1 → 1.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +1 -1
- package/package.json +2 -2
- package/src/__tests__/index.test.ts +282 -87
- package/src/index.ts +271 -141
package/dist/index.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{Schema as e,deepEqual as t,icons as n,message as r,pick as i,router as a,send as o,store as s,useConfig as c,useStorage as l}from"@koishi-ce/client";import{Fragment as u,computed as d,createBlock as f,createCommentVNode as p,createElementBlock as m,createElementVNode as h,createTextVNode as g,createVNode as _,defineComponent as v,h as y,isRef as b,openBlock as x,ref as S,renderList as C,resolveComponent as w,toDisplayString as T,unref as E,watch as D,withCtx as O,withKeys as k,withModifiers as A}from"vue";import{useRouter as j}from"vue-router";var M=l(`auth`,2,()=>({authType:0})),N=S(!1),P=S(!1),F={class:`login-form text-center`},ee={key:0},te={key:1},I={class:`hint`},L={class:`token`},R={class:`control`},z={key:0},B={key:1},V={key:0,class:`error`},H={class:`control`},U={key:0,class:`error`},W={class:`control`},G=v({__name:`login-form`,setup(e){let t=S(),n=S(),r=S(!1),i=0;async function a(){let e=Date.now();if(e<i)return;let{platform:r,userId:a}=M.value;if(!(!r||!a)){i=e+1e3;try{n.value=await o(`login/platform`,r,a)}catch(e){t.value=e.message}}}async function c(){let{name:e,password:n}=M.value;try{await o(`login/password`,e,n)}catch(e){t.value=e.message}}let l=j();function d(){s.user?N.value=!1:l.back()}return(e,i)=>{let o=w(`k-button`),l=w(`k-tab`),f=w(`k-icon`),v=w(`el-input`);return x(),m(`div`,F,[n.value?(x(),m(u,{key:0},[E(s).user?(x(),m(`h1`,ee,[...i[7]||=[h(`span`,null,`平台账户绑定`,-1)]])):(x(),m(`h1`,te,[...i[8]||=[h(`span`,null,`平台账户登录`,-1)]])),h(`p`,I,`欢迎你,`+T(n.value.name||`Koishi 用户`)+`!`,1),i[10]||=h(`p`,{class:`hint`},`请用上述账号将下面的验证码发送给任意正在运行的机器人`,-1),h(`p`,L,T(n.value.token),1),h(`div`,R,[_(o,{onClick:i[0]||=e=>n.value.token=null},{default:O(()=>[...i[9]||=[g(`返回上一步`,-1)]]),_:1})])],64)):(x(),m(u,{key:1},[E(s).user?(x(),m(`h1`,z,[...i[11]||=[h(`span`,null,`平台账户绑定`,-1)]])):(x(),m(`h1`,B,[_(l,{data:[`用户密码登录`,`平台账户登录`],modelValue:E(M).authType,"onUpdate:modelValue":i[1]||=e=>E(M).authType=e},null,8,[`modelValue`])])),E(s).user||E(M).authType===1?(x(),m(u,{key:2},[_(v,{placeholder:`平台名`,modelValue:E(M).platform,"onUpdate:modelValue":i[2]||=e=>E(M).platform=e},{prefix:O(()=>[_(f,{name:`at`})]),_:1},8,[`modelValue`]),_(v,{placeholder:`账号`,modelValue:E(M).userId,"onUpdate:modelValue":i[3]||=e=>E(M).userId=e,onKeypress:k(A(a,[`stop`]),[`enter`])},{prefix:O(()=>[_(f,{name:`user`})]),_:1},8,[`modelValue`,`onKeypress`]),t.value?(x(),m(`p`,V,T(t.value),1)):p(`v-if`,!0),h(`div`,H,[_(o,{onClick:d},{default:O(()=>[...i[12]||=[g(`返回`,-1)]]),_:1}),_(o,{onClick:a},{default:O(()=>[...i[13]||=[g(`获取验证码`,-1)]]),_:1})])],64)):(x(),m(u,{key:3},[_(v,{placeholder:`用户名`,modelValue:E(M).name,"onUpdate:modelValue":i[4]||=e=>E(M).name=e},{prefix:O(()=>[_(f,{name:`user`})]),_:1},8,[`modelValue`]),_(v,{placeholder:`密码`,modelValue:E(M).password,"onUpdate:modelValue":i[6]||=e=>E(M).password=e,onKeypress:k(A(c,[`stop`]),[`enter`]),type:r.value?`text`:`password`},{prefix:O(()=>[_(f,{name:`lock`})]),suffix:O(()=>[_(f,{name:r.value?`eye`:`eye-slash`,onClick:i[5]||=e=>r.value=!r.value},null,8,[`name`])]),_:1},8,[`modelValue`,`onKeypress`,`type`]),t.value?(x(),m(`p`,U,T(t.value),1)):p(`v-if`,!0),h(`div`,W,[_(o,{onClick:d},{default:O(()=>[...i[14]||=[g(`返回`,-1)]]),_:1}),_(o,{onClick:c},{default:O(()=>[...i[15]||=[g(`登录`,-1)]]),_:1})])],64))],64))])}}}),K=v({__name:`bind-dialog`,setup(e){return(e,t)=>{let n=w(`el-dialog`);return x(),m(u,null,[p(` 绑定平台账户对话框:已登录状态下从个人资料页唤起,内嵌登录表单 `),_(n,{center:``,modelValue:E(N),"onUpdate:modelValue":t[0]||=e=>b(N)?N.value=e:null,class:`bind-dialog`},{default:O(()=>[_(G)]),_:1},8,[`modelValue`])],2112)}}}),q=(e,t)=>{let n=e.__vccOpts||e;for(let[e,r]of t)n[e]=r;return n},J={},Y={class:`k-icon k-icon-at`,xmlns:`http://www.w3.org/2000/svg`,viewBox:`0 0 512 512`};function X(e,t){return x(),m(u,null,[p(` @ 图标:登录表单"平台名"输入框的前缀 `),(x(),m(`svg`,Y,[...t[0]||=[h(`path`,{fill:`currentColor`,d:`M256 8C118.941 8 8 118.919 8 256c0 137.059 110.919 248 248 248 48.154 0 95.342-14.14 135.408-40.223 12.005-7.815 14.625-24.288 5.552-35.372l-10.177-12.433c-7.671-9.371-21.179-11.667-31.373-5.129C325.92 429.757 291.314 440 256 440c-101.458 0-184-82.542-184-184S154.542 72 256 72c100.139 0 184 57.619 184 160 0 38.786-21.093 79.742-58.17 83.693-17.349-.454-16.91-12.857-13.476-30.024l23.433-121.11C394.653 149.75 383.308 136 368.225 136h-44.981a13.518 13.518 0 0 0-13.432 11.993l-.01.092c-14.697-17.901-40.448-21.775-59.971-21.775-74.58 0-137.831 62.234-137.831 151.46 0 65.303 36.785 105.87 96 105.87 26.984 0 57.369-15.637 74.991-38.333 9.522 34.104 40.613 34.103 70.71 34.103C462.609 379.41 504 307.798 504 232 504 95.653 394.023 8 256 8zm-21.68 304.43c-22.249 0-36.07-15.623-36.07-40.771 0-44.993 30.779-72.729 58.63-72.729 22.292 0 35.601 15.241 35.601 40.77 0 45.061-33.875 72.73-58.161 72.73z`},null,-1)]]))],2112)}var Z=q(J,[[`render`,X]]),Q={},ne={class:`k-icon check`,xmlns:`http://www.w3.org/2000/svg`,viewBox:`0 0 448 512`};function re(e,t){return x(),m(u,null,[p(` 对勾图标:个人资料页"应用更改"菜单项 `),(x(),m(`svg`,ne,[...t[0]||=[h(`path`,{fill:`currentColor`,d:`M438.6 105.4C451.1 117.9 451.1 138.1 438.6 150.6L182.6 406.6C170.1 419.1 149.9 419.1 137.4 406.6L9.372 278.6C-3.124 266.1-3.124 245.9 9.372 233.4C21.87 220.9 42.13 220.9 54.63 233.4L159.1 338.7L393.4 105.4C405.9 92.88 426.1 92.88 438.6 105.4H438.6z`},null,-1)]]))],2112)}var ie=q(Q,[[`render`,re]]),ae={},oe={class:`k-icon k-icon-lock`,xmlns:`http://www.w3.org/2000/svg`,viewBox:`0 0 448 512`};function se(e,t){return x(),m(u,null,[p(` 挂锁图标:登录表单"密码"输入框的前缀 `),(x(),m(`svg`,oe,[...t[0]||=[h(`path`,{fill:`currentColor`,d:`M400 224h-24v-72C376 68.2 307.8 0 224 0S72 68.2 72 152v72H48c-26.5 0-48 21.5-48 48v192c0 26.5 21.5 48 48 48h352c26.5 0 48-21.5 48-48V272c0-26.5-21.5-48-48-48zm-104 0H152v-72c0-39.7 32.3-72 72-72s72 32.3 72 72v72z`},null,-1)]]))],2112)}var ce=q(ae,[[`render`,se]]),le={},ue={class:`k-icon k-icon-sign-in`,xmlns:`http://www.w3.org/2000/svg`,viewBox:`0 0 512 512`};function de(e,t){return x(),m(u,null,[p(` 登入箭头图标:登录页活动入口 `),(x(),m(`svg`,ue,[...t[0]||=[h(`path`,{fill:`currentColor`,d:`M384 256c0-8.188-3.125-16.38-9.375-22.62l-128-128C237.5 96.22 223.7 93.47 211.8 98.44C199.8 103.4 192 115.1 192 128v64H48C21.49 192 0 213.5 0 240v32C0 298.5 21.49 320 48 320H192v64c0 12.94 7.797 24.62 19.75 29.56c11.97 4.969 25.72 2.219 34.88-6.938l128-128C380.9 272.4 384 264.2 384 256zM224 384V288H48C39.18 288 32 280.8 32 272v-32C32 231.2 39.18 224 48 224H224L223.1 128l128 128L224 384zM432 32h-96C327.2 32 320 39.16 320 48S327.2 64 336 64h96C458.5 64 480 85.53 480 112v288c0 26.47-21.53 48-48 48h-96c-8.844 0-16 7.156-16 16s7.156 16 16 16h96c44.13 0 80-35.88 80-80v-288C512 67.88 476.1 32 432 32z`},null,-1)]]))],2112)}var fe=q(le,[[`render`,de]]),pe={},me={class:`k-icon k-icon-sign-out`,xmlns:`http://www.w3.org/2000/svg`,viewBox:`0 0 512 512`};function he(e,t){return x(),m(u,null,[p(` 登出箭头图标:个人资料页"退出登录"菜单项 `),(x(),m(`svg`,me,[...t[0]||=[h(`path`,{fill:`currentColor`,d:`M176 448h-96C53.53 448 32 426.5 32 400v-288C32 85.53 53.53 64 80 64h96C184.8 64 192 56.84 192 48S184.8 32 176 32h-96C35.88 32 0 67.88 0 112v288C0 444.1 35.88 480 80 480h96C184.8 480 192 472.8 192 464S184.8 448 176 448zM502.6 233.4l-128-128c-9.156-9.156-22.91-11.91-34.88-6.938C327.8 103.4 320 115.1 320 128l.0918 63.1L176 192C149.5 192 128 213.5 128 240v32C128 298.5 149.5 320 176 320l144.1-.001L320 384c0 12.94 7.797 24.62 19.75 29.56c11.97 4.969 25.72 2.219 34.88-6.938l128-128C508.9 272.4 512 264.2 512 256S508.9 239.6 502.6 233.4zM352 384V288H176C167.2 288 160 280.8 160 272v-32C160 231.2 167.2 224 176 224H352l-.0039-96l128 128L352 384z`},null,-1)]]))],2112)}var ge=q(pe,[[`render`,he]]),_e={},ve={class:`k-icon`,xmlns:`http://www.w3.org/2000/svg`,viewBox:`0 0 512 512`};function ye(e,t){return x(),m(u,null,[p(` 人像图标:个人资料页活动入口 `),(x(),m(`svg`,ve,[...t[0]||=[h(`path`,{fill:`currentColor`,d:`M256 0C114.6 0 0 114.6 0 256s114.6 256 256 256s256-114.6 256-256S397.4 0 256 0zM256 480c-47.24 0-91.04-14.78-127.2-39.84C132.9 390.9 173.8 352 224 352h64c50.25 0 91.14 38.94 95.21 88.16C347 465.2 303.2 480 256 480zM411.7 416.7C397.6 361.3 347.7 320 288 320H224c-59.73 0-109.6 41.3-123.7 96.72C58.27 375.1 32 319 32 256c0-123.5 100.5-224 224-224s224 100.5 224 224C480 319 453.7 375.1 411.7 416.7zM256 128C211.8 128 176 163.8 176 208C176 252.2 211.8 288 256 288s80-35.82 80-80C336 163.8 300.2 128 256 128zM256 256C229.5 256 208 234.5 208 208S229.5 160 256 160s48 21.53 48 48S282.5 256 256 256z`},null,-1)]]))],2112)}var be=q(_e,[[`render`,ye]]),$=v({__name:`login`,setup(e){return(e,t)=>{let n=w(`k-card`),r=w(`k-layout`);return x(),m(u,null,[p(` 登录页:居中卡片内嵌登录表单 `),_(r,{main:`darker page-login`},{default:O(()=>[_(n,null,{default:O(()=>[_(G)]),_:1})]),_:1})],2112)}}}),xe={class:`k-schema-header`},Se={class:`header`},Ce={class:`left`},we={class:`right`},Te=v({__name:`profile`,setup(t){let n={platform:`平台账户`,password:`用户密码`},i=S({}),a=d(()=>e.object({name:e.string().description(`用户名`).default(M.value.name),password:e.string().role(`secret`).description(`密码`).default(M.value.password)}).description(`基本资料`));async function c(){return s.user=null,delete M.value.id,delete M.value.token,delete M.value.expiredAt,o(`user/logout`)}async function l(){try{await o(`user/update`,i.value),r.success(`修改成功!`),Object.assign(M.value,i.value),Object.assign(s.user,i.value),i.value={}}catch(e){r.error(e.message)}}let v=d(()=>s.user?.bindings.filter(e=>s.user.id===e.bid)),y=d(()=>[{icon:`check`,label:`应用更改`,disabled:!i.value||!Object.keys(i.value).length,action:l},{type:`danger`,icon:`sign-out`,label:`退出登录`,action:c}]);return(e,t)=>{let r=w(`k-form`),c=w(`el-button`),l=w(`k-content`),d=w(`k-layout`);return x(),f(d,{main:`page-profile`,menu:y.value},{default:O(()=>[_(l,null,{default:O(()=>[_(r,{schema:a.value,modelValue:i.value,"onUpdate:modelValue":t[0]||=e=>i.value=e},null,8,[`schema`,`modelValue`]),h(`h2`,xe,[t[3]||=g(` 平台绑定 `,-1),_(c,{solid:``,class:`right`,onClick:t[1]||=e=>N.value=!0},{default:O(()=>[...t[2]||=[g(`添加`,-1)]]),_:1})]),(x(!0),m(u,null,C(E(s).user.bindings,({platform:e,pid:n,bid:r})=>(x(),m(`div`,{class:`k-schema-item`,key:`${e}:${n}`},[h(`div`,Se,[h(`div`,Ce,T(e)+` (`+T(n)+`)`,1),h(`div`,we,[v.value.length>1||r!==E(s).user.id?(x(),f(c,{key:0,onClick:A(t=>E(o)(`user/unbind`,e,n),[`stop`,`prevent`])},{default:O(()=>[...t[4]||=[g(`解绑`,-1)]]),_:1},8,[`onClick`])):p(`v-if`,!0)])])]))),128)),t[6]||=h(`h2`,{class:`k-schema-header`},`登录历史`,-1),h(`ul`,null,[(x(!0),m(u,null,C(E(s).user.tokens,({inc:e,type:r,createdAt:i,lastUsedAt:a,address:s,userAgent:l})=>(x(),m(`li`,{key:e},[h(`div`,null,`登录类型:`+T(n[r]),1),h(`div`,null,`登录时间:`+T(i),1),h(`div`,null,`最后访问:`+T(a),1),h(`div`,null,`IP 地址:`+T(s),1),h(`div`,null,`客户端:`+T(l),1),h(`div`,null,[_(c,{onClick:t=>E(o)(`user/delete-token`,e)},{default:O(()=>[...t[5]||=[g(`移除会话`,-1)]]),_:1},8,[`onClick`])])]))),128))])]),_:1})]),_:1},8,[`menu`])}}}),Ee=q(v({__name:`sync-dialog`,setup(e){let t=c();async function n(e){if(M.value.sync=!!e,P.value=!1,e){if(e===`download`){t.value=s.user.config;return}try{await o(`user/update`,{config:t.value})}catch(e){r.error(e.message)}}}return(e,r)=>{let i=w(`el-button`),a=w(`el-button-group`),o=w(`el-dialog`);return x(),f(o,{class:`sync-dialog`,onClose:r[3]||=e=>E(t).sync??=!1,"show-close":!1,"close-on-click-modal":!1,"close-on-press-escape":!1,modelValue:E(P),"onUpdate:modelValue":r[4]||=e=>b(P)?P.value=e:null},{default:O(()=>[r[8]||=h(`p`,{class:`text-center`},`检测到你本地的配置与云端不同步,是否同步配置?`,-1),_(a,{class:`text-center`},{default:O(()=>[_(i,{onClick:r[0]||=e=>n(`upload`)},{default:O(()=>[...r[5]||=[g(`上传当前配置`,-1)]]),_:1}),_(i,{onClick:r[1]||=e=>n(`download`)},{default:O(()=>[...r[6]||=[g(`下载云端配置`,-1)]]),_:1}),_(i,{onClick:r[2]||=e=>n()},{default:O(()=>[...r[7]||=[g(`关闭配置同步`,-1)]]),_:1})]),_:1})]),_:1},8,[`modelValue`])}}}),[[`__scopeId`,`data-v-440ef018`]]);n.register(`at`,Z),n.register(`check`,ie),n.register(`lock`,ce),n.register(`sign-in`,fe),n.register(`sign-out`,ge),n.register(`user-full`,be);var De=n=>{let{token:l,id:u,expiredAt:d}=M.value;l&&u!==void 0&&d!==void 0&&d>Date.now()&&o(`login/token`,u,l).catch(e=>r.error(e.message)),n.on(`activity`,e=>{let t=e.authority??0;return t>0&&(!s.user||s.user.authority<t)}),n.scope.disposables.push(a.beforeEach(e=>{let{activity:t}=e.meta;if(t){if((t.authority||(t.fields??[]).includes(`user`))&&!s.user)return history.state.forward===`/login`?`/`:`/login`;if(t.authority&&s.user&&t.authority>s.user.authority)return r.error(`权限不足。`),!1}})),n.page({path:`/login`,name:`登录`,icon:`sign-in`,position:`bottom`,order:500,disabled:()=>!!s.user,component:$}),n.page({path:`/profile`,name:`用户资料`,icon:`user-full`,fields:[`user`],position:`bottom`,order:500,component:Te}),n.slot({type:`global`,component:K}),n.slot({type:`global`,component:Ee}),n.settings({id:`user`,title:`用户设置`,component:v(()=>()=>y(w(`k-form`),{schema:e.object({sync:e.boolean().description(`在多个客户端间同步设置。`)}).description(`同步设置`),initial:M.value,modelValue:M.value,"onUpdate:modelValue":e=>M.value=e}))});let f=c();function p(){s.user&&(t(s.user.config,f.value)||(P.value=!0))}n.on(`dispose`,D(f,async e=>{!e||!s.user||!M.value.sync||await o(`user/update`,{config:e})},{deep:!0})),n.on(`dispose`,D(()=>M.value.sync,async e=>{e&&s.user&&p()})),n.on(`dispose`,D(()=>s.user,(e,t)=>{if(N.value=!1,!e)return a.push(`/login`);if(M.value.sync&&p(),t)return;Object.assign(M.value,i(e,[`id`,`name`,`token`,`expiredAt`])),r.success(`欢迎回来,${e.name||`Koishi 用户`}!`);let n=a.currentRoute.value.redirectedFrom;n&&!n.path.startsWith(`/login`)?a.push(n):a.push(`/profile`)}))};export{De as default};
|
|
1
|
+
import{Schema as e,deepEqual as t,icons as n,message as r,pick as i,router as a,send as o,store as s,useConfig as c,useStorage as l}from"@koishi-ce/client";import{Fragment as u,computed as d,createBlock as f,createCommentVNode as p,createElementBlock as m,createElementVNode as h,createTextVNode as g,createVNode as _,defineComponent as v,h as y,isRef as b,openBlock as x,ref as S,renderList as C,resolveComponent as w,toDisplayString as T,unref as E,watch as D,withCtx as O,withKeys as k,withModifiers as A}from"vue";import{useRouter as j}from"vue-router";var M=l(`auth`,2,()=>({authType:0})),N=S(!1),P=S(!1),F={class:`login-form text-center`},ee={key:0},te={key:1},I={class:`hint`},L={class:`token`},R={class:`control`},z={key:0},B={key:1},V={key:0,class:`error`},H={class:`control`},U={key:0,class:`error`},W={class:`control`},G=v({__name:`login-form`,setup(e){let t=S(),n=S(),r=S(!1),i=0;async function a(){let e=Date.now();if(e<i)return;let{platform:r,userId:a}=M.value;if(!(!r||!a)){i=e+1e3;try{n.value=await o(`login/platform`,r,a)}catch(e){t.value=e.message}}}async function c(){let{name:e,password:n}=M.value;try{await o(`login/password`,e,n)}catch(e){t.value=e.message}}let l=j();function d(){s.user?N.value=!1:l.back()}return(e,i)=>{let o=w(`k-button`),l=w(`k-tab`),f=w(`k-icon`),v=w(`el-input`);return x(),m(`div`,F,[n.value?(x(),m(u,{key:0},[E(s).user?(x(),m(`h1`,ee,[...i[7]||=[h(`span`,null,`平台账户绑定`,-1)]])):(x(),m(`h1`,te,[...i[8]||=[h(`span`,null,`平台账户登录`,-1)]])),h(`p`,I,`欢迎你,`+T(n.value.name||`Koishi 用户`)+`!`,1),i[10]||=h(`p`,{class:`hint`},`请用上述账号将下面的验证码发送给任意正在运行的机器人`,-1),h(`p`,L,T(n.value.token),1),h(`div`,R,[_(o,{onClick:i[0]||=e=>n.value.token=null},{default:O(()=>[...i[9]||=[g(`返回上一步`,-1)]]),_:1})])],64)):(x(),m(u,{key:1},[E(s).user?(x(),m(`h1`,z,[...i[11]||=[h(`span`,null,`平台账户绑定`,-1)]])):(x(),m(`h1`,B,[_(l,{data:[`用户密码登录`,`平台账户登录`],modelValue:E(M).authType,"onUpdate:modelValue":i[1]||=e=>E(M).authType=e},null,8,[`modelValue`])])),E(s).user||E(M).authType===1?(x(),m(u,{key:2},[_(v,{placeholder:`平台名`,modelValue:E(M).platform,"onUpdate:modelValue":i[2]||=e=>E(M).platform=e},{prefix:O(()=>[_(f,{name:`at`})]),_:1},8,[`modelValue`]),_(v,{placeholder:`账号`,modelValue:E(M).userId,"onUpdate:modelValue":i[3]||=e=>E(M).userId=e,onKeypress:k(A(a,[`stop`]),[`enter`])},{prefix:O(()=>[_(f,{name:`user`})]),_:1},8,[`modelValue`,`onKeypress`]),t.value?(x(),m(`p`,V,T(t.value),1)):p(`v-if`,!0),h(`div`,H,[_(o,{onClick:d},{default:O(()=>[...i[12]||=[g(`返回`,-1)]]),_:1}),_(o,{onClick:a},{default:O(()=>[...i[13]||=[g(`获取验证码`,-1)]]),_:1})])],64)):(x(),m(u,{key:3},[_(v,{placeholder:`用户名`,modelValue:E(M).name,"onUpdate:modelValue":i[4]||=e=>E(M).name=e},{prefix:O(()=>[_(f,{name:`user`})]),_:1},8,[`modelValue`]),_(v,{placeholder:`密码`,modelValue:E(M).password,"onUpdate:modelValue":i[6]||=e=>E(M).password=e,onKeypress:k(A(c,[`stop`]),[`enter`]),type:r.value?`text`:`password`},{prefix:O(()=>[_(f,{name:`lock`})]),suffix:O(()=>[_(f,{name:r.value?`eye`:`eye-slash`,onClick:i[5]||=e=>r.value=!r.value},null,8,[`name`])]),_:1},8,[`modelValue`,`onKeypress`,`type`]),t.value?(x(),m(`p`,U,T(t.value),1)):p(`v-if`,!0),h(`div`,W,[_(o,{onClick:d},{default:O(()=>[...i[14]||=[g(`返回`,-1)]]),_:1}),_(o,{onClick:c},{default:O(()=>[...i[15]||=[g(`登录`,-1)]]),_:1})])],64))],64))])}}}),K=v({__name:`bind-dialog`,setup(e){return(e,t)=>{let n=w(`el-dialog`);return x(),f(n,{center:``,modelValue:E(N),"onUpdate:modelValue":t[0]||=e=>b(N)?N.value=e:null,class:`bind-dialog`},{default:O(()=>[_(G)]),_:1},8,[`modelValue`])}}}),q=(e,t)=>{let n=e.__vccOpts||e;for(let[e,r]of t)n[e]=r;return n},J={},Y={class:`k-icon k-icon-at`,xmlns:`http://www.w3.org/2000/svg`,viewBox:`0 0 512 512`};function X(e,t){return x(),m(`svg`,Y,[...t[0]||=[h(`path`,{fill:`currentColor`,d:`M256 8C118.941 8 8 118.919 8 256c0 137.059 110.919 248 248 248 48.154 0 95.342-14.14 135.408-40.223 12.005-7.815 14.625-24.288 5.552-35.372l-10.177-12.433c-7.671-9.371-21.179-11.667-31.373-5.129C325.92 429.757 291.314 440 256 440c-101.458 0-184-82.542-184-184S154.542 72 256 72c100.139 0 184 57.619 184 160 0 38.786-21.093 79.742-58.17 83.693-17.349-.454-16.91-12.857-13.476-30.024l23.433-121.11C394.653 149.75 383.308 136 368.225 136h-44.981a13.518 13.518 0 0 0-13.432 11.993l-.01.092c-14.697-17.901-40.448-21.775-59.971-21.775-74.58 0-137.831 62.234-137.831 151.46 0 65.303 36.785 105.87 96 105.87 26.984 0 57.369-15.637 74.991-38.333 9.522 34.104 40.613 34.103 70.71 34.103C462.609 379.41 504 307.798 504 232 504 95.653 394.023 8 256 8zm-21.68 304.43c-22.249 0-36.07-15.623-36.07-40.771 0-44.993 30.779-72.729 58.63-72.729 22.292 0 35.601 15.241 35.601 40.77 0 45.061-33.875 72.73-58.161 72.73z`},null,-1)]])}var Z=q(J,[[`render`,X]]),Q={},ne={class:`k-icon check`,xmlns:`http://www.w3.org/2000/svg`,viewBox:`0 0 448 512`};function re(e,t){return x(),m(`svg`,ne,[...t[0]||=[h(`path`,{fill:`currentColor`,d:`M438.6 105.4C451.1 117.9 451.1 138.1 438.6 150.6L182.6 406.6C170.1 419.1 149.9 419.1 137.4 406.6L9.372 278.6C-3.124 266.1-3.124 245.9 9.372 233.4C21.87 220.9 42.13 220.9 54.63 233.4L159.1 338.7L393.4 105.4C405.9 92.88 426.1 92.88 438.6 105.4H438.6z`},null,-1)]])}var ie=q(Q,[[`render`,re]]),ae={},oe={class:`k-icon k-icon-lock`,xmlns:`http://www.w3.org/2000/svg`,viewBox:`0 0 448 512`};function se(e,t){return x(),m(`svg`,oe,[...t[0]||=[h(`path`,{fill:`currentColor`,d:`M400 224h-24v-72C376 68.2 307.8 0 224 0S72 68.2 72 152v72H48c-26.5 0-48 21.5-48 48v192c0 26.5 21.5 48 48 48h352c26.5 0 48-21.5 48-48V272c0-26.5-21.5-48-48-48zm-104 0H152v-72c0-39.7 32.3-72 72-72s72 32.3 72 72v72z`},null,-1)]])}var ce=q(ae,[[`render`,se]]),le={},ue={class:`k-icon k-icon-sign-in`,xmlns:`http://www.w3.org/2000/svg`,viewBox:`0 0 512 512`};function de(e,t){return x(),m(`svg`,ue,[...t[0]||=[h(`path`,{fill:`currentColor`,d:`M384 256c0-8.188-3.125-16.38-9.375-22.62l-128-128C237.5 96.22 223.7 93.47 211.8 98.44C199.8 103.4 192 115.1 192 128v64H48C21.49 192 0 213.5 0 240v32C0 298.5 21.49 320 48 320H192v64c0 12.94 7.797 24.62 19.75 29.56c11.97 4.969 25.72 2.219 34.88-6.938l128-128C380.9 272.4 384 264.2 384 256zM224 384V288H48C39.18 288 32 280.8 32 272v-32C32 231.2 39.18 224 48 224H224L223.1 128l128 128L224 384zM432 32h-96C327.2 32 320 39.16 320 48S327.2 64 336 64h96C458.5 64 480 85.53 480 112v288c0 26.47-21.53 48-48 48h-96c-8.844 0-16 7.156-16 16s7.156 16 16 16h96c44.13 0 80-35.88 80-80v-288C512 67.88 476.1 32 432 32z`},null,-1)]])}var fe=q(le,[[`render`,de]]),pe={},me={class:`k-icon k-icon-sign-out`,xmlns:`http://www.w3.org/2000/svg`,viewBox:`0 0 512 512`};function he(e,t){return x(),m(`svg`,me,[...t[0]||=[h(`path`,{fill:`currentColor`,d:`M176 448h-96C53.53 448 32 426.5 32 400v-288C32 85.53 53.53 64 80 64h96C184.8 64 192 56.84 192 48S184.8 32 176 32h-96C35.88 32 0 67.88 0 112v288C0 444.1 35.88 480 80 480h96C184.8 480 192 472.8 192 464S184.8 448 176 448zM502.6 233.4l-128-128c-9.156-9.156-22.91-11.91-34.88-6.938C327.8 103.4 320 115.1 320 128l.0918 63.1L176 192C149.5 192 128 213.5 128 240v32C128 298.5 149.5 320 176 320l144.1-.001L320 384c0 12.94 7.797 24.62 19.75 29.56c11.97 4.969 25.72 2.219 34.88-6.938l128-128C508.9 272.4 512 264.2 512 256S508.9 239.6 502.6 233.4zM352 384V288H176C167.2 288 160 280.8 160 272v-32C160 231.2 167.2 224 176 224H352l-.0039-96l128 128L352 384z`},null,-1)]])}var ge=q(pe,[[`render`,he]]),_e={},ve={class:`k-icon`,xmlns:`http://www.w3.org/2000/svg`,viewBox:`0 0 512 512`};function ye(e,t){return x(),m(`svg`,ve,[...t[0]||=[h(`path`,{fill:`currentColor`,d:`M256 0C114.6 0 0 114.6 0 256s114.6 256 256 256s256-114.6 256-256S397.4 0 256 0zM256 480c-47.24 0-91.04-14.78-127.2-39.84C132.9 390.9 173.8 352 224 352h64c50.25 0 91.14 38.94 95.21 88.16C347 465.2 303.2 480 256 480zM411.7 416.7C397.6 361.3 347.7 320 288 320H224c-59.73 0-109.6 41.3-123.7 96.72C58.27 375.1 32 319 32 256c0-123.5 100.5-224 224-224s224 100.5 224 224C480 319 453.7 375.1 411.7 416.7zM256 128C211.8 128 176 163.8 176 208C176 252.2 211.8 288 256 288s80-35.82 80-80C336 163.8 300.2 128 256 128zM256 256C229.5 256 208 234.5 208 208S229.5 160 256 160s48 21.53 48 48S282.5 256 256 256z`},null,-1)]])}var be=q(_e,[[`render`,ye]]),$=v({__name:`login`,setup(e){return(e,t)=>{let n=w(`k-card`),r=w(`k-layout`);return x(),f(r,{main:`darker page-login`},{default:O(()=>[_(n,null,{default:O(()=>[_(G)]),_:1})]),_:1})}}}),xe={class:`k-schema-header`},Se={class:`header`},Ce={class:`left`},we={class:`right`},Te=v({__name:`profile`,setup(t){let n={platform:`平台账户`,password:`用户密码`},i=S({}),a=d(()=>e.object({name:e.string().description(`用户名`).default(M.value.name),password:e.string().role(`secret`).description(`密码`).default(M.value.password)}).description(`基本资料`));async function c(){return s.user=null,delete M.value.id,delete M.value.token,delete M.value.expiredAt,o(`user/logout`)}async function l(){try{await o(`user/update`,i.value),r.success(`修改成功!`),Object.assign(M.value,i.value),Object.assign(s.user,i.value),i.value={}}catch(e){r.error(e.message)}}let v=d(()=>s.user?.bindings.filter(e=>s.user.id===e.bid)),y=d(()=>[{icon:`check`,label:`应用更改`,disabled:!i.value||!Object.keys(i.value).length,action:l},{type:`danger`,icon:`sign-out`,label:`退出登录`,action:c}]);return(e,t)=>{let r=w(`k-form`),c=w(`el-button`),l=w(`k-content`),d=w(`k-layout`);return x(),f(d,{main:`page-profile`,menu:y.value},{default:O(()=>[_(l,null,{default:O(()=>[_(r,{schema:a.value,modelValue:i.value,"onUpdate:modelValue":t[0]||=e=>i.value=e},null,8,[`schema`,`modelValue`]),h(`h2`,xe,[t[3]||=g(` 平台绑定 `,-1),_(c,{solid:``,class:`right`,onClick:t[1]||=e=>N.value=!0},{default:O(()=>[...t[2]||=[g(`添加`,-1)]]),_:1})]),(x(!0),m(u,null,C(E(s).user.bindings,({platform:e,pid:n,bid:r})=>(x(),m(`div`,{class:`k-schema-item`,key:`${e}:${n}`},[h(`div`,Se,[h(`div`,Ce,T(e)+` (`+T(n)+`)`,1),h(`div`,we,[v.value.length>1||r!==E(s).user.id?(x(),f(c,{key:0,onClick:A(t=>E(o)(`user/unbind`,e,n),[`stop`,`prevent`])},{default:O(()=>[...t[4]||=[g(`解绑`,-1)]]),_:1},8,[`onClick`])):p(`v-if`,!0)])])]))),128)),t[6]||=h(`h2`,{class:`k-schema-header`},`登录历史`,-1),h(`ul`,null,[(x(!0),m(u,null,C(E(s).user.tokens,({inc:e,type:r,createdAt:i,lastUsedAt:a,address:s,userAgent:l})=>(x(),m(`li`,{key:e},[h(`div`,null,`登录类型:`+T(n[r]),1),h(`div`,null,`登录时间:`+T(i),1),h(`div`,null,`最后访问:`+T(a),1),h(`div`,null,`IP 地址:`+T(s),1),h(`div`,null,`客户端:`+T(l),1),h(`div`,null,[_(c,{onClick:t=>E(o)(`user/delete-token`,e)},{default:O(()=>[...t[5]||=[g(`移除会话`,-1)]]),_:1},8,[`onClick`])])]))),128))])]),_:1})]),_:1},8,[`menu`])}}}),Ee=q(v({__name:`sync-dialog`,setup(e){let t=c();async function n(e){if(M.value.sync=!!e,P.value=!1,e){if(e===`download`){t.value=s.user.config;return}try{await o(`user/update`,{config:t.value})}catch(e){r.error(e.message)}}}return(e,r)=>{let i=w(`el-button`),a=w(`el-button-group`),o=w(`el-dialog`);return x(),f(o,{class:`sync-dialog`,onClose:r[3]||=e=>E(t).sync??=!1,"show-close":!1,"close-on-click-modal":!1,"close-on-press-escape":!1,modelValue:E(P),"onUpdate:modelValue":r[4]||=e=>b(P)?P.value=e:null},{default:O(()=>[r[8]||=h(`p`,{class:`text-center`},`检测到你本地的配置与云端不同步,是否同步配置?`,-1),_(a,{class:`text-center`},{default:O(()=>[_(i,{onClick:r[0]||=e=>n(`upload`)},{default:O(()=>[...r[5]||=[g(`上传当前配置`,-1)]]),_:1}),_(i,{onClick:r[1]||=e=>n(`download`)},{default:O(()=>[...r[6]||=[g(`下载云端配置`,-1)]]),_:1}),_(i,{onClick:r[2]||=e=>n()},{default:O(()=>[...r[7]||=[g(`关闭配置同步`,-1)]]),_:1})]),_:1})]),_:1},8,[`modelValue`])}}}),[[`__scopeId`,`data-v-440ef018`]]);n.register(`at`,Z),n.register(`check`,ie),n.register(`lock`,ce),n.register(`sign-in`,fe),n.register(`sign-out`,ge),n.register(`user-full`,be);var De=n=>{let{token:l,id:u,expiredAt:d}=M.value;l&&u!==void 0&&d!==void 0&&d>Date.now()&&o(`login/token`,u,l).catch(e=>r.error(e.message)),n.on(`activity`,e=>{let t=e.authority??0;return t>0&&(!s.user||s.user.authority<t)}),n.scope.disposables.push(a.beforeEach(e=>{let{activity:t}=e.meta;if(t){if((t.authority||(t.fields??[]).includes(`user`))&&!s.user)return history.state.forward===`/login`?`/`:`/login`;if(t.authority&&s.user&&t.authority>s.user.authority)return r.error(`权限不足。`),!1}})),n.page({path:`/login`,name:`登录`,icon:`sign-in`,position:`bottom`,order:500,disabled:()=>!!s.user,component:$}),n.page({path:`/profile`,name:`用户资料`,icon:`user-full`,fields:[`user`],position:`bottom`,order:500,component:Te}),n.slot({type:`global`,component:K}),n.slot({type:`global`,component:Ee}),n.settings({id:`user`,title:`用户设置`,component:v(()=>()=>y(w(`k-form`),{schema:e.object({sync:e.boolean().description(`在多个客户端间同步设置。`)}).description(`同步设置`),initial:M.value,modelValue:M.value,"onUpdate:modelValue":e=>M.value=e}))});let f=c();function p(){s.user&&(t(s.user.config,f.value)||(P.value=!0))}n.on(`dispose`,D(f,async e=>{!e||!s.user||!M.value.sync||await o(`user/update`,{config:e})},{deep:!0})),n.on(`dispose`,D(()=>M.value.sync,async e=>{e&&s.user&&p()})),n.on(`dispose`,D(()=>s.user,(e,t)=>{if(N.value=!1,!e)return a.push(`/login`);if(M.value.sync&&p(),t)return;Object.assign(M.value,i(e,[`id`,`name`,`token`,`expiredAt`])),r.success(`欢迎回来,${e.name||`Koishi 用户`}!`);let n=a.currentRoute.value.redirectedFrom;n&&!n.path.startsWith(`/login`)?a.push(n):a.push(`/profile`)}))};export{De as default};
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@koishi-ce/plugin-auth",
|
|
3
3
|
"description": "Console User Authentication",
|
|
4
|
-
"version": "1.0.
|
|
4
|
+
"version": "1.0.2",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.mjs",
|
|
7
7
|
"typings": "lib/index.d.ts",
|
|
@@ -54,7 +54,7 @@
|
|
|
54
54
|
"@koishi-ce/koishi": "^1.0.0"
|
|
55
55
|
},
|
|
56
56
|
"devDependencies": {
|
|
57
|
-
"@koishi-ce/client": "^1.0.
|
|
57
|
+
"@koishi-ce/client": "^1.0.4",
|
|
58
58
|
"vue": "^3.5.12",
|
|
59
59
|
"vue-router": "^5.2.0"
|
|
60
60
|
},
|
|
@@ -11,12 +11,35 @@
|
|
|
11
11
|
* 令牌删除 / 登出 / 资料更新 / 解绑等用户管理事件。
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
|
-
import {
|
|
15
|
-
|
|
14
|
+
import {
|
|
15
|
+
afterAll,
|
|
16
|
+
beforeAll,
|
|
17
|
+
describe,
|
|
18
|
+
expect,
|
|
19
|
+
it,
|
|
20
|
+
} from "bun:test";
|
|
21
|
+
import {
|
|
22
|
+
createHash,
|
|
23
|
+
pbkdf2Sync,
|
|
24
|
+
randomBytes,
|
|
25
|
+
} from "node:crypto";
|
|
16
26
|
import type { IncomingMessage } from "node:http";
|
|
17
|
-
import {
|
|
18
|
-
|
|
19
|
-
|
|
27
|
+
import {
|
|
28
|
+
type Client,
|
|
29
|
+
Console,
|
|
30
|
+
type Entry,
|
|
31
|
+
} from "@koishi-ce/console";
|
|
32
|
+
import {
|
|
33
|
+
App,
|
|
34
|
+
Logger,
|
|
35
|
+
type Plugin,
|
|
36
|
+
Time,
|
|
37
|
+
type Universal,
|
|
38
|
+
} from "@koishi-ce/koishi";
|
|
39
|
+
import auth, {
|
|
40
|
+
type Auth,
|
|
41
|
+
randomId,
|
|
42
|
+
} from "@koishi-ce/plugin-auth";
|
|
20
43
|
import mockClient from "@koishi-ce/plugin-mock";
|
|
21
44
|
import memory from "@koishijs/plugin-database-memory";
|
|
22
45
|
|
|
@@ -42,21 +65,32 @@ interface SentMessage {
|
|
|
42
65
|
class FakeSocket {
|
|
43
66
|
sent: string[] = [];
|
|
44
67
|
// message 与 close 的监听器统一为同构签名(never 载荷),保证集合存取类型一致
|
|
45
|
-
private messageHandlers = new Set<
|
|
68
|
+
private messageHandlers = new Set<
|
|
69
|
+
(event: never) => void
|
|
70
|
+
>();
|
|
46
71
|
private closeHandlers = new Set<(event: never) => void>();
|
|
47
72
|
|
|
48
73
|
send(data: string) {
|
|
49
74
|
this.sent.push(data);
|
|
50
75
|
}
|
|
51
76
|
|
|
52
|
-
addEventListener(
|
|
53
|
-
|
|
77
|
+
addEventListener(
|
|
78
|
+
type: string,
|
|
79
|
+
listener: (event: never) => void,
|
|
80
|
+
) {
|
|
81
|
+
if (type === "message")
|
|
82
|
+
this.messageHandlers.add(listener);
|
|
54
83
|
if (type === "close") this.closeHandlers.add(listener);
|
|
55
84
|
}
|
|
56
85
|
|
|
57
|
-
removeEventListener(
|
|
58
|
-
|
|
59
|
-
|
|
86
|
+
removeEventListener(
|
|
87
|
+
type: string,
|
|
88
|
+
listener: (event: never) => void,
|
|
89
|
+
) {
|
|
90
|
+
if (type === "message")
|
|
91
|
+
this.messageHandlers.delete(listener);
|
|
92
|
+
if (type === "close")
|
|
93
|
+
this.closeHandlers.delete(listener);
|
|
60
94
|
}
|
|
61
95
|
|
|
62
96
|
receive(text: string) {
|
|
@@ -67,7 +101,8 @@ class FakeSocket {
|
|
|
67
101
|
|
|
68
102
|
shutdown() {
|
|
69
103
|
// close 监听器签名统一带 never 载荷,调用时补占位实参
|
|
70
|
-
for (const handler of this.closeHandlers)
|
|
104
|
+
for (const handler of this.closeHandlers)
|
|
105
|
+
handler(undefined as never);
|
|
71
106
|
}
|
|
72
107
|
|
|
73
108
|
get socket(): Universal.WebSocket {
|
|
@@ -90,15 +125,23 @@ function tick(ms = 30) {
|
|
|
90
125
|
class TestConsole extends Console {
|
|
91
126
|
resolveEntry(files: Entry.Files, key: string): string[] {
|
|
92
127
|
const list =
|
|
93
|
-
typeof files === "string" || Array.isArray(files)
|
|
128
|
+
typeof files === "string" || Array.isArray(files)
|
|
129
|
+
? files
|
|
130
|
+
: files.prod;
|
|
94
131
|
return [String(list), key];
|
|
95
132
|
}
|
|
96
133
|
|
|
97
|
-
acceptClient(
|
|
134
|
+
acceptClient(
|
|
135
|
+
socket: Universal.WebSocket,
|
|
136
|
+
request: IncomingMessage,
|
|
137
|
+
): Client {
|
|
98
138
|
let accepted: Client | undefined;
|
|
99
|
-
const dispose = this.ctx.on(
|
|
100
|
-
|
|
101
|
-
|
|
139
|
+
const dispose = this.ctx.on(
|
|
140
|
+
"console/connection",
|
|
141
|
+
(client) => {
|
|
142
|
+
accepted = client;
|
|
143
|
+
},
|
|
144
|
+
);
|
|
102
145
|
this.accept(socket, request);
|
|
103
146
|
dispose();
|
|
104
147
|
if (!accepted) throw new Error("client not accepted");
|
|
@@ -112,7 +155,10 @@ class TestClient {
|
|
|
112
155
|
readonly client: Client;
|
|
113
156
|
private nextId = 0;
|
|
114
157
|
|
|
115
|
-
constructor(
|
|
158
|
+
constructor(
|
|
159
|
+
service: TestConsole,
|
|
160
|
+
headers: Record<string, string> = {},
|
|
161
|
+
) {
|
|
116
162
|
this.client = service.acceptClient(
|
|
117
163
|
this.socket.socket,
|
|
118
164
|
fakeRequest(headers),
|
|
@@ -120,13 +166,18 @@ class TestClient {
|
|
|
120
166
|
}
|
|
121
167
|
|
|
122
168
|
/** 发起一次 RPC 并轮询等待回执(PBKDF2 校验耗时较长,固定等待不可靠) */
|
|
123
|
-
async call(
|
|
169
|
+
async call(
|
|
170
|
+
type: string,
|
|
171
|
+
args: unknown[],
|
|
172
|
+
timeout = 5000,
|
|
173
|
+
) {
|
|
124
174
|
const id = ++this.nextId;
|
|
125
175
|
this.socket.receive(JSON.stringify({ type, args, id }));
|
|
126
176
|
const deadline = Date.now() + timeout;
|
|
127
177
|
for (;;) {
|
|
128
178
|
const response = this.messages().find(
|
|
129
|
-
(msg) =>
|
|
179
|
+
(msg) =>
|
|
180
|
+
msg.type === "response" && msg.body.id === id,
|
|
130
181
|
);
|
|
131
182
|
if (response) return response.body;
|
|
132
183
|
if (Date.now() > deadline) {
|
|
@@ -139,7 +190,10 @@ class TestClient {
|
|
|
139
190
|
/** 最近一条 user 数据消息 */
|
|
140
191
|
lastUserData() {
|
|
141
192
|
const data = this.messages()
|
|
142
|
-
.filter(
|
|
193
|
+
.filter(
|
|
194
|
+
(msg) =>
|
|
195
|
+
msg.type === "data" && msg.body.key === "user",
|
|
196
|
+
)
|
|
143
197
|
.at(-1);
|
|
144
198
|
return data?.body.value as
|
|
145
199
|
| (Auth & { tokens: unknown[] })
|
|
@@ -148,7 +202,9 @@ class TestClient {
|
|
|
148
202
|
}
|
|
149
203
|
|
|
150
204
|
messages(): SentMessage[] {
|
|
151
|
-
return this.socket.sent.map(
|
|
205
|
+
return this.socket.sent.map(
|
|
206
|
+
(line) => JSON.parse(line) as SentMessage,
|
|
207
|
+
);
|
|
152
208
|
}
|
|
153
209
|
|
|
154
210
|
close() {
|
|
@@ -161,9 +217,15 @@ const app = new App();
|
|
|
161
217
|
app.plugin(memory as unknown as typeof memory.default);
|
|
162
218
|
app.plugin(mockClient);
|
|
163
219
|
// Console 基类的 static inject 是 cordis 3 旧形态,与 Plugin.Constructor 期待类型不兼容,仅做类型层转型
|
|
164
|
-
app.plugin(
|
|
220
|
+
app.plugin(
|
|
221
|
+
TestConsole as unknown as Plugin.Constructor<App>,
|
|
222
|
+
);
|
|
165
223
|
app.plugin(auth, {
|
|
166
|
-
admin: {
|
|
224
|
+
admin: {
|
|
225
|
+
enabled: true,
|
|
226
|
+
username: "root",
|
|
227
|
+
password: "admin-pass",
|
|
228
|
+
},
|
|
167
229
|
authTokenExpire: Time.week,
|
|
168
230
|
loginTokenExpire: Time.minute * 5,
|
|
169
231
|
});
|
|
@@ -173,12 +235,18 @@ let aliceId = 0;
|
|
|
173
235
|
let bobId = 0;
|
|
174
236
|
|
|
175
237
|
beforeAll(async () => {
|
|
238
|
+
// 「creating admin account」是启动期一次性生命周期 info,收敛为仅错误级
|
|
239
|
+
(Logger.levels as Record<string, number>)["auth"] = 1;
|
|
176
240
|
await app.start();
|
|
177
241
|
// alice:用于平台验证码登录;bob:用于绑定改挂场景
|
|
178
|
-
const alice = await app.database.createUser(
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
242
|
+
const alice = await app.database.createUser(
|
|
243
|
+
"mock",
|
|
244
|
+
"111",
|
|
245
|
+
{
|
|
246
|
+
name: "alice",
|
|
247
|
+
authority: 2,
|
|
248
|
+
},
|
|
249
|
+
);
|
|
182
250
|
const bob = await app.database.createUser("mock", "222", {
|
|
183
251
|
name: "bob",
|
|
184
252
|
authority: 1,
|
|
@@ -194,6 +262,7 @@ afterAll(async () => {
|
|
|
194
262
|
}
|
|
195
263
|
await tick();
|
|
196
264
|
await app.stop();
|
|
265
|
+
delete (Logger.levels as Record<string, number>)["auth"];
|
|
197
266
|
});
|
|
198
267
|
|
|
199
268
|
/** 以管理员身份登录并返回客户端 */
|
|
@@ -202,16 +271,23 @@ async function loginAdmin() {
|
|
|
202
271
|
"user-agent": "test-agent",
|
|
203
272
|
"x-forwarded-for": "1.2.3.4",
|
|
204
273
|
});
|
|
205
|
-
await client.call("login/password", [
|
|
274
|
+
await client.call("login/password", [
|
|
275
|
+
"root",
|
|
276
|
+
"admin-pass",
|
|
277
|
+
]);
|
|
206
278
|
return client;
|
|
207
279
|
}
|
|
208
280
|
|
|
209
281
|
describe("@koishi-ce/plugin-auth", () => {
|
|
210
282
|
it("启动时创建管理员账户;randomId 生成随机令牌", async () => {
|
|
211
|
-
const [admin] = await app.database.get("user", {
|
|
283
|
+
const [admin] = await app.database.get("user", {
|
|
284
|
+
id: 0,
|
|
285
|
+
});
|
|
212
286
|
expect(admin?.name).toBe("root");
|
|
213
287
|
expect(admin?.authority).toBe(5);
|
|
214
|
-
expect(admin?.password?.startsWith("pbkdf2$")).toBe(
|
|
288
|
+
expect(admin?.password?.startsWith("pbkdf2$")).toBe(
|
|
289
|
+
true,
|
|
290
|
+
);
|
|
215
291
|
expect(randomId(8)).toMatch(/^[0-9a-zA-Z]{8}$/);
|
|
216
292
|
expect(randomId()).toHaveLength(40);
|
|
217
293
|
});
|
|
@@ -219,9 +295,15 @@ describe("@koishi-ce/plugin-auth", () => {
|
|
|
219
295
|
describe("密码登录", () => {
|
|
220
296
|
it("密码错误与账户缺失时拒绝", async () => {
|
|
221
297
|
const client = new TestClient(service());
|
|
222
|
-
const wrong = await client.call("login/password", [
|
|
298
|
+
const wrong = await client.call("login/password", [
|
|
299
|
+
"root",
|
|
300
|
+
"wrong-pass",
|
|
301
|
+
]);
|
|
223
302
|
expect(wrong.error).toContain("用户名或密码错误");
|
|
224
|
-
const missing = await client.call("login/password", [
|
|
303
|
+
const missing = await client.call("login/password", [
|
|
304
|
+
"nobody",
|
|
305
|
+
"x",
|
|
306
|
+
]);
|
|
225
307
|
expect(missing.error).toContain("用户名或密码错误");
|
|
226
308
|
client.close();
|
|
227
309
|
});
|
|
@@ -256,8 +338,12 @@ describe("@koishi-ce/plugin-auth", () => {
|
|
|
256
338
|
});
|
|
257
339
|
|
|
258
340
|
it("旧版无盐 SHA-256 哈希校验通过后透明升级", async () => {
|
|
259
|
-
const legacy = createHash("sha256")
|
|
260
|
-
|
|
341
|
+
const legacy = createHash("sha256")
|
|
342
|
+
.update("legacy-pass")
|
|
343
|
+
.digest("hex");
|
|
344
|
+
await app.database.set("user", 0, {
|
|
345
|
+
password: legacy,
|
|
346
|
+
});
|
|
261
347
|
|
|
262
348
|
const client = new TestClient(service());
|
|
263
349
|
const response = await client.call("login/password", [
|
|
@@ -265,36 +351,59 @@ describe("@koishi-ce/plugin-auth", () => {
|
|
|
265
351
|
"legacy-pass",
|
|
266
352
|
]);
|
|
267
353
|
expect(response.error).toBeUndefined();
|
|
268
|
-
const [row] = await app.database.get(
|
|
269
|
-
|
|
354
|
+
const [row] = await app.database.get(
|
|
355
|
+
"user",
|
|
356
|
+
{ id: 0 },
|
|
357
|
+
["password"],
|
|
358
|
+
);
|
|
359
|
+
expect(row?.password?.startsWith("pbkdf2$")).toBe(
|
|
360
|
+
true,
|
|
361
|
+
);
|
|
270
362
|
client.close();
|
|
271
363
|
});
|
|
272
364
|
|
|
273
365
|
it("畸形哈希与空密码一律拒绝", async () => {
|
|
274
|
-
await app.database.set("user", 0, {
|
|
366
|
+
await app.database.set("user", 0, {
|
|
367
|
+
password: "pbkdf2$malformed",
|
|
368
|
+
});
|
|
275
369
|
const malformed = new TestClient(service());
|
|
276
370
|
expect(
|
|
277
|
-
(
|
|
371
|
+
(
|
|
372
|
+
await malformed.call("login/password", [
|
|
373
|
+
"root",
|
|
374
|
+
"x",
|
|
375
|
+
])
|
|
376
|
+
).error,
|
|
278
377
|
).toContain("用户名或密码错误");
|
|
279
378
|
malformed.close();
|
|
280
379
|
|
|
281
|
-
await app.database.set("user", 0, {
|
|
380
|
+
await app.database.set("user", 0, {
|
|
381
|
+
password: "not-a-hash-at-all",
|
|
382
|
+
});
|
|
282
383
|
const alien = new TestClient(service());
|
|
283
384
|
expect(
|
|
284
|
-
(await alien.call("login/password", ["root", "x"]))
|
|
385
|
+
(await alien.call("login/password", ["root", "x"]))
|
|
386
|
+
.error,
|
|
285
387
|
).toContain("用户名或密码错误");
|
|
286
388
|
alien.close();
|
|
287
389
|
|
|
288
390
|
await app.database.set("user", 0, { password: "" });
|
|
289
391
|
const empty = new TestClient(service());
|
|
290
392
|
expect(
|
|
291
|
-
(await empty.call("login/password", ["root", "x"]))
|
|
393
|
+
(await empty.call("login/password", ["root", "x"]))
|
|
394
|
+
.error,
|
|
292
395
|
).toContain("用户名或密码错误");
|
|
293
396
|
empty.close();
|
|
294
397
|
|
|
295
398
|
// 还原为合法管理员密码(与插件同格式的 PBKDF2 哈希)供后续用例使用
|
|
296
399
|
const salt = randomBytes(16);
|
|
297
|
-
const dk = pbkdf2Sync(
|
|
400
|
+
const dk = pbkdf2Sync(
|
|
401
|
+
"admin-pass",
|
|
402
|
+
salt,
|
|
403
|
+
600_000,
|
|
404
|
+
32,
|
|
405
|
+
"sha256",
|
|
406
|
+
);
|
|
298
407
|
await app.database.set("user", 0, {
|
|
299
408
|
password: `pbkdf2$600000$${salt.toString("hex")}$${dk.toString("hex")}`,
|
|
300
409
|
});
|
|
@@ -308,11 +417,16 @@ describe("@koishi-ce/plugin-auth", () => {
|
|
|
308
417
|
expect(token).toBeTruthy();
|
|
309
418
|
|
|
310
419
|
const client = new TestClient(service());
|
|
311
|
-
const response = await client.call("login/token", [
|
|
420
|
+
const response = await client.call("login/token", [
|
|
421
|
+
0,
|
|
422
|
+
token,
|
|
423
|
+
]);
|
|
312
424
|
expect(response.error).toBeUndefined();
|
|
313
425
|
expect(client.client.auth?.id).toBe(0);
|
|
314
426
|
|
|
315
|
-
const [row] = await app.database.get("token", {
|
|
427
|
+
const [row] = await app.database.get("token", {
|
|
428
|
+
token: token!,
|
|
429
|
+
});
|
|
316
430
|
expect(row?.lastUsedAt?.valueOf()).toBeGreaterThan(
|
|
317
431
|
row?.createdAt?.valueOf() ?? 0,
|
|
318
432
|
);
|
|
@@ -344,13 +458,28 @@ describe("@koishi-ce/plugin-auth", () => {
|
|
|
344
458
|
|
|
345
459
|
const client = new TestClient(service());
|
|
346
460
|
expect(
|
|
347
|
-
(
|
|
461
|
+
(
|
|
462
|
+
await client.call("login/token", [
|
|
463
|
+
0,
|
|
464
|
+
"expired-token",
|
|
465
|
+
])
|
|
466
|
+
).error,
|
|
348
467
|
).toContain("令牌已失效");
|
|
349
468
|
expect(
|
|
350
|
-
(
|
|
469
|
+
(
|
|
470
|
+
await client.call("login/token", [
|
|
471
|
+
0,
|
|
472
|
+
"no-such-token",
|
|
473
|
+
])
|
|
474
|
+
).error,
|
|
351
475
|
).toContain("令牌已失效");
|
|
352
476
|
expect(
|
|
353
|
-
(
|
|
477
|
+
(
|
|
478
|
+
await client.call("login/token", [
|
|
479
|
+
999,
|
|
480
|
+
"ghost-token",
|
|
481
|
+
])
|
|
482
|
+
).error,
|
|
354
483
|
).toContain("用户不存在");
|
|
355
484
|
client.close();
|
|
356
485
|
});
|
|
@@ -360,22 +489,39 @@ describe("@koishi-ce/plugin-auth", () => {
|
|
|
360
489
|
it("账户不存在或已绑定同一账户时拒绝", async () => {
|
|
361
490
|
const client = new TestClient(service());
|
|
362
491
|
expect(
|
|
363
|
-
(
|
|
492
|
+
(
|
|
493
|
+
await client.call("login/platform", [
|
|
494
|
+
"mock",
|
|
495
|
+
"404",
|
|
496
|
+
])
|
|
497
|
+
).error,
|
|
364
498
|
).toContain("找不到此账户");
|
|
365
499
|
|
|
366
500
|
client.client.auth = { id: aliceId } as Auth;
|
|
367
501
|
expect(
|
|
368
|
-
(
|
|
502
|
+
(
|
|
503
|
+
await client.call("login/platform", [
|
|
504
|
+
"mock",
|
|
505
|
+
"111",
|
|
506
|
+
])
|
|
507
|
+
).error,
|
|
369
508
|
).toContain("你已经绑定了此账户");
|
|
370
509
|
client.close();
|
|
371
510
|
});
|
|
372
511
|
|
|
373
512
|
it("未登录客户端经验证码完成登录", async () => {
|
|
374
513
|
const client = new TestClient(service());
|
|
375
|
-
const result = await client.call("login/platform", [
|
|
514
|
+
const result = await client.call("login/platform", [
|
|
515
|
+
"mock",
|
|
516
|
+
"111",
|
|
517
|
+
]);
|
|
376
518
|
expect(result.error).toBeUndefined();
|
|
377
|
-
expect(result.value).toMatchObject({
|
|
378
|
-
|
|
519
|
+
expect(result.value).toMatchObject({
|
|
520
|
+
id: aliceId,
|
|
521
|
+
name: "alice",
|
|
522
|
+
});
|
|
523
|
+
const code = (result.value as { token: string })
|
|
524
|
+
.token;
|
|
379
525
|
expect(code).toMatch(/^\d{6}$/);
|
|
380
526
|
|
|
381
527
|
// 无关消息不消费验证码状态
|
|
@@ -396,9 +542,13 @@ describe("@koishi-ce/plugin-auth", () => {
|
|
|
396
542
|
const client = new TestClient(service());
|
|
397
543
|
// 已登录为 alice,把 bob 的平台账号绑到 alice 名下
|
|
398
544
|
client.client.auth = { id: aliceId } as Auth;
|
|
399
|
-
const result = await client.call("login/platform", [
|
|
545
|
+
const result = await client.call("login/platform", [
|
|
546
|
+
"mock",
|
|
547
|
+
"222",
|
|
548
|
+
]);
|
|
400
549
|
expect(result.error).toBeUndefined();
|
|
401
|
-
const code = (result.value as { token: string })
|
|
550
|
+
const code = (result.value as { token: string })
|
|
551
|
+
.token;
|
|
402
552
|
|
|
403
553
|
const user = app.mock.client("222");
|
|
404
554
|
await user.receive(code);
|
|
@@ -417,10 +567,15 @@ describe("@koishi-ce/plugin-auth", () => {
|
|
|
417
567
|
const keep = app.auth.config.loginTokenExpire;
|
|
418
568
|
app.auth.config.loginTokenExpire = 50;
|
|
419
569
|
const client = new TestClient(service());
|
|
420
|
-
const result = await client.call("login/platform", [
|
|
570
|
+
const result = await client.call("login/platform", [
|
|
571
|
+
"mock",
|
|
572
|
+
"111",
|
|
573
|
+
]);
|
|
421
574
|
expect(result.error).toBeUndefined();
|
|
422
575
|
// 等待超时回调执行(状态清理与否取决于到期判定,此处验证回调不抛错)
|
|
423
|
-
await new Promise((resolve) =>
|
|
576
|
+
await new Promise((resolve) =>
|
|
577
|
+
setTimeout(resolve, 150),
|
|
578
|
+
);
|
|
424
579
|
client.close();
|
|
425
580
|
app.auth.config.loginTokenExpire = keep;
|
|
426
581
|
});
|
|
@@ -428,15 +583,19 @@ describe("@koishi-ce/plugin-auth", () => {
|
|
|
428
583
|
|
|
429
584
|
describe("权限拦截(console/intercept)", () => {
|
|
430
585
|
it("按登录态与权限等级拦截事件", async () => {
|
|
431
|
-
app.console.addListener(
|
|
432
|
-
|
|
433
|
-
|
|
586
|
+
app.console.addListener(
|
|
587
|
+
"test/admin-only",
|
|
588
|
+
() => "secret",
|
|
589
|
+
{
|
|
590
|
+
authority: 4,
|
|
591
|
+
},
|
|
592
|
+
);
|
|
434
593
|
|
|
435
594
|
// 未登录:拦截
|
|
436
595
|
const anonymous = new TestClient(service());
|
|
437
|
-
expect(
|
|
438
|
-
"
|
|
439
|
-
);
|
|
596
|
+
expect(
|
|
597
|
+
(await anonymous.call("test/admin-only", [])).error,
|
|
598
|
+
).toBe("unauthorized");
|
|
440
599
|
anonymous.close();
|
|
441
600
|
|
|
442
601
|
// 权限不足:拦截
|
|
@@ -447,9 +606,9 @@ describe("@koishi-ce/plugin-auth", () => {
|
|
|
447
606
|
expiredAt: Date.now() + Time.hour,
|
|
448
607
|
token: "t",
|
|
449
608
|
} as Auth;
|
|
450
|
-
expect(
|
|
451
|
-
"
|
|
452
|
-
);
|
|
609
|
+
expect(
|
|
610
|
+
(await low.call("test/admin-only", [])).error,
|
|
611
|
+
).toBe("unauthorized");
|
|
453
612
|
low.close();
|
|
454
613
|
|
|
455
614
|
// 令牌过期:拦截
|
|
@@ -460,14 +619,16 @@ describe("@koishi-ce/plugin-auth", () => {
|
|
|
460
619
|
expiredAt: Date.now() - 1,
|
|
461
620
|
token: "t",
|
|
462
621
|
} as Auth;
|
|
463
|
-
expect(
|
|
464
|
-
"
|
|
465
|
-
);
|
|
622
|
+
expect(
|
|
623
|
+
(await expired.call("test/admin-only", [])).error,
|
|
624
|
+
).toBe("unauthorized");
|
|
466
625
|
expired.close();
|
|
467
626
|
|
|
468
627
|
// 管理员:放行
|
|
469
628
|
const admin = await loginAdmin();
|
|
470
|
-
expect(
|
|
629
|
+
expect(
|
|
630
|
+
(await admin.call("test/admin-only", [])).value,
|
|
631
|
+
).toBe("secret");
|
|
471
632
|
admin.close();
|
|
472
633
|
});
|
|
473
634
|
});
|
|
@@ -475,21 +636,29 @@ describe("@koishi-ce/plugin-auth", () => {
|
|
|
475
636
|
describe("用户管理事件", () => {
|
|
476
637
|
it("user/delete-token 删除指定会话", async () => {
|
|
477
638
|
const anonymous = new TestClient(service());
|
|
478
|
-
expect(
|
|
479
|
-
"
|
|
480
|
-
|
|
639
|
+
expect(
|
|
640
|
+
(await anonymous.call("user/delete-token", [1]))
|
|
641
|
+
.error,
|
|
642
|
+
).toContain("请先登录");
|
|
481
643
|
anonymous.close();
|
|
482
644
|
|
|
483
645
|
const admin = await loginAdmin();
|
|
484
646
|
const [row] = await app.database.get("token", {
|
|
485
647
|
token: admin.client.auth!.token,
|
|
486
648
|
});
|
|
487
|
-
const bad = await admin.call(
|
|
649
|
+
const bad = await admin.call(
|
|
650
|
+
"user/delete-token",
|
|
651
|
+
[99999],
|
|
652
|
+
);
|
|
488
653
|
expect(bad.error).toContain("令牌不存在");
|
|
489
654
|
|
|
490
|
-
const ok = await admin.call("user/delete-token", [
|
|
655
|
+
const ok = await admin.call("user/delete-token", [
|
|
656
|
+
row?.inc,
|
|
657
|
+
]);
|
|
491
658
|
expect(ok.error).toBeUndefined();
|
|
492
|
-
const [removed] = await app.database.get("token", {
|
|
659
|
+
const [removed] = await app.database.get("token", {
|
|
660
|
+
inc: row!.inc,
|
|
661
|
+
});
|
|
493
662
|
expect(removed).toBeUndefined();
|
|
494
663
|
admin.close();
|
|
495
664
|
});
|
|
@@ -500,7 +669,9 @@ describe("@koishi-ce/plugin-auth", () => {
|
|
|
500
669
|
const response = await admin.call("user/logout", []);
|
|
501
670
|
expect(response.error).toBeUndefined();
|
|
502
671
|
// 令牌已被删除,其它设备无法再用它续期(loginAdmin 成功后令牌必存在)
|
|
503
|
-
const [row] = await app.database.get("token", {
|
|
672
|
+
const [row] = await app.database.get("token", {
|
|
673
|
+
token: token!,
|
|
674
|
+
});
|
|
504
675
|
expect(row).toBeUndefined();
|
|
505
676
|
// 移植偏差说明:上游 logout 以 setAuth(this, null) 显式清空登录态,
|
|
506
677
|
// 本仓改为传 undefined,命中 setAuth 的默认参数(沿用 client.auth),
|
|
@@ -522,20 +693,32 @@ describe("@koishi-ce/plugin-auth", () => {
|
|
|
522
693
|
it("user/update 修改资料(密码加哈希、配置透传)", async () => {
|
|
523
694
|
const anonymous = new TestClient(service());
|
|
524
695
|
expect(
|
|
525
|
-
(
|
|
696
|
+
(
|
|
697
|
+
await anonymous.call("user/update", [
|
|
698
|
+
{ name: "x" },
|
|
699
|
+
])
|
|
700
|
+
).error,
|
|
526
701
|
).toContain("请先登录");
|
|
527
702
|
anonymous.close();
|
|
528
703
|
|
|
529
704
|
const admin = await loginAdmin();
|
|
530
705
|
const response = await admin.call("user/update", [
|
|
531
|
-
{
|
|
706
|
+
{
|
|
707
|
+
name: "renamed",
|
|
708
|
+
password: "new-pass",
|
|
709
|
+
config: { theme: "dark" },
|
|
710
|
+
},
|
|
532
711
|
]);
|
|
533
712
|
expect(response.error).toBeUndefined();
|
|
534
713
|
// passive 更新:本地登录态同步但不推送数据
|
|
535
714
|
expect(admin.client.auth?.name).toBe("renamed");
|
|
536
|
-
const [row] = await app.database.get("user", {
|
|
715
|
+
const [row] = await app.database.get("user", {
|
|
716
|
+
id: 0,
|
|
717
|
+
});
|
|
537
718
|
expect(row?.name).toBe("renamed");
|
|
538
|
-
expect(row?.password?.startsWith("pbkdf2$")).toBe(
|
|
719
|
+
expect(row?.password?.startsWith("pbkdf2$")).toBe(
|
|
720
|
+
true,
|
|
721
|
+
);
|
|
539
722
|
expect(row?.config).toEqual({ theme: "dark" });
|
|
540
723
|
admin.close();
|
|
541
724
|
});
|
|
@@ -543,13 +726,21 @@ describe("@koishi-ce/plugin-auth", () => {
|
|
|
543
726
|
it("user/unbind 解绑平台账号的三种分支", async () => {
|
|
544
727
|
const anonymous = new TestClient(service());
|
|
545
728
|
expect(
|
|
546
|
-
(
|
|
729
|
+
(
|
|
730
|
+
await anonymous.call("user/unbind", [
|
|
731
|
+
"mock",
|
|
732
|
+
"111",
|
|
733
|
+
])
|
|
734
|
+
).error,
|
|
547
735
|
).toContain("请先登录");
|
|
548
736
|
anonymous.close();
|
|
549
737
|
|
|
550
738
|
// 以 alice 身份登录
|
|
551
739
|
const alice = new TestClient(service());
|
|
552
|
-
const grant = await alice.call("login/platform", [
|
|
740
|
+
const grant = await alice.call("login/platform", [
|
|
741
|
+
"mock",
|
|
742
|
+
"111",
|
|
743
|
+
]);
|
|
553
744
|
const code = (grant.value as { token: string }).token;
|
|
554
745
|
await app.mock.client("111").receive(code);
|
|
555
746
|
await tick();
|
|
@@ -557,12 +748,14 @@ describe("@koishi-ce/plugin-auth", () => {
|
|
|
557
748
|
|
|
558
749
|
// 绑定不存在
|
|
559
750
|
expect(
|
|
560
|
-
(await alice.call("user/unbind", ["mock", "000"]))
|
|
751
|
+
(await alice.call("user/unbind", ["mock", "000"]))
|
|
752
|
+
.error,
|
|
561
753
|
).toContain("绑定不存在");
|
|
562
754
|
|
|
563
755
|
// 仅剩一个自绑定(主账号):拒绝解绑
|
|
564
756
|
expect(
|
|
565
|
-
(await alice.call("user/unbind", ["mock", "111"]))
|
|
757
|
+
(await alice.call("user/unbind", ["mock", "111"]))
|
|
758
|
+
.error,
|
|
566
759
|
).toContain("无法解除绑定");
|
|
567
760
|
|
|
568
761
|
// 追加第二个自绑定后可解绑
|
|
@@ -573,7 +766,8 @@ describe("@koishi-ce/plugin-auth", () => {
|
|
|
573
766
|
pid: "333",
|
|
574
767
|
});
|
|
575
768
|
expect(
|
|
576
|
-
(await alice.call("user/unbind", ["mock", "333"]))
|
|
769
|
+
(await alice.call("user/unbind", ["mock", "333"]))
|
|
770
|
+
.error,
|
|
577
771
|
).toBeUndefined();
|
|
578
772
|
const [gone] = await app.database.get("binding", {
|
|
579
773
|
platform: "mock",
|
|
@@ -589,7 +783,8 @@ describe("@koishi-ce/plugin-auth", () => {
|
|
|
589
783
|
pid: "444",
|
|
590
784
|
});
|
|
591
785
|
expect(
|
|
592
|
-
(await alice.call("user/unbind", ["mock", "444"]))
|
|
786
|
+
(await alice.call("user/unbind", ["mock", "444"]))
|
|
787
|
+
.error,
|
|
593
788
|
).toBeUndefined();
|
|
594
789
|
const [moved] = await app.database.get("binding", {
|
|
595
790
|
platform: "mock",
|
package/src/index.ts
CHANGED
|
@@ -19,7 +19,10 @@ import {
|
|
|
19
19
|
timingSafeEqual,
|
|
20
20
|
} from "node:crypto";
|
|
21
21
|
import { resolve } from "node:path";
|
|
22
|
-
import type {
|
|
22
|
+
import type {
|
|
23
|
+
Client,
|
|
24
|
+
DataService,
|
|
25
|
+
} from "@koishi-ce/console";
|
|
23
26
|
import {
|
|
24
27
|
type Binding,
|
|
25
28
|
type Context,
|
|
@@ -66,10 +69,22 @@ declare module "@koishi-ce/console" {
|
|
|
66
69
|
platform: string,
|
|
67
70
|
pid: string,
|
|
68
71
|
): Promise<UserLogin>;
|
|
69
|
-
"login/password"(
|
|
70
|
-
|
|
72
|
+
"login/password"(
|
|
73
|
+
this: Client,
|
|
74
|
+
name: string,
|
|
75
|
+
password: string,
|
|
76
|
+
): void;
|
|
77
|
+
"login/token"(
|
|
78
|
+
this: Client,
|
|
79
|
+
id: number,
|
|
80
|
+
token: string,
|
|
81
|
+
): void;
|
|
71
82
|
"user/delete-token"(this: Client, inc: number): void;
|
|
72
|
-
"user/unbind"(
|
|
83
|
+
"user/unbind"(
|
|
84
|
+
this: Client,
|
|
85
|
+
platform: string,
|
|
86
|
+
pid: string,
|
|
87
|
+
): void;
|
|
73
88
|
"user/update"(this: Client, data: UserUpdate): void;
|
|
74
89
|
"user/logout"(this: Client): void;
|
|
75
90
|
}
|
|
@@ -120,18 +135,24 @@ const letters =
|
|
|
120
135
|
export function randomId(length = 40) {
|
|
121
136
|
return Array(length)
|
|
122
137
|
.fill(0)
|
|
123
|
-
.map(
|
|
138
|
+
.map(
|
|
139
|
+
() =>
|
|
140
|
+
letters[Math.floor(Math.random() * letters.length)],
|
|
141
|
+
)
|
|
124
142
|
.join("");
|
|
125
143
|
}
|
|
126
144
|
|
|
127
145
|
/** login/platform 事件的返回值:待登录用户信息 + 一次性验证码及其过期时间。 */
|
|
128
|
-
export interface UserLogin
|
|
146
|
+
export interface UserLogin
|
|
147
|
+
extends Pick<User, "id" | "name"> {
|
|
129
148
|
token: string;
|
|
130
149
|
expiredAt: number;
|
|
131
150
|
}
|
|
132
151
|
|
|
133
152
|
/** user/update 事件允许修改的用户字段。 */
|
|
134
|
-
export type UserUpdate = Partial<
|
|
153
|
+
export type UserUpdate = Partial<
|
|
154
|
+
Pick<User, "name" | "password" | "config">
|
|
155
|
+
>;
|
|
135
156
|
|
|
136
157
|
/** PBKDF2-HMAC-SHA256 迭代次数(OWASP 2023 建议 600k;登录低频,开销可接受) */
|
|
137
158
|
const PBKDF2_ROUNDS = 600_000;
|
|
@@ -139,7 +160,13 @@ const PBKDF2_ROUNDS = 600_000;
|
|
|
139
160
|
/** 新格式密码哈希:`pbkdf2$<rounds>$<salt-hex>$<dk-hex>`(加盐 + 慢哈希)。 */
|
|
140
161
|
function toHash(password: string) {
|
|
141
162
|
const salt = randomBytes(16);
|
|
142
|
-
const dk = pbkdf2Sync(
|
|
163
|
+
const dk = pbkdf2Sync(
|
|
164
|
+
password,
|
|
165
|
+
salt,
|
|
166
|
+
PBKDF2_ROUNDS,
|
|
167
|
+
32,
|
|
168
|
+
"sha256",
|
|
169
|
+
);
|
|
143
170
|
return `pbkdf2$${PBKDF2_ROUNDS}$${salt.toString("hex")}$${dk.toString("hex")}`;
|
|
144
171
|
}
|
|
145
172
|
|
|
@@ -151,7 +178,10 @@ function toHash(password: string) {
|
|
|
151
178
|
* - 64 位十六进制旧格式:历史上无盐 SHA-256,仅用于校验(命中后由调用方
|
|
152
179
|
* 透明升级为 PBKDF2),同样以恒定时间比较。
|
|
153
180
|
*/
|
|
154
|
-
function verifyPassword(
|
|
181
|
+
function verifyPassword(
|
|
182
|
+
password: string,
|
|
183
|
+
stored: string,
|
|
184
|
+
): boolean {
|
|
155
185
|
if (stored.startsWith("pbkdf2$")) {
|
|
156
186
|
const [, rounds, saltHex, dkHex] = stored.split("$");
|
|
157
187
|
if (!rounds || !saltHex || !dkHex) return false;
|
|
@@ -171,8 +201,13 @@ function verifyPassword(password: string, stored: string): boolean {
|
|
|
171
201
|
// 命中后由调用方透明升级为 PBKDF2;新建密码一律走 toHash(pbkdf2$ 格式),
|
|
172
202
|
// 故此处非弱哈希存储,属误报(Default setup 不支持注释抑制,须在平台 dismiss)。
|
|
173
203
|
if (!/^[0-9a-f]{64}$/i.test(stored)) return false;
|
|
174
|
-
const actual = createHash("sha256")
|
|
175
|
-
|
|
204
|
+
const actual = createHash("sha256")
|
|
205
|
+
.update(password)
|
|
206
|
+
.digest();
|
|
207
|
+
return timingSafeEqual(
|
|
208
|
+
actual,
|
|
209
|
+
Buffer.from(stored, "hex"),
|
|
210
|
+
);
|
|
176
211
|
}
|
|
177
212
|
|
|
178
213
|
/**
|
|
@@ -189,37 +224,41 @@ class AuthService extends Service {
|
|
|
189
224
|
// 的导出值迁为静态成员(合并到 class 的 namespace 本就编译为静态属性,运行时等价)
|
|
190
225
|
static filter = false;
|
|
191
226
|
|
|
192
|
-
static Admin: Schema<AuthService.Admin> =
|
|
193
|
-
Schema.
|
|
194
|
-
enabled: Schema.boolean().default(true),
|
|
195
|
-
}),
|
|
196
|
-
Schema.union([
|
|
227
|
+
static Admin: Schema<AuthService.Admin> =
|
|
228
|
+
Schema.intersect([
|
|
197
229
|
Schema.object({
|
|
198
|
-
enabled: Schema.
|
|
199
|
-
username: Schema.string().default("admin"),
|
|
200
|
-
password: Schema.string().role("secret").required(),
|
|
230
|
+
enabled: Schema.boolean().default(true),
|
|
201
231
|
}),
|
|
202
|
-
Schema.
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
.
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
232
|
+
Schema.union([
|
|
233
|
+
Schema.object({
|
|
234
|
+
enabled: Schema.const(true),
|
|
235
|
+
username: Schema.string().default("admin"),
|
|
236
|
+
password: Schema.string()
|
|
237
|
+
.role("secret")
|
|
238
|
+
.required(),
|
|
239
|
+
}),
|
|
240
|
+
Schema.object({}),
|
|
241
|
+
]),
|
|
242
|
+
]);
|
|
243
|
+
|
|
244
|
+
static Config: Schema<AuthService.Config> =
|
|
245
|
+
Schema.intersect([
|
|
246
|
+
Schema.object({
|
|
247
|
+
admin: AuthService.Admin,
|
|
248
|
+
}),
|
|
249
|
+
Schema.object({
|
|
250
|
+
authTokenExpire: Schema.natural()
|
|
251
|
+
.role("ms")
|
|
252
|
+
.default(Time.week)
|
|
253
|
+
.min(Time.hour),
|
|
254
|
+
loginTokenExpire: Schema.natural()
|
|
255
|
+
.role("ms")
|
|
256
|
+
.default(Time.minute * 10)
|
|
257
|
+
.min(Time.minute),
|
|
258
|
+
}),
|
|
259
|
+
]).i18n({
|
|
260
|
+
"zh-CN": zhCN,
|
|
261
|
+
});
|
|
223
262
|
|
|
224
263
|
// Service 基类已声明 config(T = any),此处覆盖为插件配置类型
|
|
225
264
|
override config: AuthService.Config;
|
|
@@ -267,7 +306,8 @@ class AuthService extends Service {
|
|
|
267
306
|
|
|
268
307
|
/** 启动时按配置确保管理员账户(id = 0)存在。 */
|
|
269
308
|
override async start() {
|
|
270
|
-
const { enabled, username, password } =
|
|
309
|
+
const { enabled, username, password } =
|
|
310
|
+
this.config.admin;
|
|
271
311
|
if (!enabled) return;
|
|
272
312
|
this.ctx.logger.info("creating admin account");
|
|
273
313
|
// enabled 分支的 Schema 已保证 username/password 存在(默认 admin / required),
|
|
@@ -291,24 +331,42 @@ class AuthService extends Service {
|
|
|
291
331
|
* @param passive 为 true 时只写 client.auth,不推送数据也不刷新
|
|
292
332
|
* (已登录状态下修改资料后仅同步本地时使用)
|
|
293
333
|
*/
|
|
294
|
-
async setAuth(
|
|
334
|
+
async setAuth(
|
|
335
|
+
client: Client,
|
|
336
|
+
auth = client.auth,
|
|
337
|
+
passive = false,
|
|
338
|
+
) {
|
|
295
339
|
client.auth = auth;
|
|
296
340
|
if (passive) return;
|
|
297
341
|
if (auth) {
|
|
298
|
-
const bindings = await this.ctx.database.get(
|
|
342
|
+
const bindings = await this.ctx.database.get(
|
|
343
|
+
"binding",
|
|
344
|
+
{ aid: auth.id },
|
|
345
|
+
);
|
|
299
346
|
// 下发前剥离服务端字段;minato 模型字段必填,需按可删除的形状断言
|
|
300
|
-
bindings.forEach(
|
|
301
|
-
|
|
347
|
+
bindings.forEach(
|
|
348
|
+
(binding) =>
|
|
349
|
+
delete (binding as Partial<Binding>).aid,
|
|
350
|
+
);
|
|
351
|
+
const tokens = await this.ctx.database.get("token", {
|
|
352
|
+
id: auth.id,
|
|
353
|
+
});
|
|
302
354
|
tokens.reverse().forEach((login) => {
|
|
303
355
|
delete (login as Partial<LoginToken>).id;
|
|
304
356
|
delete (login as Partial<LoginToken>).token;
|
|
305
357
|
});
|
|
306
358
|
client.send({
|
|
307
359
|
type: "data",
|
|
308
|
-
body: {
|
|
360
|
+
body: {
|
|
361
|
+
key: "user",
|
|
362
|
+
value: { ...auth, bindings, tokens },
|
|
363
|
+
},
|
|
309
364
|
});
|
|
310
365
|
} else {
|
|
311
|
-
client.send({
|
|
366
|
+
client.send({
|
|
367
|
+
type: "data",
|
|
368
|
+
body: { key: "user", value: null },
|
|
369
|
+
});
|
|
312
370
|
}
|
|
313
371
|
client.ctx.emit("console/connection", client);
|
|
314
372
|
client.refresh();
|
|
@@ -322,7 +380,10 @@ class AuthService extends Service {
|
|
|
322
380
|
async createToken(
|
|
323
381
|
client: Client,
|
|
324
382
|
type: LoginType,
|
|
325
|
-
user: Pick<
|
|
383
|
+
user: Pick<
|
|
384
|
+
User,
|
|
385
|
+
"id" | "name" | "authority" | "config"
|
|
386
|
+
>,
|
|
326
387
|
) {
|
|
327
388
|
// WebSocket 升级连接必带 HTTP 请求对象(见 console 服务端构造 Client 处)
|
|
328
389
|
const { headers, socket } = client.request;
|
|
@@ -330,8 +391,10 @@ class AuthService extends Service {
|
|
|
330
391
|
const lastUsedAt = new Date();
|
|
331
392
|
const userAgent = headers["user-agent"]?.toString();
|
|
332
393
|
const address =
|
|
333
|
-
headers["x-forwarded-for"]?.toString() ||
|
|
334
|
-
|
|
394
|
+
headers["x-forwarded-for"]?.toString() ||
|
|
395
|
+
socket.remoteAddress;
|
|
396
|
+
const expiredAt =
|
|
397
|
+
Date.now() + this.config.authTokenExpire;
|
|
335
398
|
const token = randomId();
|
|
336
399
|
// 请求头字段可能缺失,undefined 由 minato 归一化为 NULL 落库,不做默认值替换
|
|
337
400
|
await this.ctx.database.create("token", {
|
|
@@ -344,7 +407,11 @@ class AuthService extends Service {
|
|
|
344
407
|
userAgent: userAgent as string,
|
|
345
408
|
address: address as string,
|
|
346
409
|
});
|
|
347
|
-
await this.setAuth(client, {
|
|
410
|
+
await this.setAuth(client, {
|
|
411
|
+
...user,
|
|
412
|
+
expiredAt,
|
|
413
|
+
token,
|
|
414
|
+
});
|
|
348
415
|
}
|
|
349
416
|
|
|
350
417
|
/** 注册全部登录 / 用户管理事件与权限拦截逻辑(构造时调用)。 */
|
|
@@ -353,46 +420,67 @@ class AuthService extends Service {
|
|
|
353
420
|
const { ctx, config } = this;
|
|
354
421
|
// 平台验证码登录的进行中状态:键为 `${platform}:${userId}`,
|
|
355
422
|
// 值为 [验证码, 过期时间, 发起登录的客户端]
|
|
356
|
-
const states: Record<string, [string, number, Client]> =
|
|
423
|
+
const states: Record<string, [string, number, Client]> =
|
|
424
|
+
{};
|
|
357
425
|
|
|
358
426
|
// 用户密码登录:校验通过后签发新令牌;
|
|
359
427
|
// 命中旧的无盐 SHA-256 存储时透明升级为 PBKDF2
|
|
360
|
-
ctx.console.addListener(
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
428
|
+
ctx.console.addListener(
|
|
429
|
+
"login/password",
|
|
430
|
+
async function (name, password) {
|
|
431
|
+
const [user] = await ctx.database.get(
|
|
432
|
+
"user",
|
|
433
|
+
{ name },
|
|
434
|
+
["password", "name", "id", "authority", "config"],
|
|
435
|
+
);
|
|
436
|
+
if (
|
|
437
|
+
!user?.password ||
|
|
438
|
+
!verifyPassword(password, user.password)
|
|
439
|
+
)
|
|
440
|
+
throw new Error("用户名或密码错误。");
|
|
441
|
+
if (!user.password.startsWith("pbkdf2$")) {
|
|
442
|
+
await ctx.database.set("user", user.id, {
|
|
443
|
+
password: toHash(password),
|
|
444
|
+
});
|
|
445
|
+
}
|
|
446
|
+
await self.createToken(
|
|
447
|
+
this,
|
|
448
|
+
"password",
|
|
449
|
+
omit(user, ["password"]),
|
|
450
|
+
);
|
|
451
|
+
},
|
|
452
|
+
);
|
|
377
453
|
|
|
378
454
|
// 已存令牌续期登录:本地记录的令牌未过期即恢复登录态,
|
|
379
455
|
// 同时刷新该令牌的最后访问时间
|
|
380
|
-
ctx.console.addListener(
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
456
|
+
ctx.console.addListener(
|
|
457
|
+
"login/token",
|
|
458
|
+
async function (aid, token) {
|
|
459
|
+
const [data] = await ctx.database.get(
|
|
460
|
+
"token",
|
|
461
|
+
{ id: aid, token },
|
|
462
|
+
["expiredAt"],
|
|
463
|
+
);
|
|
464
|
+
if (!data || data.expiredAt <= Date.now())
|
|
465
|
+
throw new Error("令牌已失效。");
|
|
466
|
+
const [user] = await ctx.database.get(
|
|
467
|
+
"user",
|
|
468
|
+
{ id: aid },
|
|
469
|
+
["id", "name", "authority", "config"],
|
|
470
|
+
);
|
|
471
|
+
if (!user) throw new Error("用户不存在。");
|
|
472
|
+
await ctx.database.set(
|
|
473
|
+
"token",
|
|
474
|
+
{ token },
|
|
475
|
+
{ lastUsedAt: new Date() },
|
|
476
|
+
);
|
|
477
|
+
await self.setAuth(this, {
|
|
478
|
+
...user,
|
|
479
|
+
...data,
|
|
480
|
+
token,
|
|
481
|
+
});
|
|
482
|
+
},
|
|
483
|
+
);
|
|
396
484
|
|
|
397
485
|
// 平台账户登录(第一步):校验平台账号存在后生成一次性验证码,
|
|
398
486
|
// 用户把验证码发给任意机器人即可完成登录/绑定(见下方中间件)。
|
|
@@ -400,16 +488,19 @@ class AuthService extends Service {
|
|
|
400
488
|
ctx.console.addListener(
|
|
401
489
|
"login/platform",
|
|
402
490
|
async function (platform, userId) {
|
|
403
|
-
const user = await ctx.database.getUser(
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
491
|
+
const user = await ctx.database.getUser(
|
|
492
|
+
platform,
|
|
493
|
+
userId,
|
|
494
|
+
["id", "name"],
|
|
495
|
+
);
|
|
407
496
|
if (!user) throw new Error("找不到此账户。");
|
|
408
|
-
if (this.auth?.id === user.id)
|
|
497
|
+
if (this.auth?.id === user.id)
|
|
498
|
+
throw new Error("你已经绑定了此账户。");
|
|
409
499
|
|
|
410
500
|
const key = `${platform}:${userId}`;
|
|
411
501
|
const token = Math.random().toString().slice(2, 8);
|
|
412
|
-
const expiredAt =
|
|
502
|
+
const expiredAt =
|
|
503
|
+
Date.now() + config.loginTokenExpire;
|
|
413
504
|
states[key] = [token, expiredAt, this];
|
|
414
505
|
|
|
415
506
|
// 客户端断开或验证码超时即作废本次登录状态
|
|
@@ -424,7 +515,12 @@ class AuthService extends Service {
|
|
|
424
515
|
}, config.loginTokenExpire);
|
|
425
516
|
this.socket.addEventListener("close", listener);
|
|
426
517
|
|
|
427
|
-
return {
|
|
518
|
+
return {
|
|
519
|
+
id: user.id,
|
|
520
|
+
name: user.name,
|
|
521
|
+
token,
|
|
522
|
+
expiredAt,
|
|
523
|
+
};
|
|
428
524
|
},
|
|
429
525
|
);
|
|
430
526
|
|
|
@@ -459,65 +555,99 @@ class AuthService extends Service {
|
|
|
459
555
|
|
|
460
556
|
// 拦截带 authority 要求的 console 事件:未登录、令牌过期或
|
|
461
557
|
// 权限不足时拒绝(返回 true 表示拦截)
|
|
462
|
-
ctx.on(
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
558
|
+
ctx.on(
|
|
559
|
+
"console/intercept",
|
|
560
|
+
async (client, listener) => {
|
|
561
|
+
if (!listener.authority) return false;
|
|
562
|
+
if (!client.auth) return true;
|
|
563
|
+
if (client.auth.expiredAt <= Date.now())
|
|
564
|
+
return true;
|
|
565
|
+
if (client.auth.authority < listener.authority)
|
|
566
|
+
return true;
|
|
567
|
+
return false;
|
|
568
|
+
},
|
|
569
|
+
);
|
|
469
570
|
|
|
470
571
|
// 删除指定登录会话(登出其它设备)
|
|
471
|
-
ctx.console.addListener(
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
572
|
+
ctx.console.addListener(
|
|
573
|
+
"user/delete-token",
|
|
574
|
+
async function (inc) {
|
|
575
|
+
if (!this.auth) throw new Error("请先登录。");
|
|
576
|
+
const [data] = await ctx.database.get("token", {
|
|
577
|
+
id: this.auth.id,
|
|
578
|
+
inc,
|
|
579
|
+
});
|
|
580
|
+
if (!data) throw new Error("令牌不存在。");
|
|
581
|
+
await ctx.database.remove("token", { inc });
|
|
582
|
+
await self.setAuth(this);
|
|
583
|
+
},
|
|
584
|
+
);
|
|
478
585
|
|
|
479
586
|
// 退出登录:删除当前令牌并清除登录态
|
|
480
|
-
ctx.console.addListener(
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
587
|
+
ctx.console.addListener(
|
|
588
|
+
"user/logout",
|
|
589
|
+
async function () {
|
|
590
|
+
if (this.auth) {
|
|
591
|
+
await ctx.database.remove("token", {
|
|
592
|
+
token: this.auth.token,
|
|
593
|
+
});
|
|
594
|
+
}
|
|
595
|
+
await self.setAuth(this, undefined);
|
|
596
|
+
},
|
|
597
|
+
);
|
|
486
598
|
|
|
487
599
|
// 修改用户资料(用户名 / 密码 / 配置),密码先哈希再落库
|
|
488
|
-
ctx.console.addListener(
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
600
|
+
ctx.console.addListener(
|
|
601
|
+
"user/update",
|
|
602
|
+
async function (data) {
|
|
603
|
+
if (!this.auth) throw new Error("请先登录。");
|
|
604
|
+
if (data.password)
|
|
605
|
+
data.password = toHash(data.password);
|
|
606
|
+
await ctx.database.set(
|
|
607
|
+
"user",
|
|
608
|
+
{ id: this.auth.id },
|
|
609
|
+
data,
|
|
610
|
+
);
|
|
611
|
+
Object.assign(this.auth, data);
|
|
612
|
+
await self.setAuth(this, undefined, true);
|
|
613
|
+
},
|
|
614
|
+
);
|
|
495
615
|
|
|
496
616
|
// 解绑平台账号:绑到别的用户时改指回其主账号;是自身主账号且
|
|
497
617
|
// 仅剩一个自绑定时拒绝解绑(避免用户失去登录途径),否则删除记录
|
|
498
|
-
ctx.console.addListener(
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
{ platform, pid },
|
|
510
|
-
{ aid: binding.bid },
|
|
618
|
+
ctx.console.addListener(
|
|
619
|
+
"user/unbind",
|
|
620
|
+
async function (platform, pid) {
|
|
621
|
+
if (!this.auth) throw new Error("请先登录。");
|
|
622
|
+
const bindings = await ctx.database.get("binding", {
|
|
623
|
+
aid: this.auth.id,
|
|
624
|
+
});
|
|
625
|
+
// 客户端仅对已列出的绑定发起解绑,查找必命中,未命中视为异常状态
|
|
626
|
+
const binding = bindings.find(
|
|
627
|
+
(item) =>
|
|
628
|
+
item.platform === platform && item.pid === pid,
|
|
511
629
|
);
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
630
|
+
if (!binding) throw new Error("绑定不存在。");
|
|
631
|
+
if (binding.aid !== binding.bid) {
|
|
632
|
+
await ctx.database.set(
|
|
633
|
+
"binding",
|
|
634
|
+
{ platform, pid },
|
|
635
|
+
{ aid: binding.bid },
|
|
636
|
+
);
|
|
637
|
+
} else if (
|
|
638
|
+
bindings.filter((item) => item.aid === item.bid)
|
|
639
|
+
.length === 1
|
|
640
|
+
) {
|
|
641
|
+
throw new Error("无法解除绑定。");
|
|
642
|
+
} else {
|
|
643
|
+
await ctx.database.remove("binding", {
|
|
644
|
+
platform,
|
|
645
|
+
pid,
|
|
646
|
+
});
|
|
647
|
+
}
|
|
648
|
+
await self.setAuth(this);
|
|
649
|
+
},
|
|
650
|
+
);
|
|
521
651
|
}
|
|
522
652
|
}
|
|
523
653
|
|