diff --git a/components/supadb/aksupa.uts b/components/supadb/aksupa.uts index 2a318b89..eb558e71 100644 --- a/components/supadb/aksupa.uts +++ b/components/supadb/aksupa.uts @@ -658,6 +658,73 @@ export class AkSupa { } } + /** + * 模拟 supabase-js 的 auth 属性,提供认证相关方法 + */ + get auth() : AkSupa { + return this; + } + + /** + * 校验密码或登录(别名,兼容 supabase-js 命名) + */ + async signInWithPassword(credentials : UTSJSONObject) : Promise { + const email = credentials.getString('email'); + const password = credentials.getString('password'); + if (email == null || password == null) { + throw new Error('Email and password are required'); + } + return await this.signIn(email, password); + } + + /** + * 更新用户信息(如修改密码、修改元数据等) + * 对应 Supabase Auth API: PUT /auth/v1/user (部分版本或Kong配置可能只支持PUT) + */ + async updateUser(attributes : UTSJSONObject) : Promise> { + const url = this.baseUrl + '/auth/v1/user'; + const token = AkReq.getToken(); + if (token == null || token == '') { + throw new Error('未登录,无法更新用户信息'); + } + + const headers = { + apikey: this.apikey, + 'Content-Type': 'application/json', + Authorization: `Bearer ${token}` + } as UTSJSONObject; + + // 尝试先用 PUT 方法,因为部分环境 PATCH 报 405 Method Not Allowed + const reqOptions : AkReqOptions = { + url, + method: 'PUT', + headers, + data: attributes, + contentType: 'application/json' + }; + + // updateUser 后,Supabase 会返回更新后的用户对象 + let res = await AkReq.request(reqOptions, false); + + // 如果 PUT 也是 405,则尝试 PATCH + if (res.status == 405) { + reqOptions.method = 'PATCH'; + res = await AkReq.request(reqOptions, false); + } + + if (res.status >= 200 && res.status < 300 && res.data != null) { + // 如果返回了新的 user 对象,更新本地缓存 + try { + const newUser = new UTSJSONObject(res.data); + this.user = newUser; + if (this.session != null) { + this.session!!.user = newUser; + } + } catch (e) {} + } + return res; + } + // [CHANGE][2026-01-30] hydrate user from /auth/v1/user when token exists in storage async hydrateSessionFromStorage() : Promise { try { @@ -1052,7 +1119,7 @@ async delete(table : string, filter : string | null) : Promise authUser.email -> authUser.phone -> truncate(id)。 +效果:UI 的渲染此时有了唯一根据。但一旦按下 F5 刷新依然会闪烁错位,并且跨 Tab 的数据霸权问题未解。 +第三阶段:修页面刷新时的阻断与身份恢复时序 +动作:为彻底消灭渲染抢跑和左侧菜单的“一帧残影”,我们在 AdminLayout.uvue 强力引入了阻断变量 isAuthReady = ref(false)。 +实现:单例 aksupainstance.uts 导出 export const supaReady = supaInstance.hydrateSessionFromStorage() 这个关键的顶层 await。只有当它从磁盘恢复甚至向后端校验完凭证后,isAuthReady 才会变成 True 放行子组件(包含菜单组件)挂载。 +效果:页面变得顺滑不再闪动,但核心发现爆出——A 的数据被验证仍然读取到了缓存池中 B 的身份。 +第四阶段:修多 Tab 会话隔离 (本役关键转折点) +动作:剖开底层基石,拦截 ak-req.uts 所有对 Token 的操作,使用条件编译为 H5 环境单独开辟特区。 +实现: +效果:Token 不再流入跨 Tab 共享的 localStorage,而是被封存入完全按浏览器标签页物理隔离的 sessionStorage!至此,“连坐”与“身份覆盖”奇迹般停止了,基础认证的主链路完成了隔离闭环。 +第五阶段:清理共享 identity 旁路 +动作:利用正则在全局排查出所有 getStorageSync('user_id')、merchant_id 的老旧黑魔法并肃清。 +执行细节: +删除 login.uvue 里私自存放 user_id 的操作。 +修复 edit.uvue 中原有的越权隐患代码: +const mId = uni.getStorageSync('merchant_id') +--> 坚决替换为:const mId = supa.getSession().user?.id +删除 aksupa.uts 中退出登录时清理全局变量的指令,保护了其他 Tab 的安危。 +效果:彻底拔除了所有潜伏在业务深处的侧漏点。user_id/merchant_id 现在只通过当前 Tab 的运行时计算得来。5. 最终闭环逻辑是什么 +此时整套前端系统的状态机和请求流向,已凝结为绝对干净的单向闭环。 + +【最终身份闭环链路图】 + +因果解答: + +为什么始终是同一个账号? 因为 View 层的渲染全盘接驳到了 store.uts 这个单例枢纽。绝不会有组件私自“偷吃”外部缓存。 +为什么刷新不串号? Tab 刷新加载的第一秒,框架在 sessionStorage 摸到的全是属于自己这个窗口原教旨级的鉴权 Token,从而去 Supabase 换来了自身的资料。它哪怕想认识别的 Tab 也跨越不来。 +为什么 A 退出不影响 B? 移除底部的黑魔法连坐缓存后,A 退出仅仅清空其拥有的 sessionStorage 和当前内存;B 安然无恙。6. 为什么这种方式能够实现真正闭环 +从宏观建构看,这次并不是用“if/else”补坑或者临时写一套脚本洗数据造成的“表面修复”。它的彻底生效依赖四大架构调整: + +共享状态源的强制切断机制(持久化隔离):这不单是将 localStorage 改为 sessionStorage,而是从最底盘网络客户端驱动发起底层劫持(条件编译拦截),确保了跨平台包容性不折损的同时完成了 H5 下环境级别的强制网闸。 +重塑出唯一真实数据源(SSOT - Single Source Of Truth):剥夺了之前 role.uts 中那些诸如 admin_role 等眼花缭乱却错误百出的缓存优先读取权利,改由 user_metadata 与远程 ak_users 库做绝对断言。从根本收缩了多数据源带来的状态裂变。 +引入 UI 的防抢跑闸门(同步阻断设计):通过 isAuthReady 这一全局屏障的引入,我们将应用初始化时异步加载造成的并发渲染冲突消灭。这类似于后端的资源自检网关(Health Check)。 +抹除缓存旁路的零容忍合规化(业务重耦合):清查了业务内残存直接对接 Storage 获取凭据的调用(如商品 edit.uvue)。强制业务层取身份只能向上游状态池发出“请示”,而非自己瞎去找“私房钱”;这是在业务流闭环。7. 最终验证结果与结论 +严格重走原始所有异常验证用例: + +并行共存: Tab A 在 admin 权限全开操作,Tab B 在 merchant 权限受限下操作。各自跑通。 +单独或交叉强刷测试: 狂按 F5。A 的屏幕显示阻断遮罩 -> 解锁 -> 依然是完美的 Admin 环境;B 屏幕显示遮罩 -> 解锁 -> 依然是严谨的 Merchant 环境。无“串号重置”现象,无“未知用户”闪烁,无菜单“退化”现象。 +退登独立测试: 在 A 主动注销,系统清理当前 Tab 无任何抛错跳转回页;切回 B,点任何需要验证的请求,依然成功拉取属于 B 的数据。无“拉断电闸式连坐”。 +唯余一项边界规范注意: +现代浏览器存在“复制当前标签页(Duplicate Tab)”功能。使用该原生的操作会附带将原封不动的 sessionStorage 克隆给新的克隆 Tab。这是浏览器的底层特性,也是合乎常理的双开机制(相当于镜像);普通“新打开一个空白标签访问系统”,将仍是未经登录的纯净环境,不会继承。 + +结论: 跨 Tab 身份冲突缺陷已经从根源和机制两端收口。 + +8. 最终总结 + 回顾本次排查修复之旅,它极具警示意义:前端多重状态混乱与生命周期视觉闪烁,往往不是在组件内调调判断条件或给几个 timeout 能绕过去的,而是“设计架构中的状态流转通道出现了裂隙”。 + +问题本质:是系统缺少对环境隔离上下文(Web Context Scope)与 Vue 内存生命周期一致性的尊重。任由各个模块向大池子(localStorage)不负责任地伸手索取。 +机制收益:通过一套稳固的:“物理层面按 Tab 剥离” + “生命周期按重获取阻断” + “业务使用按单一中枢获取” + “清理法外旁路缓存” 组合拳,使得前端对登录信息的掌控进入了真正的自治安全期。 +长效架构思考:这套解法相比只在界面写个逻辑来挡一挡或者仅仅修复由于 undefined 导致的文案显示,它的层级更加高维。未来我们无论新增多少种中等角色抑或新增几十个需要拉取 user_id 的特殊业务,只要严格依从这条收拢唯一化的高速链路去调用接口去获取 Token/ID,在底层框架机制不变的前提下,类似数据倒流与覆盖的噩梦将再无将在工程中宣告绝迹。 diff --git a/pages/mall/admin/userCenter/index.uvue b/pages/mall/admin/userCenter/index.uvue index 27a9bb52..108c968e 100644 --- a/pages/mall/admin/userCenter/index.uvue +++ b/pages/mall/admin/userCenter/index.uvue @@ -56,9 +56,11 @@