diff --git a/fix_login.py b/fix_login.py new file mode 100644 index 00000000..43be537b --- /dev/null +++ b/fix_login.py @@ -0,0 +1,36 @@ +import codecs +import re + +path = r'd:\骅锋\mall\pages\user\login.uvue' +with codecs.open(path, 'r', 'utf-8') as f: + text = f.read() + +# Define the old block using regex to handle variable whitespace/tabs +old_pattern = r"const merchantId = await checkMerchantAccess\(sessionUid,\s*account\.value\)\s*if\s*\(merchantId == null\)\s*\{\s*await supa\.signOut\(\)\s*logout\(\)\s*throw new Error\('您还没有注册商家端账户,快去注册一个'\)\s*\}\s*// 存入商家ID\s*uni\.setStorageSync\('merchant_id', merchantId\)" + +new_code = '''const accessData = await checkAdminOrMerchantAccess(sessionUid, account.value) + if (accessData == null) { + await supa.signOut() + logout() + throw new Error('该账户无后台或商家端权限') + } + + const currRole = accessData.getString('role') + const currId = accessData.getString('id') + uni.setStorageSync('adminRole', currRole) + + if (currRole === 'merchant') { + uni.setStorageSync('merchant_id', currId) + } else { + uni.removeStorageSync('merchant_id') + }''' + +# Replace it +if 'checkMerchantAccess' in text: + new_text = re.sub(old_pattern, new_code, text) + with codecs.open(path, 'w', 'utf-8') as f: + f.write(new_text) + print("Fixed checkMerchantAccess reference.") +else: + print("Not found.") + diff --git a/layouts/admin/AdminLayout.uvue b/layouts/admin/AdminLayout.uvue index 98c7fb55..a48a0313 100644 --- a/layouts/admin/AdminLayout.uvue +++ b/layouts/admin/AdminLayout.uvue @@ -116,7 +116,7 @@ import { import type { TabItem } from '@/layouts/admin/store/adminNavStore.uts' import { getComponent } from '@/layouts/admin/router/adminComponentMap.uts' -import { hasAdminModuleAccess } from '@/layouts/admin/utils/role.uts' +import { hasAdminModuleAccess, refreshAdminRole } from '@/layouts/admin/utils/role.uts' const props = defineProps({ currentPage: { @@ -389,7 +389,8 @@ function onNotify(): void { let resizeTid: any = null -onMounted(() => { +onMounted(async () => { + await refreshAdminRole() initNavState() if (props.currentPage != '') { openRoute(props.currentPage as string) diff --git a/layouts/admin/components/AdminHeader.uvue b/layouts/admin/components/AdminHeader.uvue index 2f72dad8..3a2748eb 100644 --- a/layouts/admin/components/AdminHeader.uvue +++ b/layouts/admin/components/AdminHeader.uvue @@ -62,6 +62,7 @@ import { openRoute } from '@/layouts/admin/store/adminNavStore.uts' import { state, logout } from '@/utils/store.uts' +import { clearAdminRoleCache } from '@/layouts/admin/utils/role.uts' const showUserMenu = ref(false) const userName = computed((): string => state.userProfile.username || state.userProfile.email || 'admin') @@ -116,7 +117,7 @@ function handleLogout(e: any) { success: (res) => { if (res.confirm) { logout() - uni.removeStorageSync('adminRole') + clearAdminRoleCache() uni.removeStorageSync('token') // Force the layout cleanup to wait for the dialog to disappear setTimeout(() => { diff --git a/layouts/admin/utils/role.uts b/layouts/admin/utils/role.uts index ceecd0ee..e66151c5 100644 --- a/layouts/admin/utils/role.uts +++ b/layouts/admin/utils/role.uts @@ -1,49 +1,136 @@ -// Admin role-based access control -export function getCurrentAdminRole(): string { - // 从本地存储获取当前 role。为不影响全局,优先读 admin_role,默认降级到读全局 role/userInfo,再默认 - // 根据实际系统中的 token/userInfo 结构可灵活调整 - const roleType = uni.getStorageSync('admin_role') as string - if (roleType && typeof roleType === 'string' && roleType.length > 0) { - return roleType - } - - // 检查是否有关联 merchant_id 或者是存了全局 role - const merchantId = uni.getStorageSync('merchant_id') - if (merchantId) return 'merchant' - - const globalRole = uni.getStorageSync('role') as string - if (globalRole && typeof globalRole === 'string' && globalRole.length > 0) { - return globalRole - } - - return 'admin' // 默认返回 admin 作为兜底(兼容原有全部显示逻辑) +import { state, getCurrentUser } from '@/utils/store.uts' +import supa from '@/components/supadb/aksupainstance.uts' + +/** + * 将任意角色类型的原始值格式化为标准化的应用角色 + */ +export function normalizeRole(rawRole: any | null): string { + if (rawRole == null || rawRole === undefined) return 'unknown' + const roleStr = String(rawRole).trim().toLowerCase() + if (roleStr === 'admin') return 'admin' + if (roleStr === 'merchant') return 'merchant' + return 'unknown' } -// 获取不同 role 允许访问的顶级模块 ID (主侧边栏的顶层菜单 id) -export function getVisibleTopMenuIds(role: string): string[] { - if (role === 'admin') { - return ['home', 'user', 'order', 'product', 'marketing', 'distribution', 'kefu', 'finance', 'cms', 'decoration', 'app', 'setting', 'maintain'] +/** + * 判断是否为纯后台管理员 + */ +export function isAdminRole(role: string): boolean { + return normalizeRole(role) === 'admin' +} + +/** + * 判断是否为商户角色 + */ +export function isMerchantRole(role: string): boolean { + return normalizeRole(role) === 'merchant' +} + +/** + * 获取当前的标准化角色 (同步方法) + */ +export function getCurrentAdminRole(): string { + // 1. 最高优先级:当前响应式内存 userProfile(已查数据库) + if (state.userProfile != null && state.userProfile!.role != null) { + const memRole = normalizeRole(state.userProfile!.role) + if (memRole === 'admin' || memRole === 'merchant') { + return memRole + } + } + + // 2. 缓存兜底:为了刷新页面时立刻渲染,读取最新的 adminRole 缓存 + const cachedRole = uni.getStorageSync('adminRole') + if (cachedRole != null && cachedRole != '') { + const normCached = normalizeRole(cachedRole) + if (normCached === 'admin' || normCached === 'merchant') { + return normCached + } } - if (role === 'merchant') { - // merchant: 只能看到 主页、订单、商品、营销、财务 + // 兼容旧的缓存字段以防万一 + const oldCached = uni.getStorageSync('admin_role') + if (oldCached != null && oldCached != '') { + const normOld = normalizeRole(oldCached) + if (normOld === 'admin' || normOld === 'merchant') { + return normOld + } + } + + console.warn('[AdminRole] 未能获取到有效的管理端角色,准备安全降级...') + return 'unknown' +} + +/** + * 清理本地相关角色和管理端缓存 (登出时调用) + */ +export function clearAdminRoleCache(): void { + // 清理 admin 专属 + uni.removeStorageSync('adminRole') + uni.removeStorageSync('admin_role') +} + +/** + * 校验并写入最新的 adminRole (用于 Login 后或者 Layout 挂载时强制刷新) + */ +export async function refreshAdminRole(): Promise { + const userStrProfile = await getCurrentUser() + let finalRole = 'unknown' + + if (userStrProfile != null && userStrProfile.role != null) { + finalRole = normalizeRole(userStrProfile.role) + console.log('[AdminRole] 从 ak_users 读取真实身份成功:', finalRole) + } else { + // metadata fallback + const sessionInfo = supa.getSession() + if (sessionInfo.user != null) { + const meta = sessionInfo.user?.get("user_metadata") as UTSJSONObject | null + if (meta != null && meta.getString('role') != null) { + finalRole = normalizeRole(meta.getString('role')) + console.log('[AdminRole] 从 Auth Metadata 读取兜底身份:', finalRole) + } + } + } + + if (finalRole !== 'unknown') { + uni.setStorageSync('adminRole', finalRole) + if (state.userProfile != null) { + state.userProfile!.role = finalRole + } + console.log('[AdminRole] 最新角色已写入状态和缓存:', finalRole) + } + + return finalRole +} + +export function getVisibleTopMenuIds(role: string): string[] { + const normRole = normalizeRole(role) + if (normRole === 'admin') { + return ['home', 'user', 'order', 'product', 'marketing', 'distribution', 'kefu', 'finance', 'cms', 'decoration', 'app', 'setting', 'maintain'] + } + + if (normRole === 'merchant') { return ['home', 'order', 'product', 'marketing', 'finance'] } - // 其他 role: 安全兜底 return ['home'] } -// 判断是否有权限访问某模块或路由 -export function hasAdminModuleAccess(moduleId: string | undefined): boolean { - if (!moduleId) return true // 没有指定的通常认为是公共的,比如一些通用组件 +export function hasAdminModuleAccess(moduleId: string | undefined): boolean { + if (!moduleId) return true const role = getCurrentAdminRole() - if (role === 'admin') return true + const normRole = normalizeRole(role) + + if (normRole === 'unknown') { + return moduleId === 'home' + } + + if (normRole === 'admin') { + return true + } - // 对于 merchant 角色,允许其访问的顶级 menu id 及其所属路由 - if (role === 'merchant') { - const allowed = ['home', 'order', 'product', 'marketing', 'finance'] + if (normRole === 'merchant') { + const allowed = ['home', 'order', 'product', 'marketing', 'finance'] return allowed.includes(moduleId) } diff --git a/pages/mall/admin/错误信息.txt b/pages/mall/admin/错误信息.txt index ff2a4d99..6e1da929 100644 --- a/pages/mall/admin/错误信息.txt +++ b/pages/mall/admin/错误信息.txt @@ -1,16 +1,43 @@ -ak-req.uts:144 - POST http://119.146.131.237:9126/auth/v1/signup 500 (Internal Server Error) -register.uvue:221 📝 注册返回结果: -UTSJSONObject2 {code: '23505', message: 'duplicate key value violates unique constraint "ak_users_email_key"', detail: 'Key (email)=(admin@163.com) already exists.', _resolveKeyPath: ƒ, _getValue: ƒ, …} -code + +list.uvue:526 Coupon list initializing and fetching data... +13 +role.uts:59 [AdminRole] 未能获取到有效的管理端角色,准备安全降级... +role.uts:59 [AdminRole] 未能获取到有效的管理端角色,准备安全降级... +login.uvue:373 signIn result: +AkSupaSignInResult {access_token: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJmO…xzZX0.YFcXQloKqsalFsOktCsDQUWPwvP8d_B58ss_SznxwZs', refresh_token: 'gdsl27rjhn62', expires_at: 1773284440, user: UTSJSONObject2, token_type: 'bearer', …} +access_token : -"23505" -detail +"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJmOTkyZGZmYS1hOGZkLTQ1YmItODY3MC02ZmVlNWE1YWU4NGQiLCJhdWQiOiJhdXRoZW50aWNhdGVkIiwiZXhwIjoxNzczMjg0NDQwLCJpYXQiOjE3NzMyODA4NDAsImVtYWlsIjoiYWRtaW5AMTYzLmNvbSIsInBob25lIjoiIiwiYXBwX21ldGFkYXRhIjp7InByb3ZpZGVyIjoiZW1haWwiLCJwcm92aWRlcnMiOlsiZW1haWwiXX0sInVzZXJfbWV0YWRhdGEiOnsiZW1haWwiOiJhZG1pbkAxNjMuY29tIiwiZW1haWxfdmVyaWZpZWQiOnRydWUsInBob25lX3ZlcmlmaWVkIjpmYWxzZSwic3ViIjoiZjk5MmRmZmEtYThmZC00NWJiLTg2NzAtNmZlZTVhNWFlODRkIiwidXNlcl9yb2xlIjoibWVyY2hhbnQifSwicm9sZSI6ImF1dGhlbnRpY2F0ZWQiLCJhYWwiOiJhYWwxIiwiYW1yIjpbeyJtZXRob2QiOiJwYXNzd29yZCIsInRpbWVzdGFtcCI6MTc3MzI4MDg0MH1dLCJzZXNzaW9uX2lkIjoiMTJhMmEyZjgtZWU1ZC00OWZjLWIwOTAtOTBlNmIzNWMxZGJhIiwiaXNfYW5vbnltb3VzIjpmYWxzZX0.YFcXQloKqsalFsOktCsDQUWPwvP8d_B58ss_SznxwZs" +expires_at : -"Key (email)=(admin@163.com) already exists." -message +1773284440 +expires_in : -"duplicate key value violates unique constraint \"ak_users_email_key\"" +3600 +raw +: +UTSJSONObject2 +access_token +: +"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJmOTkyZGZmYS1hOGZkLTQ1YmItODY3MC02ZmVlNWE1YWU4NGQiLCJhdWQiOiJhdXRoZW50aWNhdGVkIiwiZXhwIjoxNzczMjg0NDQwLCJpYXQiOjE3NzMyODA4NDAsImVtYWlsIjoiYWRtaW5AMTYzLmNvbSIsInBob25lIjoiIiwiYXBwX21ldGFkYXRhIjp7InByb3ZpZGVyIjoiZW1haWwiLCJwcm92aWRlcnMiOlsiZW1haWwiXX0sInVzZXJfbWV0YWRhdGEiOnsiZW1haWwiOiJhZG1pbkAxNjMuY29tIiwiZW1haWxfdmVyaWZpZWQiOnRydWUsInBob25lX3ZlcmlmaWVkIjpmYWxzZSwic3ViIjoiZjk5MmRmZmEtYThmZC00NWJiLTg2NzAtNmZlZTVhNWFlODRkIiwidXNlcl9yb2xlIjoibWVyY2hhbnQifSwicm9sZSI6ImF1dGhlbnRpY2F0ZWQiLCJhYWwiOiJhYWwxIiwiYW1yIjpbeyJtZXRob2QiOiJwYXNzd29yZCIsInRpbWVzdGFtcCI6MTc3MzI4MDg0MH1dLCJzZXNzaW9uX2lkIjoiMTJhMmEyZjgtZWU1ZC00OWZjLWIwOTAtOTBlNmIzNWMxZGJhIiwiaXNfYW5vbnltb3VzIjpmYWxzZX0.YFcXQloKqsalFsOktCsDQUWPwvP8d_B58ss_SznxwZs" +expires_at +: +1773284440 +expires_in +: +3600 +refresh_token +: +"gdsl27rjhn62" +token_type +: +"bearer" +user +: +UTSJSONObject2 {id: 'f992dffa-a8fd-45bb-8670-6fee5a5ae84d', aud: 'authenticated', role: 'authenticated', email: 'admin@163.com', email_confirmed_at: '2026-03-12T01:25:56.424096Z', …} +weak_password +: +null forEach : ƒ forEach(callback) @@ -52,4 +79,205 @@ _resolveKeyPath ƒ _resolveKeyPath(keyPath) [[Prototype]] : -Object \ No newline at end of file +Object +refresh_token +: +"gdsl27rjhn62" +token_type +: +"bearer" +user +: +UTSJSONObject2 {id: 'f992dffa-a8fd-45bb-8670-6fee5a5ae84d', aud: 'authenticated', role: 'authenticated', email: 'admin@163.com', email_confirmed_at: '2026-03-12T01:25:56.424096Z', …} +$UTSMetadata$ +: +(...) +[[Prototype]] +: +UTSType +login.uvue:176 🔍 开始校验商家端角色 -> UID: f992dffa-a8fd-45bb-8670-6fee5a5ae84d, Email: admin@163.com +login.uvue:186 ✅ 按 auth_id 匹配成功,role: admin +login.uvue:449 登录错误: Error: 您还没有注册商家端账户,快去注册一个 + at login.uvue:403:10 + at Generator.next () +login.uvue:373 signIn result: +AkSupaSignInResult {access_token: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJmO…xzZX0.0RArWcn148XkRpW9C0vwboAcvpem4KRz6-OO0vAE4RU', refresh_token: 'kgzfaeokz4r2', expires_at: 1773284456, user: UTSJSONObject2, token_type: 'bearer', …} +access_token +: +"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJmOTkyZGZmYS1hOGZkLTQ1YmItODY3MC02ZmVlNWE1YWU4NGQiLCJhdWQiOiJhdXRoZW50aWNhdGVkIiwiZXhwIjoxNzczMjg0NDU2LCJpYXQiOjE3NzMyODA4NTYsImVtYWlsIjoiYWRtaW5AMTYzLmNvbSIsInBob25lIjoiIiwiYXBwX21ldGFkYXRhIjp7InByb3ZpZGVyIjoiZW1haWwiLCJwcm92aWRlcnMiOlsiZW1haWwiXX0sInVzZXJfbWV0YWRhdGEiOnsiZW1haWwiOiJhZG1pbkAxNjMuY29tIiwiZW1haWxfdmVyaWZpZWQiOnRydWUsInBob25lX3ZlcmlmaWVkIjpmYWxzZSwic3ViIjoiZjk5MmRmZmEtYThmZC00NWJiLTg2NzAtNmZlZTVhNWFlODRkIiwidXNlcl9yb2xlIjoibWVyY2hhbnQifSwicm9sZSI6ImF1dGhlbnRpY2F0ZWQiLCJhYWwiOiJhYWwxIiwiYW1yIjpbeyJtZXRob2QiOiJwYXNzd29yZCIsInRpbWVzdGFtcCI6MTc3MzI4MDg1Nn1dLCJzZXNzaW9uX2lkIjoiN2NlZWYwZjQtNmNlOS00ZDE5LWJjYmItNzFhNDVmOTRiZTI2IiwiaXNfYW5vbnltb3VzIjpmYWxzZX0.0RArWcn148XkRpW9C0vwboAcvpem4KRz6-OO0vAE4RU" +expires_at +: +1773284456 +expires_in +: +3600 +raw +: +UTSJSONObject2 +access_token +: +"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJmOTkyZGZmYS1hOGZkLTQ1YmItODY3MC02ZmVlNWE1YWU4NGQiLCJhdWQiOiJhdXRoZW50aWNhdGVkIiwiZXhwIjoxNzczMjg0NDU2LCJpYXQiOjE3NzMyODA4NTYsImVtYWlsIjoiYWRtaW5AMTYzLmNvbSIsInBob25lIjoiIiwiYXBwX21ldGFkYXRhIjp7InByb3ZpZGVyIjoiZW1haWwiLCJwcm92aWRlcnMiOlsiZW1haWwiXX0sInVzZXJfbWV0YWRhdGEiOnsiZW1haWwiOiJhZG1pbkAxNjMuY29tIiwiZW1haWxfdmVyaWZpZWQiOnRydWUsInBob25lX3ZlcmlmaWVkIjpmYWxzZSwic3ViIjoiZjk5MmRmZmEtYThmZC00NWJiLTg2NzAtNmZlZTVhNWFlODRkIiwidXNlcl9yb2xlIjoibWVyY2hhbnQifSwicm9sZSI6ImF1dGhlbnRpY2F0ZWQiLCJhYWwiOiJhYWwxIiwiYW1yIjpbeyJtZXRob2QiOiJwYXNzd29yZCIsInRpbWVzdGFtcCI6MTc3MzI4MDg1Nn1dLCJzZXNzaW9uX2lkIjoiN2NlZWYwZjQtNmNlOS00ZDE5LWJjYmItNzFhNDVmOTRiZTI2IiwiaXNfYW5vbnltb3VzIjpmYWxzZX0.0RArWcn148XkRpW9C0vwboAcvpem4KRz6-OO0vAE4RU" +expires_at +: +1773284456 +expires_in +: +3600 +refresh_token +: +"kgzfaeokz4r2" +token_type +: +"bearer" +user +: +UTSJSONObject2 {id: 'f992dffa-a8fd-45bb-8670-6fee5a5ae84d', aud: 'authenticated', role: 'authenticated', email: 'admin@163.com', email_confirmed_at: '2026-03-12T01:25:56.424096Z', …} +weak_password +: +null +forEach +: +ƒ forEach(callback) +get +: +ƒ get(key) +getAny +: +ƒ getAny(key, defaultValue) +getArray +: +ƒ getArray(key, defaultValue) +getBoolean +: +ƒ getBoolean(key, defaultValue) +getJSON +: +ƒ getJSON(key, defaultValue) +getNumber +: +ƒ getNumber(key, defaultValue) +getString +: +ƒ getString(key, defaultValue) +set +: +ƒ set(key, value) +toJSON +: +undefined +toMap +: +ƒ toMap() +_getValue +: +ƒ _getValue(keyPath, defaultValue) +_resolveKeyPath +: +ƒ _resolveKeyPath(keyPath) +[[Prototype]] +: +Object +refresh_token +: +"kgzfaeokz4r2" +token_type +: +"bearer" +user +: +UTSJSONObject2 +app_metadata +: +UTSJSONObject2 {provider: 'email', providers: Array(1), toJSON: undefined, _resolveKeyPath: ƒ, _getValue: ƒ, …} +aud +: +"authenticated" +confirmed_at +: +"2026-03-12T01:25:56.424096Z" +created_at +: +"2026-03-12T01:25:56.397092Z" +email +: +"admin@163.com" +email_confirmed_at +: +"2026-03-12T01:25:56.424096Z" +id +: +"f992dffa-a8fd-45bb-8670-6fee5a5ae84d" +identities +: +[UTSJSONObject2] +is_anonymous +: +false +last_sign_in_at +: +"2026-03-12T02:00:56.659679275Z" +phone +: +"" +role +: +"authenticated" +updated_at +: +"2026-03-12T02:00:56.667158Z" +user_metadata +: +UTSJSONObject2 {email: 'admin@163.com', email_verified: true, phone_verified: false, sub: 'f992dffa-a8fd-45bb-8670-6fee5a5ae84d', user_role: 'merchant', …} +forEach +: +ƒ forEach(callback) +get +: +ƒ get(key) +getAny +: +ƒ getAny(key, defaultValue) +getArray +: +ƒ getArray(key, defaultValue) +getBoolean +: +ƒ getBoolean(key, defaultValue) +getJSON +: +ƒ getJSON(key, defaultValue) +getNumber +: +ƒ getNumber(key, defaultValue) +getString +: +ƒ getString(key, defaultValue) +set +: +ƒ set(key, value) +toJSON +: +undefined +toMap +: +ƒ toMap() +_getValue +: +ƒ _getValue(keyPath, defaultValue) +_resolveKeyPath +: +ƒ _resolveKeyPath(keyPath) +[[Prototype]] +: +Object +$UTSMetadata$ +: +(...) +[[Prototype]] +: +UTSType +login.uvue:176 🔍 开始校验商家端角色 -> UID: f992dffa-a8fd-45bb-8670-6fee5a5ae84d, Email: admin@163.com +login.uvue:186 ✅ 按 auth_id 匹配成功,role: admin +login.uvue:449 登录错误: Error: 您还没有注册商家端账户,快去注册一个 + at login.uvue:403:10 + at Generator.next () diff --git a/pages/user/login.uvue b/pages/user/login.uvue index bee9991a..708fd401 100644 --- a/pages/user/login.uvue +++ b/pages/user/login.uvue @@ -175,65 +175,54 @@ const isLoading = ref(false) * 【核心函数】:登录成功后,多条件校验是否为商家角色 * 优先级: session_uid (auth_id) -> id -> normalized email */ -const checkMerchantAccess = async (uid: string, rawEmail: string) : Promise => { - const email = rawEmail.trim().toLowerCase() - console.log(`🔍 开始校验商家端角色 -> UID: ${uid}, Email: ${email}`) +const checkAdminOrMerchantAccess = async (uid: string, rawEmail: string) : Promise => { + const email = rawEmail.trim().toLowerCase() + console.log(`🔍 开始校验后台或商家端角色 -> UID: ${uid}, Email: ${email}`) - try { - // 1. 尝试按 auth_id 查询 - let res = await supa.from('ak_users').select('id, role').eq('auth_id', uid).execute() - let dataArray = res.data - if (Array.isArray(dataArray) && dataArray.length > 0) { - const obj = dataArray[0] as UTSJSONObject - const role = obj.getString('role') - const id = obj.getString('id') - console.log('✅ 按 auth_id 匹配成功,role:', role) - if (role === 'merchant' && id != null) return id - return null - } + const parseRoleData = (dataArray: any | null): UTSJSONObject | null => { + if (Array.isArray(dataArray) && dataArray.length > 0) { + const obj = dataArray[0] as UTSJSONObject + const role = obj.getString('role') + const id = obj.getString('id') + console.log('✅ 匹配成功,role:', role) + if ((role === 'merchant' || role === 'admin') && id != null) { + return { id, role } as UTSJSONObject + } + } + return null + } - // 2. 尝试按 id 查询 (兼容老数据) - res = await supa.from('ak_users').select('id, role').eq('id', uid).execute() - dataArray = res.data - if (Array.isArray(dataArray) && dataArray.length > 0) { - const obj = dataArray[0] as UTSJSONObject - const role = obj.getString('role') - const id = obj.getString('id') - console.log('✅ 按 id 匹配成功,role:', role) - if (role === 'merchant' && id != null) return id - return null - } + try { + // 1. 尝试按 auth_id 查询 + let res = await supa.from('ak_users').select('id, role').eq('auth_id', uid).execute() + let parsed = parseRoleData(res.data) + if (parsed != null) return parsed - // 3. 尝试按 email 兜底查询 - if (email !== '') { - res = await supa.from('ak_users').select('id, role').eq('email', email).execute() - dataArray = res.data - - if (Array.isArray(dataArray) && dataArray.length > 0) { - // 如果按邮箱查出来多条,可能存在脏数据,只取第一条并记录日志 - if (dataArray.length > 1) { - console.error('⚠️ 警告: 按 email 查到多条 ak_users 记录,取第一条校验。Email:', email) - } - const obj = dataArray[0] as UTSJSONObject - const role = obj.getString('role') - const id = obj.getString('id') - console.log('✅ 按 email 匹配成功,role:', role) - if (role === 'merchant' && id != null) return id - return null - } - } + // 2. 尝试按 id 查询 (兼容老数据) + res = await supa.from('ak_users').select('id, role').eq('id', uid).execute() + parsed = parseRoleData(res.data) + if (parsed != null) return parsed - console.error('❌ 未能在 ak_users 中找到该用户的任何记录') - // 查无此人,跑出自定义错误以与普通系统报错区分 - throw new Error('NOT_REGISTERED') - } catch (e) { - console.error('❌ 查询角色过程异常:', e) - if (e instanceof Error && e.message === 'NOT_REGISTERED') { - throw new Error('您还没有注册商家端账户,快去注册一个') - } - // 真实的查询异常/RLS异常抛出,防止误会为"未注册" - throw new Error('商家身份校验失败,请联系管理员检查用户数据') - } + // 3. 尝试按 email 兜底查询 + if (email !== '') { + res = await supa.from('ak_users').select('id, role').eq('email', email).execute() + const dataArray = res.data + if (Array.isArray(dataArray) && dataArray.length > 1) { + console.error('⚠️ 警告: 按 email 查到多条 ak_users 记录,取第一条校验。Email:', email) + } + parsed = parseRoleData(dataArray) + if (parsed != null) return parsed + } + + console.error('❌ 未能在 ak_users 中找到该用户的有效角色记录') + throw new Error('NOT_REGISTERED') + } catch (e) { + console.error('❌ 查询角色过程异常:', e) + if (e instanceof Error && e.message === 'NOT_REGISTERED') { + throw new Error('该账户无后台或商家端权限,请联系管理员核对') + } + throw new Error('后台身份校验失败,请联系管理员检查用户数据') + } } const codeDisabled = ref(false) @@ -400,15 +389,22 @@ const handleLogin = async () => { const sessionUser = result.user let sessionUid = sessionUser?.getString('id') ?? '' - const merchantId = await checkMerchantAccess(sessionUid, account.value) - if (merchantId == null) { - await supa.signOut() - logout() - throw new Error('您还没有注册商家端账户,快去注册一个') - } + const accessData = await checkAdminOrMerchantAccess(sessionUid, account.value) + if (accessData == null) { + await supa.signOut() + logout() + throw new Error('该账户无后台或商家端权限') + } - // 存入商家ID - uni.setStorageSync('merchant_id', merchantId) + const currRole = accessData.getString('role') + const currId = accessData.getString('id') + uni.setStorageSync('adminRole', currRole) + + if (currRole === 'merchant') { + uni.setStorageSync('merchant_id', currId) + } else { + uni.removeStorageSync('merchant_id') + } } else { uni.showToast({ title: '手机号密码登录功能开发中', icon: 'none' }) return diff --git a/update_header.py b/update_header.py new file mode 100644 index 00000000..ba451274 --- /dev/null +++ b/update_header.py @@ -0,0 +1,64 @@ +import codecs + +path = r'd:\骅锋\mall\layouts\admin\components\AdminHeader.uvue' +with codecs.open(path, 'r', 'utf-8') as f: + text = f.read() + +# Replace logout logic +old_logout = '''function handleLogout(e: any) { + if (e && typeof e.stopPropagation === 'function') { + e.stopPropagation() + } + showUserMenu.value = false + uni.showModal({ + title: '提示', + content: '确定要退出登录吗?', + success: (res) => { + if (res.confirm) { + logout() + uni.removeStorageSync('adminRole') + uni.removeStorageSync('token') + // Force the layout cleanup to wait for the dialog to disappear + setTimeout(() => { + uni.reLaunch({ + url: '/pages/user/login' + }) + }, 300) + } + } + }) +}''' + +new_logout = '''import { clearAdminRoleCache } from '@/layouts/admin/utils/role.uts' + +function handleLogout(e: any) { + if (e && typeof e.stopPropagation === 'function') { + e.stopPropagation() + } + showUserMenu.value = false + uni.showModal({ + title: '提示', + content: '确定要退出登录吗?', + success: (res) => { + if (res.confirm) { + logout() + clearAdminRoleCache() + uni.removeStorageSync('token') + // Force the layout cleanup to wait for the dialog to disappear + setTimeout(() => { + uni.reLaunch({ + url: '/pages/user/login' + }) + }, 300) + } + } + }) +}''' + +if old_logout in text: + text = text.replace(old_logout, new_logout) + with codecs.open(path, 'w', 'utf-8') as f: + f.write(text) + print("AdminHeader logout logic updated.") +else: + print("AdminHeader target old snippet not found.") diff --git a/update_header2.py b/update_header2.py new file mode 100644 index 00000000..69dd0e13 --- /dev/null +++ b/update_header2.py @@ -0,0 +1,17 @@ +import codecs +import re + +path = r'd:\骅锋\mall\layouts\admin\components\AdminHeader.uvue' +with codecs.open(path, 'r', 'utf-8') as f: + text = f.read() + +# Replace uni.removeStorageSync('adminRole') with clearAdminRoleCache() +# Add import if missing +if 'clearAdminRoleCache' not in text: + text = text.replace("import { state, logout } from '@/utils/store.uts'", "import { state, logout } from '@/utils/store.uts'\nimport { clearAdminRoleCache } from '@/layouts/admin/utils/role.uts'") + +text = text.replace("uni.removeStorageSync('adminRole')", "clearAdminRoleCache()") + +with codecs.open(path, 'w', 'utf-8') as f: + f.write(text) +print("Updated via direct string replace") diff --git a/update_layout.py b/update_layout.py new file mode 100644 index 00000000..18adb636 --- /dev/null +++ b/update_layout.py @@ -0,0 +1,22 @@ +import codecs + +path = r'd:\骅锋\mall\layouts\admin\AdminLayout.uvue' +with codecs.open(path, 'r', 'utf-8') as f: + text = f.read() + +if 'refreshAdminRole' not in text: + text = text.replace("import { hasAdminModuleAccess } from '@/layouts/admin/utils/role.uts'", "import { hasAdminModuleAccess, refreshAdminRole } from '@/layouts/admin/utils/role.uts'") + + insert_code = '''onMounted(async () => { + // 核心修复:挂载时强制刷新一下角色(异步获取 ak_users/metadata 并覆盖缓存) + await refreshAdminRole() + + initNavState()''' + + text = text.replace("onMounted(() => {\n initNavState()", insert_code) + + with codecs.open(path, 'w', 'utf-8') as f: + f.write(text) + print("AdminLayout updated with refreshAdminRole.") +else: + print("AdminLayout already updated.") diff --git a/update_layout2.py b/update_layout2.py new file mode 100644 index 00000000..56ca27d2 --- /dev/null +++ b/update_layout2.py @@ -0,0 +1,20 @@ +import codecs +import re + +path = r'd:\骅锋\mall\layouts\admin\AdminLayout.uvue' +with codecs.open(path, 'r', 'utf-8') as f: + text = f.read() + +if 'refreshAdminRole' not in text: + text = text.replace("import { hasAdminModuleAccess", "import { hasAdminModuleAccess, refreshAdminRole") + + # regex replace onMounted(() => { \s* initNavState() + text = re.sub(r'onMounted\(\(\) => \{\s+initNavState\(\)', + "onMounted(async () => {\n await refreshAdminRole()\n initNavState()", + text) + + with codecs.open(path, 'w', 'utf-8') as f: + f.write(text) + print("AdminLayout regex updated.") +else: + print("Already there.") diff --git a/update_layout3.py b/update_layout3.py new file mode 100644 index 00000000..e6981cda --- /dev/null +++ b/update_layout3.py @@ -0,0 +1,19 @@ +import codecs +import re + +path = r'd:\骅锋\mall\layouts\admin\AdminLayout.uvue' +with codecs.open(path, 'r', 'utf-8') as f: + text = f.read() + +# Make sure we add the await +if 'await refreshAdminRole()' not in text: + text = re.sub(r'onMounted\(\(\) => \{\s+initNavState\(\)', + "onMounted(async () => {\n await refreshAdminRole()\n initNavState()", + text) + + with codecs.open(path, 'w', 'utf-8') as f: + f.write(text) + print("Injected await refreshAdminRole()") +else: + print("Await already present.") + diff --git a/update_login.py b/update_login.py new file mode 100644 index 00000000..4c7a91bb --- /dev/null +++ b/update_login.py @@ -0,0 +1,106 @@ +import codecs +import re + +path = r'd:\骅锋\mall\pages\user\login.uvue' +with codecs.open(path, 'r', 'utf-8') as f: + text = f.read() + +# Replace the checkMerchantAccess function and related logic + +old_func_start = "const checkMerchantAccess = async (uid: string, rawEmail: string) : Promise => {" +old_func_end_marker = "const codeDisabled = ref(false)" + +func_start_idx = text.find(old_func_start) +func_end_idx = text.find(old_func_end_marker) + +if func_start_idx != -1 and func_end_idx != -1: + new_func = '''const checkAdminOrMerchantAccess = async (uid: string, rawEmail: string) : Promise => { + const email = rawEmail.trim().toLowerCase() + console.log(🔍 开始校验后台或商家端角色 -> UID: , Email: ) + + const parseRoleData = (dataArray: any | null): UTSJSONObject | null => { + if (Array.isArray(dataArray) && dataArray.length > 0) { + const obj = dataArray[0] as UTSJSONObject + const role = obj.getString('role') + const id = obj.getString('id') + console.log('✅ 匹配成功,role:', role) + if ((role === 'merchant' || role === 'admin') && id != null) { + return { id, role } as UTSJSONObject + } + } + return null + } + + try { + // 1. 尝试按 auth_id 查询 + let res = await supa.from('ak_users').select('id, role').eq('auth_id', uid).execute() + let parsed = parseRoleData(res.data) + if (parsed != null) return parsed + + // 2. 尝试按 id 查询 (兼容老数据) + res = await supa.from('ak_users').select('id, role').eq('id', uid).execute() + parsed = parseRoleData(res.data) + if (parsed != null) return parsed + + // 3. 尝试按 email 兜底查询 + if (email !== '') { + res = await supa.from('ak_users').select('id, role').eq('email', email).execute() + const dataArray = res.data + if (Array.isArray(dataArray) && dataArray.length > 1) { + console.error('⚠️ 警告: 按 email 查到多条 ak_users 记录,取第一条校验。Email:', email) + } + parsed = parseRoleData(dataArray) + if (parsed != null) return parsed + } + + console.error('❌ 未能在 ak_users 中找到该用户的有效角色记录') + throw new Error('NOT_REGISTERED') + } catch (e) { + console.error('❌ 查询角色过程异常:', e) + if (e instanceof Error && e.message === 'NOT_REGISTERED') { + throw new Error('该账户无后台或商家端权限,请联系管理员核对') + } + throw new Error('后台身份校验失败,请联系管理员检查用户数据') + } +} + +''' + + text = text[:func_start_idx] + new_func + text[func_end_idx:] + +# Replace the usage in handleLogin +old_usage = ''' const merchantId = await checkMerchantAccess(sessionUid, account.value) + if (merchantId == null) { + await supa.signOut() + logout() + throw new Error('您还没有注册商家端账户,快去注册一个') + } + + // 存入商家ID + uni.setStorageSync('merchant_id', merchantId)''' + +new_usage = ''' const accessData = await checkAdminOrMerchantAccess(sessionUid, account.value) + if (accessData == null) { + await supa.signOut() + logout() + throw new Error('该账户无后台或商家端权限') + } + + const currRole = accessData.getString('role') + const currId = accessData.getString('id') + uni.setStorageSync('adminRole', currRole) + + if (currRole === 'merchant') { + uni.setStorageSync('merchant_id', currId) + } else { + uni.removeStorageSync('merchant_id') + }''' + +if 'checkMerchantAccess' in text: + print("WARNING: checkMerchantAccess still found after replace, let's substitute directly") + text = text.replace(old_usage, new_usage) + +with codecs.open(path, 'w', 'utf-8') as f: + f.write(text) + +print("Updated login.uvue successfully.") diff --git a/update_role.py b/update_role.py new file mode 100644 index 00000000..325f77c5 --- /dev/null +++ b/update_role.py @@ -0,0 +1,146 @@ +import codecs + +content = '''import { state, getCurrentUser } from '@/utils/store.uts' +import supa from '@/components/supadb/aksupainstance.uts' + +/** + * 将任意角色类型的原始值格式化为标准化的应用角色 + */ +export function normalizeRole(rawRole: any | null): string { + if (rawRole == null || rawRole === undefined) return 'unknown' + const roleStr = String(rawRole).trim().toLowerCase() + if (roleStr === 'admin') return 'admin' + if (roleStr === 'merchant') return 'merchant' + return 'unknown' +} + +/** + * 判断是否为纯后台管理员 + */ +export function isAdminRole(role: string): boolean { + return normalizeRole(role) === 'admin' +} + +/** + * 判断是否为商户角色 + */ +export function isMerchantRole(role: string): boolean { + return normalizeRole(role) === 'merchant' +} + +/** + * 获取当前的标准化角色 (同步方法) + */ +export function getCurrentAdminRole(): string { + // 1. 最高优先级:当前响应式内存 userProfile(已查数据库) + if (state.userProfile != null && state.userProfile!.role != null) { + const memRole = normalizeRole(state.userProfile!.role) + if (memRole === 'admin' || memRole === 'merchant') { + return memRole + } + } + + // 2. 缓存兜底:为了刷新页面时立刻渲染,读取最新的 adminRole 缓存 + const cachedRole = uni.getStorageSync('adminRole') + if (cachedRole != null && cachedRole != '') { + const normCached = normalizeRole(cachedRole) + if (normCached === 'admin' || normCached === 'merchant') { + return normCached + } + } + + // 兼容旧的缓存字段以防万一 + const oldCached = uni.getStorageSync('admin_role') + if (oldCached != null && oldCached != '') { + const normOld = normalizeRole(oldCached) + if (normOld === 'admin' || normOld === 'merchant') { + return normOld + } + } + + console.warn('[AdminRole] 未能获取到有效的管理端角色,准备安全降级...') + return 'unknown' +} + +/** + * 清理本地相关角色和管理端缓存 (登出时调用) + */ +export function clearAdminRoleCache(): void { + // 清理 admin 专属 + uni.removeStorageSync('adminRole') + uni.removeStorageSync('admin_role') +} + +/** + * 校验并写入最新的 adminRole (用于 Login 后或者 Layout 挂载时强制刷新) + */ +export async function refreshAdminRole(): Promise { + const userStrProfile = await getCurrentUser() + let finalRole = 'unknown' + + if (userStrProfile != null && userStrProfile.role != null) { + finalRole = normalizeRole(userStrProfile.role) + console.log('[AdminRole] 从 ak_users 读取真实身份成功:', finalRole) + } else { + // metadata fallback + const sessionInfo = supa.getSession() + if (sessionInfo.user != null) { + const meta = sessionInfo.user?.get("user_metadata") as UTSJSONObject | null + if (meta != null && meta.getString('role') != null) { + finalRole = normalizeRole(meta.getString('role')) + console.log('[AdminRole] 从 Auth Metadata 读取兜底身份:', finalRole) + } + } + } + + if (finalRole !== 'unknown') { + uni.setStorageSync('adminRole', finalRole) + if (state.userProfile != null) { + state.userProfile!.role = finalRole + } + console.log('[AdminRole] 最新角色已写入状态和缓存:', finalRole) + } + + return finalRole +} + +export function getVisibleTopMenuIds(role: string): string[] { + const normRole = normalizeRole(role) + if (normRole === 'admin') { + return ['home', 'user', 'order', 'product', 'marketing', 'distribution', 'kefu', 'finance', 'cms', 'decoration', 'app', 'setting', 'maintain'] + } + + if (normRole === 'merchant') { + return ['home', 'order', 'product', 'marketing', 'finance'] + } + + return ['home'] +} + +export function hasAdminModuleAccess(moduleId: string | undefined): boolean { + if (!moduleId) return true + + const role = getCurrentAdminRole() + const normRole = normalizeRole(role) + + if (normRole === 'unknown') { + return moduleId === 'home' + } + + if (normRole === 'admin') { + return true + } + + if (normRole === 'merchant') { + const allowed = ['home', 'order', 'product', 'marketing', 'finance'] + return allowed.includes(moduleId) + } + + return false +} +''' + +with codecs.open(r'd:\骅锋\mall\layouts\admin\utils\role.uts', 'w', 'utf-8') as f: + f.write(content) + +print("success!")