aslan-h5/src/utils/routeGuard.js
2026-04-21 16:42:29 +08:00

291 lines
7.2 KiB
JavaScript

/**
* Route guard for:
* 1. ensuring app bridge connection
* 2. loading identity information
* 3. redirecting users to pages they can access
*/
import { getUserIdentity, getMemberProfile } from '../api/wallet.js'
import { isPublicPath } from '@/config/security.js'
import { isDebugMode } from './env.js'
import { permissionManager, PAGES, USER_ROLES } from './permissionManager.js'
import { getUserId, setUserInfo } from './userStore.js'
import {
connectApplication,
isInApp,
parseAccessOrigin,
parseHeader,
setHttpHeaders,
} from './appBridge.js'
import { appConnectionManager, isAppConnected } from './appConnectionManager.js'
class AppConnectionChecker {
async ensureConnection() {
console.debug('Checking APP connection...')
if (!isInApp()) {
if (!isDebugMode()) {
console.warn('Blocked protected route outside official app')
return false
}
console.debug('Browser environment detected in debug mode')
return true
}
if (isAppConnected()) {
console.debug('APP already connected')
return true
}
if (appConnectionManager.isConnecting()) {
console.debug('Connection already in progress, waiting...')
try {
await appConnectionManager.getConnectionPromise()
return isAppConnected()
} catch (error) {
console.error('Connection wait failed:', error)
return false
}
}
return this._performConnection()
}
async _performConnection() {
try {
const connectionPromise = new Promise((resolve, reject) => {
connectApplication(async (access) => {
try {
const result = parseAccessOrigin(access)
if (!result.success) {
appConnectionManager.reset()
reject(new Error(result.error || 'accessOrigin Error.'))
return
}
const headerInfo = parseHeader(result.data)
await setHttpHeaders(headerInfo)
appConnectionManager.setConnected(headerInfo)
await this._fetchUserInfoAfterConnection()
resolve(true)
} catch (error) {
appConnectionManager.reset()
reject(error)
}
})
})
appConnectionManager.setConnecting(connectionPromise)
return await connectionPromise
} catch (error) {
console.error('APP connection failed:', error)
appConnectionManager.reset()
return false
}
}
async _fetchUserInfoAfterConnection() {
try {
const response = await getMemberProfile()
if (response && response.status && response.body) {
setUserInfo(response.body, null)
}
} catch (error) {
console.warn('Failed to fetch user info after connection:', error)
}
}
reset() {
appConnectionManager.reset()
}
}
class IdentityChecker {
constructor() {
this.isChecking = false
this.checkPromise = null
}
async checkUserIdentity(userId) {
if (!userId) {
permissionManager.setUserIdentity(null)
return USER_ROLES.GUEST
}
if (this.isChecking && this.checkPromise) {
return await this.checkPromise
}
this.isChecking = true
this.checkPromise = this._performIdentityCheck(userId)
try {
return await this.checkPromise
} finally {
this.isChecking = false
this.checkPromise = null
}
}
async _performIdentityCheck(userId) {
try {
console.debug('Checking user identity for:', userId)
const response = await getUserIdentity()
if (response && response.status && response.body) {
permissionManager.setUserIdentity(response.body)
return permissionManager.getPrimaryRole()
}
permissionManager.setUserIdentity(null)
return USER_ROLES.GUEST
} catch (error) {
console.error('Failed to fetch user identity:', error)
permissionManager.setUserIdentity(null)
return USER_ROLES.GUEST
}
}
reset() {
this.isChecking = false
this.checkPromise = null
}
}
class PageRedirector {
async redirectToCorrectPage(router, targetPath, currentPath) {
const hasPermission = permissionManager.hasPagePermission(targetPath)
if (hasPermission) {
return false
}
const defaultPage = permissionManager.getDefaultPage()
if (defaultPage !== currentPath) {
await router.replace(defaultPage)
return true
}
return false
}
async handleUnauthorizedAccess(router) {
await router.replace(permissionManager.getDefaultPage())
}
}
class RouteGuard {
constructor() {
this.appConnectionChecker = new AppConnectionChecker()
this.identityChecker = new IdentityChecker()
this.pageRedirector = new PageRedirector()
this.isGuardActive = false
}
install(router) {
router.beforeEach(async (to, from, next) => {
await this.handleRouteChange(to, from, next)
})
this.isGuardActive = true
console.debug('Route guard installed')
}
async handleRouteChange(to, from, next) {
console.group('Route Guard Check')
console.debug('Route change:', from.path, '->', to.path)
try {
if (to.meta?.isPublic || this.isPublicPage(to.path)) {
console.groupEnd()
next()
return
}
const isConnected = await this.appConnectionChecker.ensureConnection()
if (!isConnected) {
console.groupEnd()
next({
path: PAGES.NOT_APP,
query: {
message: 'Please open this page inside the official app',
},
})
return
}
if (!permissionManager.isLoaded()) {
const userId = getUserId()
if (!userId) {
console.groupEnd()
next(PAGES.APPLY)
return
}
await this.identityChecker.checkUserIdentity(userId)
}
if (permissionManager.hasPagePermission(to.path)) {
console.groupEnd()
next()
return
}
const defaultPage = permissionManager.getDefaultPage()
console.groupEnd()
next(defaultPage)
} catch (error) {
console.error('Route guard error:', error)
console.groupEnd()
const message = String(error?.message || '').toLowerCase()
if (message.includes('connection') || message.includes('app required')) {
next(PAGES.NOT_APP)
return
}
next(PAGES.APPLY)
}
}
isPublicPage(path) {
return isPublicPath(path)
}
async checkAndRedirect(router, userId) {
try {
const primaryRole = await this.identityChecker.checkUserIdentity(userId)
const currentPath = router.currentRoute.value.path
if (!permissionManager.hasPagePermission(currentPath)) {
await this.pageRedirector.redirectToCorrectPage(router, currentPath, currentPath)
}
return primaryRole
} catch (error) {
console.error('Manual check and redirect failed:', error)
throw error
}
}
reset() {
this.appConnectionChecker.reset()
this.identityChecker.reset()
permissionManager.reset()
console.debug('Route guard reset')
}
}
export const routeGuard = new RouteGuard()
export const installRouteGuard = (router) => routeGuard.install(router)
export const checkAndRedirect = (router, userId) => routeGuard.checkAndRedirect(router, userId)
export const resetRouteGuard = () => routeGuard.reset()