import { defineStore } from 'pinia' import { ref, computed } from 'vue' import { AuthService, setAuthToken, clearAuthToken, OmsLoginType, type LoginResultDto, type UserInfoDto, } from '@/api/rbac' import { handleApiError } from '@/utils/rbac-error-handler' import { getLoginRouteNameByType } from '@/utils/login-portal' // Token 存储 key const TOKEN_KEY = 'rbac_access_token' const REFRESH_TOKEN_KEY = 'rbac_refresh_token' const TOKEN_EXPIRES_KEY = 'rbac_token_expires_at' const USER_INFO_KEY = 'rbac_userInfo' // 兼容旧代码的 key const OLD_TOKEN_KEY = 'rbac_token' const LOGIN_TYPE_KEY = 'rbac_login_type' const SSO_SESSION_FLAG = 'rbac_sso_session' function isSsoSessionActive(): boolean { return sessionStorage.getItem(SSO_SESSION_FLAG) === '1' } function getAuthStorage(): Storage { return isSsoSessionActive() ? sessionStorage : localStorage } function readAuthItem(key: string): string { if (isSsoSessionActive()) { return sessionStorage.getItem(key) || '' } return localStorage.getItem(key) || '' } // 1小时无操作则认为会话应过期(前端主导的“空闲超时”,用于控制是否允许续期) const KEEP_ALIVE_IDLE_MS = 60 * 60 * 1000 // 注意:tokenExpiresAt 在 saveTokens 里会提前 5 分钟到期刷新(当前实现是 -300s) // 因此“真实 accessToken 过期时间”= tokenExpiresAt + 5min const ACCESS_TOKEN_REAL_EXPIRES_AT_OFFSET_MS = 5 * 60 * 1000 export const useRbacUserStore = defineStore('rbacUser', () => { const persistInSession = ref(isSsoSessionActive()) const accessToken = ref(readAuthItem(TOKEN_KEY)) const refreshToken = ref(readAuthItem(REFRESH_TOKEN_KEY)) const tokenExpiresAt = ref(Number(readAuthItem(TOKEN_EXPIRES_KEY)) || 0) // 仅在用户发生交互后才会开启;开启后允许续期,并在倒计时到达时登出 const keepAliveUntil = ref(0) const savedUserInfo = getAuthStorage().getItem(USER_INFO_KEY) const userInfo = ref(savedUserInfo ? JSON.parse(savedUserInfo) : null) const loginPortalType = ref(readStoredLoginType()) const permissions = computed(() => userInfo.value?.permissions || []) const menus = computed(() => userInfo.value?.menus || []) const isLoggedIn = computed(() => !!accessToken.value) // 兼容旧代码 const token = computed(() => accessToken.value) function readStoredLoginType(): OmsLoginType { const raw = getAuthStorage().getItem(LOGIN_TYPE_KEY) ?? localStorage.getItem(LOGIN_TYPE_KEY) if (raw === String(OmsLoginType.Customer)) return OmsLoginType.Customer return OmsLoginType.User } function setLoginPortalType(type: OmsLoginType, options?: { sessionOnly?: boolean }) { loginPortalType.value = type if (options?.sessionOnly) { persistInSession.value = true sessionStorage.setItem(SSO_SESSION_FLAG, '1') sessionStorage.setItem(LOGIN_TYPE_KEY, String(type)) return } persistInSession.value = false sessionStorage.removeItem(SSO_SESSION_FLAG) localStorage.setItem(LOGIN_TYPE_KEY, String(type)) } function getLoginRouteName(): string { return getLoginRouteNameByType(loginPortalType.value) } function isCustomerPortal(): boolean { return loginPortalType.value === OmsLoginType.Customer } // 保存 Token 信息 function saveTokens(result: LoginResultDto, options?: { sessionOnly?: boolean }) { if (!result.accessToken) { return } accessToken.value = result.accessToken refreshToken.value = result.refreshToken || '' const expiresInSeconds = typeof result.expiresIn === 'number' && result.expiresIn > 0 ? result.expiresIn : 3600 tokenExpiresAt.value = Date.now() + (expiresInSeconds - 300) * 1000 const storage = options?.sessionOnly ? sessionStorage : localStorage if (options?.sessionOnly) { persistInSession.value = true sessionStorage.setItem(SSO_SESSION_FLAG, '1') } else { persistInSession.value = false sessionStorage.removeItem(SSO_SESSION_FLAG) } storage.setItem(TOKEN_KEY, accessToken.value) if (refreshToken.value) { storage.setItem(REFRESH_TOKEN_KEY, refreshToken.value) } storage.setItem(TOKEN_EXPIRES_KEY, String(tokenExpiresAt.value)) setAuthToken(accessToken.value) if (!options?.sessionOnly) { // 兼容旧代码及 auth utils 的 isLogin 检查 localStorage.setItem(OLD_TOKEN_KEY, accessToken.value) localStorage.setItem('token', accessToken.value) } } // 真实 accessToken 过期时间(用于“无操作也要按时间跳登录”的兜底) function getAccessTokenRealExpiresAt(): number { if (!tokenExpiresAt.value) return 0 return tokenExpiresAt.value + ACCESS_TOKEN_REAL_EXPIRES_AT_OFFSET_MS } function isKeepAliveActive(): boolean { return keepAliveUntil.value > 0 && Date.now() < keepAliveUntil.value } function enableKeepAlive() { keepAliveUntil.value = Date.now() + KEEP_ALIVE_IDLE_MS } function disableKeepAlive() { keepAliveUntil.value = 0 } function getKeepAliveUntil(): number { return keepAliveUntil.value } // 检查 Token 是否需要刷新 function isTokenExpiringSoon(): boolean { return tokenExpiresAt.value > 0 && Date.now() >= tokenExpiresAt.value } function normalizeRefreshToken(raw: string): string | null { let rt = raw if (rt.startsWith('Bearer ')) { rt = rt.slice('Bearer '.length) } const parts = rt.split('.') if (parts.length !== 3 || parts.some((p) => !p || p.length < 8)) { return null } if (rt.includes('[') || rt.includes(']') || rt.includes('{') || rt.includes('}')) { return null } return rt } // 刷新 Token async function refreshAccessToken(): Promise { if (!isKeepAliveActive()) { return false } if (!refreshToken.value) { return false } const rt = normalizeRefreshToken(refreshToken.value) if (!rt) { console.warn('[rbac] refreshToken format invalid, skip refresh') logout() return false } try { const result = await AuthService.postRbacAuthRefreshToken({ refreshToken: rt }) if (result?.accessToken) { saveTokens(result, { sessionOnly: persistInSession.value }) return true } return false } catch { logout() return false } } // 登录(OMS 管理端默认 type=User) async function login(account: string, password: string, loginType: OmsLoginType = OmsLoginType.User) { try { const result = await AuthService.postRbacAuthLogin({ account, password, type: loginType, }) saveTokens(result) setLoginPortalType(loginType) await getCurrentUser() return true } catch (error) { handleApiError(error) return false } } /** 微信扫码登录成功后写入 Token 并拉取用户信息 */ async function loginByWxResult(result: LoginResultDto): Promise { try { if (!result?.accessToken) { return false } saveTokens(result) setLoginPortalType(OmsLoginType.Customer) await getCurrentUser() return true } catch (error) { handleApiError(error) return false } } /** 管理员代登客户门户:Token 仅存 sessionStorage,避免覆盖管理端登录态 */ async function loginBySsoResult(result: LoginResultDto): Promise { try { if (!result?.accessToken) { return false } saveTokens(result, { sessionOnly: true }) setLoginPortalType(OmsLoginType.Customer, { sessionOnly: true }) await getCurrentUser() return true } catch (error) { handleApiError(error) return false } } // 获取当前用户信息(基于 Token,无需 userId 参数) async function getCurrentUser() { try { if (isTokenExpiringSoon()) { await refreshAccessToken() } const data = await AuthService.getRbacAuthGetCurrentUser() userInfo.value = data getAuthStorage().setItem(USER_INFO_KEY, JSON.stringify(data)) return data } catch (error) { logout() throw error } } // 刷新用户信息(强制从服务器获取最新数据) async function refreshUserInfo() { try { const data = await AuthService.getRbacAuthGetCurrentUser() userInfo.value = data getAuthStorage().setItem(USER_INFO_KEY, JSON.stringify(data)) return data } catch (error) { handleApiError(error) throw error } } // 登出 function logout() { accessToken.value = '' refreshToken.value = '' tokenExpiresAt.value = 0 keepAliveUntil.value = 0 userInfo.value = null persistInSession.value = false localStorage.removeItem(TOKEN_KEY) localStorage.removeItem(REFRESH_TOKEN_KEY) localStorage.removeItem(TOKEN_EXPIRES_KEY) localStorage.removeItem(USER_INFO_KEY) // 兼容旧代码 localStorage.removeItem(OLD_TOKEN_KEY) localStorage.removeItem('token') localStorage.removeItem('userInfo') sessionStorage.removeItem(SSO_SESSION_FLAG) sessionStorage.removeItem(TOKEN_KEY) sessionStorage.removeItem(REFRESH_TOKEN_KEY) sessionStorage.removeItem(TOKEN_EXPIRES_KEY) sessionStorage.removeItem(USER_INFO_KEY) sessionStorage.removeItem(LOGIN_TYPE_KEY) clearAuthToken() } // 初始化:恢复登录状态 function initAuth() { if (accessToken.value) { setAuthToken(accessToken.value) } } initAuth() // 检查权限(与 @/utils/rbac-permission 规则一致:资源码 ↔ Query 互通) function hasPermission(permission: string): boolean { if (!permission?.trim()) return true const code = permission.trim() const granted = permissions.value if (granted.includes(code)) return true if (/\.Query$/i.test(code)) { const resource = code.replace(/\.Query$/i, '') if (resource && granted.includes(resource)) return true } else if (granted.includes(`${code}.Query`)) { return true } return false } // 检查角色 function hasRole(roleCode: string): boolean { return userInfo.value?.roles?.some((r) => r.code === roleCode) || false } return { token, accessToken, refreshToken, userInfo, permissions, menus, isLoggedIn, loginPortalType, getLoginRouteName, isCustomerPortal, getAccessTokenRealExpiresAt, isKeepAliveActive, enableKeepAlive, disableKeepAlive, getKeepAliveUntil, login, loginByWxResult, loginBySsoResult, getCurrentUser, refreshUserInfo, logout, hasPermission, hasRole, refreshAccessToken, isTokenExpiringSoon, } })