291 lines
7.2 KiB
JavaScript
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()
|