+
![]()
diff --git a/src/config/imagePaths.js b/src/config/imagePaths.js
index 712ced4..dfef6f9 100644
--- a/src/config/imagePaths.js
+++ b/src/config/imagePaths.js
@@ -1,14 +1,22 @@
-// src/config/imagePaths.js
+import { protectAssetUrl } from '@/utils/protectedAssets.js'
+
export const OSS_BASE_URL =
process.env.NODE_ENV === 'development'
- ? '/oss/h5/Azizi/' // 开发环境使用代理
- : 'https://tkm-likei.oss-ap-southeast-1.aliyuncs.com/h5/Azizi/' // 生产环境直接访问
+ ? '/oss/h5/Azizi/'
+ : 'https://tkm-likei.oss-ap-southeast-1.aliyuncs.com/h5/Azizi/'
+
+function buildAssetUrl(imagePath, filename, extension) {
+ return `${OSS_BASE_URL}${imagePath}${filename}.${extension}`
+}
+
+export const getRawPngUrl = (imagePath, filename) => buildAssetUrl(imagePath, filename, 'png')
+
+export const getRawWebpUrl = (imagePath, filename) => buildAssetUrl(imagePath, filename, 'webp')
-// 组合使用示例
export const getPngUrl = (imagePath, filename) => {
- return `${OSS_BASE_URL}${imagePath}${filename}.png`
+ return protectAssetUrl(getRawPngUrl(imagePath, filename), imagePath)
}
export const getWebpUrl = (imagePath, filename) => {
- return `${OSS_BASE_URL}${imagePath}${filename}.webp`
+ return protectAssetUrl(getRawWebpUrl(imagePath, filename), imagePath)
}
diff --git a/src/config/security.js b/src/config/security.js
new file mode 100644
index 0000000..2eea29d
--- /dev/null
+++ b/src/config/security.js
@@ -0,0 +1,54 @@
+export const PUBLIC_PATHS = Object.freeze([
+ '/',
+ '/loading',
+ '/about',
+ '/apply',
+ '/not_app',
+ '/map',
+ '/404',
+ '/error',
+ '/top-list',
+ '/weekly-star',
+ '/ranking',
+ '/games-king',
+ '/couple',
+ '/invitation-to-register',
+ '/invitation/invite-new-user',
+ '/recharge-reward',
+ '/login-reward',
+ '/activities/lucky-dollars-season4',
+ '/activities/lesser-bairam',
+ '/activities/lucky-dollars-season3',
+ '/activities/spring-festival',
+ '/recharge',
+ '/recharge-pay-way',
+ '/recharge-guide',
+ '/recharge-agency-recruit',
+ '/pay-result',
+ '/search-payee',
+ '/cash-withdraw',
+ '/cash-out',
+ '/cash-out-details',
+ '/KYC',
+ '/bank-card',
+ '/good-ID',
+])
+
+export function normalizePath(path = '') {
+ const normalizedPath = String(path || '/').split('?')[0].trim()
+
+ if (!normalizedPath) {
+ return '/'
+ }
+
+ return normalizedPath.startsWith('/') ? normalizedPath : `/${normalizedPath}`
+}
+
+export function isPublicPath(path = '') {
+ return PUBLIC_PATHS.includes(normalizePath(path))
+}
+
+export function getCurrentHashPath() {
+ const hashPath = window.location.hash.replace(/^#/, '')
+ return normalizePath(hashPath || '/')
+}
diff --git a/src/directives/smartImage.js b/src/directives/smartImage.js
index 326cf92..7bf4f1a 100644
--- a/src/directives/smartImage.js
+++ b/src/directives/smartImage.js
@@ -1,68 +1,54 @@
-// src/directives/smartImage.js
-import { imageCacheManager } from '@/utils/image/imageCacheManager'
import { nextTick } from 'vue'
+import { imageCacheManager } from '@/utils/image/imageCacheManager'
+import { resolveProtectedAssetUrl } from '@/utils/protectedAssets.js'
export const smartImage = {
async mounted(el, binding) {
- // 检查是否需要跳过渐变效果
const skipFade = binding.modifiers.noFade || binding.arg === 'noFade'
- // 设置加载状态样式
if (!skipFade) {
el.style.opacity = '0'
el.style.transition = 'opacity 0.3s ease-in-out'
}
- // 获取 URL:优先使用 binding.value,如果为空则尝试从元素获取
const url = getValidUrl(binding.value, el)
if (url) {
await processImage(el, url, skipFade)
-
- // 保存当前处理的 URL,用于后续比较
el._smartImageCurrentUrl = url
- // 保存原始绑定值
el._smartImageOriginalBindingValue = binding.value
- } else {
- // 如果 URL 无效,恢复透明度
- if (!skipFade) {
- el.style.opacity = '1'
- }
+ return
+ }
+
+ if (!skipFade) {
+ el.style.opacity = '1'
}
},
async updated(el, binding) {
- // 检查是否需要跳过渐变效果
const skipFade = binding.modifiers.noFade || binding.arg === 'noFade'
-
- // 获取当前有效的 URL
const currentUrl = getValidUrl(binding.value, el)
-
- // 检查是否真的需要更新
- // 1. binding.value 发生了变化
- // 2. 或者元素的 src 属性发生了变化
const bindingChanged = binding.value !== binding.oldValue
const elementSrcChanged = currentUrl && currentUrl !== el._smartImageCurrentUrl
- if (bindingChanged || elementSrcChanged) {
- if (currentUrl && currentUrl !== el._smartImageCurrentUrl) {
- // 设置加载状态
- if (!skipFade) {
- el.style.opacity = '0'
- }
- await processImage(el, currentUrl, skipFade)
- el._smartImageCurrentUrl = currentUrl
- el._smartImageOriginalBindingValue = binding.value
+ if (!bindingChanged && !elementSrcChanged) {
+ return
+ }
+
+ if (currentUrl && currentUrl !== el._smartImageCurrentUrl) {
+ if (!skipFade) {
+ el.style.opacity = '0'
}
+
+ await processImage(el, currentUrl, skipFade)
+ el._smartImageCurrentUrl = currentUrl
+ el._smartImageOriginalBindingValue = binding.value
}
},
- // 清理函数
unmounted(el) {
- // 清理可能的事件监听器
el.removeEventListener('load', el._smartImageLoadHandler)
el.removeEventListener('error', el._smartImageErrorHandler)
- // 清理保存的引用
delete el._smartImageCurrentUrl
delete el._smartImageOriginalBindingValue
delete el._smartImageLoadHandler
@@ -70,9 +56,7 @@ export const smartImage = {
},
}
-// 获取有效的 URL
function getValidUrl(bindingValue, el) {
- // 优先级:1. binding.value 2. el.getAttribute('src') 3. el.src
const candidates = [bindingValue, el.getAttribute('src'), el.src]
for (const candidate of candidates) {
@@ -90,7 +74,6 @@ function getValidUrl(bindingValue, el) {
return null
}
-// 将图片处理逻辑提取为单独函数
async function processImage(el, url, skipFade = false) {
if (!url) {
return
@@ -99,35 +82,37 @@ async function processImage(el, url, skipFade = false) {
await nextTick()
try {
- // 使用调度器统一管理图片请求,避免重复请求同一 URL
- const imageUrl = await imageCacheManager.scheduleRequest(url)
+ const resolvedUrl = resolveProtectedAssetUrl(url)
+ const imageUrl = await imageCacheManager.scheduleRequest(resolvedUrl)
- // 设置加载完成的回调
const handleLoad = () => {
if (!skipFade) {
el.style.opacity = '1'
}
+
el.removeEventListener('load', handleLoad)
}
+
el.addEventListener('load', handleLoad)
el._smartImageLoadHandler = handleLoad
- // 设置错误处理回调
const handleError = (error) => {
- console.error(`图片加载失败:`, url, error)
+ console.error('Image load failed:', resolvedUrl, error)
if (!skipFade) {
el.style.opacity = '1'
}
+
el.removeEventListener('error', handleError)
}
+
el.addEventListener('error', handleError)
el._smartImageErrorHandler = handleError
-
- // 设置图片源
el.src = imageUrl
} catch (error) {
- console.warn(`图片处理失败: ${url}`, error)
- el.src = url
+ const fallbackUrl = resolveProtectedAssetUrl(url)
+ console.warn(`Image processing failed: ${fallbackUrl}`, error)
+ el.src = fallbackUrl
+
if (!skipFade) {
el.style.opacity = '1'
}
diff --git a/src/main.js b/src/main.js
index 63b2eee..903abff 100644
--- a/src/main.js
+++ b/src/main.js
@@ -4,6 +4,8 @@ import './styles/fonts.css'
import './styles/global.css' // 引入全局样式
import { logEnvInfo } from './utils/env.js'
+import { installProtectedAssetRuntime } from './utils/protectedAssetRuntime.js'
+import { installRuntimeSecurity } from './utils/runtimeSecurity.js'
import { versionChecker } from './utils/versionChecker.js'
import { smartImage } from './directives/smartImage.js'
import { createPinia } from 'pinia'
@@ -20,6 +22,9 @@ import piniaPluginPersistedstate from 'pinia-plugin-persistedstate'
logEnvInfo()
// 检查版本更新(首先判断执行一次对缓存的操作,最后返回检测结果)
+installRuntimeSecurity()
+installProtectedAssetRuntime()
+
if (await versionChecker.checkVersion()) {
console.log('版本已更新')
}
diff --git a/src/utils/http.js b/src/utils/http.js
index a42839b..62bb030 100644
--- a/src/utils/http.js
+++ b/src/utils/http.js
@@ -1,5 +1,7 @@
// HTTP请求配置
import { getApiBaseUrl, getCurrentEnv, getUserAuth, isDebugMode } from './env.js'
+import { isAppConnected } from './appConnectionManager.js'
+import { getCurrentHashPath, isPublicPath } from '@/config/security.js'
/**
* 自定义API错误类
@@ -112,6 +114,18 @@ export { COMMON_HEADERS, ApiError }
export async function request(url, options = {}) {
const { method = 'GET', data = null, headers = {}, ...otherOptions } = options
const fullUrl = url.startsWith('http') ? url : `${API_BASE_URL}${url}`
+ const currentPath = typeof window !== 'undefined' ? getCurrentHashPath() : '/'
+ const hasAppBridge =
+ typeof window !== 'undefined' &&
+ Boolean(window.app || window.webkit || window.FlutterPageControl)
+
+ if (!DEBUG_MODE && !isPublicPath(currentPath) && (!hasAppBridge || !isAppConnected())) {
+ throw new ApiError('Protected API access is only allowed inside the official app', {
+ type: 'security',
+ status: 403,
+ errorCodeName: 'APP_REQUIRED',
+ })
+ }
const config = {
method,
diff --git a/src/utils/image/imageCacheManager.js b/src/utils/image/imageCacheManager.js
index d9cb9c3..a75412e 100644
--- a/src/utils/image/imageCacheManager.js
+++ b/src/utils/image/imageCacheManager.js
@@ -1,5 +1,6 @@
// src/utils/imageCacheManager.js
import { IMAGE_CACHE_VERSION } from '@/config/imageCacheVersion'
+import { resolveProtectedAssetUrl } from '@/utils/protectedAssets.js'
import { useDebounce } from '@/utils/useDebounce'
class ImageCacheManager {
@@ -311,15 +312,17 @@ class ImageCacheManager {
// 规范化 URL 以确保一致性
normalizeUrl(url) {
try {
+ const resolvedUrl = resolveProtectedAssetUrl(url)
+
// 如果是相对路径,转换为完整 URL 进行匹配
- if (url.startsWith('/')) {
- return new URL(url, window.location.origin).toString()
+ if (resolvedUrl.startsWith('/')) {
+ return new URL(resolvedUrl, window.location.origin).toString()
}
// 如果已经是完整 URL,直接返回
- return new URL(url).toString()
+ return new URL(resolvedUrl).toString()
} catch (e) {
// 如果 URL 格式不正确,返回原 URL
- return url
+ return resolveProtectedAssetUrl(url)
}
}
diff --git a/src/utils/protectedAssetRuntime.js b/src/utils/protectedAssetRuntime.js
new file mode 100644
index 0000000..ab45e9c
--- /dev/null
+++ b/src/utils/protectedAssetRuntime.js
@@ -0,0 +1,87 @@
+import { imageCacheManager } from '@/utils/image/imageCacheManager'
+import { isProtectedAssetUrl, resolveProtectedAssetUrl } from '@/utils/protectedAssets.js'
+
+const PROTECTED_SELECTOR = 'img[src^="likei-protected:"]'
+const PROTECTED_SOURCE_ATTR = 'data-protected-src'
+
+async function resolveImageElement(img) {
+ const originalSrc = img.getAttribute('src') || ''
+
+ if (!isProtectedAssetUrl(originalSrc)) {
+ return
+ }
+
+ if (img.getAttribute(PROTECTED_SOURCE_ATTR) === originalSrc) {
+ return
+ }
+
+ img.setAttribute(PROTECTED_SOURCE_ATTR, originalSrc)
+ img.setAttribute('draggable', 'false')
+
+ try {
+ const resolvedUrl = resolveProtectedAssetUrl(originalSrc)
+ const objectUrl = await imageCacheManager.scheduleRequest(resolvedUrl)
+
+ if (img.getAttribute(PROTECTED_SOURCE_ATTR) !== originalSrc) {
+ return
+ }
+
+ img.src = objectUrl
+ } catch (error) {
+ console.warn('Failed to resolve protected image element:', error)
+ img.src = resolveProtectedAssetUrl(originalSrc)
+ }
+}
+
+function scanProtectedImages(rootNode = document) {
+ if (!rootNode?.querySelectorAll) {
+ return
+ }
+
+ rootNode.querySelectorAll(PROTECTED_SELECTOR).forEach((img) => {
+ void resolveImageElement(img)
+ })
+}
+
+function handleMutationRecord(record) {
+ if (record.type === 'attributes' && record.target instanceof HTMLImageElement) {
+ void resolveImageElement(record.target)
+ return
+ }
+
+ record.addedNodes.forEach((node) => {
+ if (!(node instanceof HTMLElement)) {
+ return
+ }
+
+ if (node instanceof HTMLImageElement) {
+ void resolveImageElement(node)
+ return
+ }
+
+ scanProtectedImages(node)
+ })
+}
+
+export function installProtectedAssetRuntime() {
+ scanProtectedImages()
+
+ const observer = new MutationObserver((records) => {
+ records.forEach(handleMutationRecord)
+ })
+
+ observer.observe(document.documentElement, {
+ subtree: true,
+ childList: true,
+ attributes: true,
+ attributeFilter: ['src'],
+ })
+
+ window.addEventListener(
+ 'hashchange',
+ () => {
+ scanProtectedImages()
+ },
+ { passive: true },
+ )
+}
diff --git a/src/utils/protectedAssets.js b/src/utils/protectedAssets.js
new file mode 100644
index 0000000..59a68a5
--- /dev/null
+++ b/src/utils/protectedAssets.js
@@ -0,0 +1,66 @@
+const PROTECTED_ASSET_PREFIX = 'likei-protected:'
+const PROTECTED_PATH_PREFIXES = ['Activities/', 'Ranking/']
+const NON_PROTECTED_MODES = new Set(['development', 'test'])
+const CURRENT_MODE = import.meta.env.VITE_NODE_ENV || import.meta.env.MODE || 'development'
+
+function encodeBase64(value) {
+ if (typeof globalThis.btoa === 'function') {
+ return globalThis.btoa(unescape(encodeURIComponent(value)))
+ }
+
+ if (typeof globalThis.Buffer !== 'undefined') {
+ return globalThis.Buffer.from(value, 'utf-8').toString('base64')
+ }
+
+ throw new Error('Base64 encoder is not available in the current runtime')
+}
+
+function decodeBase64(value) {
+ if (typeof globalThis.atob === 'function') {
+ return decodeURIComponent(escape(globalThis.atob(value)))
+ }
+
+ if (typeof globalThis.Buffer !== 'undefined') {
+ return globalThis.Buffer.from(value, 'base64').toString('utf-8')
+ }
+
+ throw new Error('Base64 decoder is not available in the current runtime')
+}
+
+export function shouldProtectAssetPath(assetPath = '') {
+ if (!assetPath || NON_PROTECTED_MODES.has(CURRENT_MODE)) {
+ return false
+ }
+
+ return PROTECTED_PATH_PREFIXES.some((prefix) => assetPath.startsWith(prefix))
+}
+
+export function protectAssetUrl(url = '', assetPath = '') {
+ if (!url || !shouldProtectAssetPath(assetPath)) {
+ return url
+ }
+
+ return `${PROTECTED_ASSET_PREFIX}${encodeBase64(url)}`
+}
+
+export function isProtectedAssetUrl(url = '') {
+ return typeof url === 'string' && url.startsWith(PROTECTED_ASSET_PREFIX)
+}
+
+export function resolveProtectedAssetUrl(url = '') {
+ if (!isProtectedAssetUrl(url)) {
+ return url
+ }
+
+ try {
+ return decodeBase64(url.slice(PROTECTED_ASSET_PREFIX.length))
+ } catch (error) {
+ console.warn('Failed to decode protected asset url:', error)
+ return url
+ }
+}
+
+export function toCssBackgroundImage(url = '') {
+ const resolvedUrl = resolveProtectedAssetUrl(url)
+ return resolvedUrl ? `url(${resolvedUrl})` : 'none'
+}
diff --git a/src/utils/routeGuard.js b/src/utils/routeGuard.js
index 1d90fe1..eaf16e3 100644
--- a/src/utils/routeGuard.js
+++ b/src/utils/routeGuard.js
@@ -1,177 +1,122 @@
/**
- * 路由守卫
- * 提供APP连接检查、页面访问权限控制和自动重定向功能
+ * Route guard for:
+ * 1. ensuring app bridge connection
+ * 2. loading identity information
+ * 3. redirecting users to pages they can access
*/
-import { permissionManager, PAGES, USER_ROLES } from './permissionManager.js'
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,
- isInApp,
} from './appBridge.js'
import { appConnectionManager, isAppConnected } from './appConnectionManager.js'
-/**
- * APP连接检查器
- */
class AppConnectionChecker {
- constructor() {
- this.isConnecting = false
- }
-
- /**
- * 确保APP连接
- * @returns {Promise
} 连接是否成功
- */
async ensureConnection() {
- console.debug('🔗 Checking APP connection...')
+ console.debug('Checking APP connection...')
- // 如果不在APP环境中,直接返回成功
-
- console.log('我是app环境', window.app)
- console.log('我是app环境', window.webkit)
- console.log('我是app环境', window.FlutterPageControl)
if (!isInApp()) {
- console.debug('🌐 Browser environment, no connection needed')
+ 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')
+ console.debug('APP already connected')
return true
}
- // 如果正在连接中,等待现有连接
if (appConnectionManager.isConnecting()) {
- console.debug('⏳ Connection already in progress, waiting...')
+ console.debug('Connection already in progress, waiting...')
try {
await appConnectionManager.getConnectionPromise()
return isAppConnected()
} catch (error) {
- console.error('❌ Connection wait failed:', error)
+ console.error('Connection wait failed:', error)
return false
}
}
- // 开始新的连接
- return await this._performConnection()
+ return this._performConnection()
}
- /**
- * 执行APP连接
- * @returns {Promise}
- */
async _performConnection() {
try {
- console.debug('📱 Starting new APP connection...')
-
const connectionPromise = new Promise((resolve, reject) => {
connectApplication(async (access) => {
try {
const result = parseAccessOrigin(access)
- if (result.success) {
- // 解析头部信息
- const headerInfo = parseHeader(result.data)
-
- // 设置HTTP请求头
- await setHttpHeaders(headerInfo)
-
- // 更新连接状态
- appConnectionManager.setConnected(headerInfo)
-
- console.debug('🎉 APP connection established successfully')
-
- // APP连接成功后立即获取用户信息
- await this._fetchUserInfoAfterConnection()
-
- resolve(true)
- } else {
- console.error('❌ Failed to parse access origin:', result.error)
+ if (!result.success) {
appConnectionManager.reset()
- reject(new Error(result.error))
+ 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) {
- console.error('❌ Connection process failed:', error)
appConnectionManager.reset()
reject(error)
}
})
})
- // 设置连接状态
appConnectionManager.setConnecting(connectionPromise)
-
- // 等待连接完成
return await connectionPromise
} catch (error) {
- console.error('❌ APP connection failed:', error)
+ console.error('APP connection failed:', error)
appConnectionManager.reset()
return false
}
}
- /**
- * APP连接成功后获取用户信息
- * @private
- */
async _fetchUserInfoAfterConnection() {
try {
- console.debug('👤 Fetching user info after connection...')
-
const response = await getMemberProfile()
if (response && response.status && response.body) {
- // 缓存用户信息
- console.debug('✅ User info cached successfully')
setUserInfo(response.body, null)
- } else {
- console.warn('⚠️ Failed to get team entry or invalid response')
}
} catch (error) {
- // 这里不抛出错误,因为APP连接已经成功
- // 用户信息获取失败可以在后续的身份检查中处理
- console.warn('⚠️ Failed to fetch user info after connection:', error)
+ console.warn('Failed to fetch user info after connection:', error)
}
}
- /**
- * 重置连接状态
- */
reset() {
appConnectionManager.reset()
}
}
-/**
- * 身份检查器
- */
class IdentityChecker {
constructor() {
this.isChecking = false
this.checkPromise = null
}
- /**
- * 检查并设置用户身份
- * @param {string} userId - 用户ID
- * @returns {Promise} 返回主要身份
- */
async checkUserIdentity(userId) {
if (!userId) {
- console.warn('⚠️ No user ID provided')
permissionManager.setUserIdentity(null)
return USER_ROLES.GUEST
}
- // 如果正在检查中,等待现有的检查完成
if (this.isChecking && this.checkPromise) {
- console.debug('⏳ Identity check already in progress, waiting...')
return await this.checkPromise
}
@@ -179,122 +124,61 @@ class IdentityChecker {
this.checkPromise = this._performIdentityCheck(userId)
try {
- const primaryRole = await this.checkPromise
- return primaryRole
+ return await this.checkPromise
} finally {
this.isChecking = false
this.checkPromise = null
}
}
- /**
- * 执行实际的身份检查
- * @param {string} userId - 用户ID
- * @returns {Promise} 主要身份
- */
async _performIdentityCheck(userId) {
try {
- console.debug('🔍 Checking user identity for:', userId)
-
+ console.debug('Checking user identity for:', userId)
const response = await getUserIdentity()
if (response && response.status && response.body) {
- const identity = response.body
- console.debug('✅ Identity fetched:', identity)
-
- // 设置权限信息
- permissionManager.setUserIdentity(identity)
-
+ permissionManager.setUserIdentity(response.body)
return permissionManager.getPrimaryRole()
- } else {
- console.warn('⚠️ Invalid identity response')
- permissionManager.setUserIdentity(null)
- return USER_ROLES.GUEST
}
+
+ permissionManager.setUserIdentity(null)
+ return USER_ROLES.GUEST
} catch (error) {
- console.error('❌ Failed to fetch user identity:', 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 {
- /**
- * 根据身份自动重定向到正确页面
- * @param {Object} router - Vue Router 实例
- * @param {string} targetPath - 目标页面路径
- * @param {string} currentPath - 当前页面路径
- * @returns {Promise} 是否进行了重定向
- */
async redirectToCorrectPage(router, targetPath, currentPath) {
- const primaryRole = permissionManager.getPrimaryRole()
-
- // 检查目标页面权限
const hasPermission = permissionManager.hasPagePermission(targetPath)
- console.debug('🔄 Redirect check:', {
- targetPath,
- currentPath,
- primaryRole,
- hasPermission,
- })
-
if (hasPermission) {
- // 有权限访问目标页面
- if (targetPath !== currentPath) {
- console.debug('✅ Access granted, navigating to:', targetPath)
- return false // 不需要重定向,让路由正常进行
- }
- return false
- } else {
- // 无权限访问,重定向到默认页面
- const defaultPage = permissionManager.getDefaultPage()
-
- console.debug('❌ Access denied, redirecting to:', defaultPage)
-
- if (defaultPage !== currentPath) {
- await router.replace(defaultPage)
- return true // 已进行重定向
- }
return false
}
- }
- /**
- * 处理越权访问
- * @param {Object} router - Vue Router 实例
- * @param {string} attemptedPage - 尝试访问的页面
- */
- async handleUnauthorizedAccess(router, attemptedPage) {
- const primaryRole = permissionManager.getPrimaryRole()
const defaultPage = permissionManager.getDefaultPage()
- console.warn('🚫 Unauthorized access attempt:', {
- attemptedPage,
- primaryRole,
- redirectingTo: defaultPage,
- })
+ if (defaultPage !== currentPath) {
+ await router.replace(defaultPage)
+ return true
+ }
- // 重定向到用户有权限的默认页面
- await router.replace(defaultPage)
+ return false
+ }
+
+ async handleUnauthorizedAccess(router) {
+ await router.replace(permissionManager.getDefaultPage())
}
}
-/**
- * 路由守卫类
- */
class RouteGuard {
constructor() {
this.appConnectionChecker = new AppConnectionChecker()
@@ -303,192 +187,104 @@ class RouteGuard {
this.isGuardActive = false
}
- /**
- * 安装路由守卫
- * @param {Object} router - Vue Router 实例
- */
install(router) {
router.beforeEach(async (to, from, next) => {
await this.handleRouteChange(to, from, next)
})
this.isGuardActive = true
- console.debug('🛡️ Route guard installed')
+ console.debug('Route guard installed')
}
- /**
- * 处理路由变化
- * @param {Object} to - 目标路由
- * @param {Object} from - 来源路由
- * @param {Function} next - 路由继续函数
- */
async handleRouteChange(to, from, next) {
- console.group('🛡️ Route Guard Check')
- console.debug('📍 Route change:', from.path, '→', to.path)
+ console.group('Route Guard Check')
+ console.debug('Route change:', from.path, '->', to.path)
try {
- // 1. 检查是否为公共页面(无需任何验证)
- if (this.isPublicPage(to.path)) {
- console.debug('🌐 Public page, allowing access')
+ if (to.meta?.isPublic || this.isPublicPage(to.path)) {
console.groupEnd()
next()
return
}
- // 2. 确保APP连接
- console.debug('🔗 Ensuring APP connection...')
const isConnected = await this.appConnectionChecker.ensureConnection()
if (!isConnected) {
- console.error('❌ APP connection failed, redirecting to error page')
console.groupEnd()
- next(PAGES.NOT_APP)
+ next({
+ path: PAGES.NOT_APP,
+ query: {
+ message: 'Please open this page inside the official app',
+ },
+ })
return
}
- // 3. 检查用户身份是否已加载
if (!permissionManager.isLoaded()) {
- console.debug('🔍 Identity not loaded, checking...')
-
const userId = getUserId()
+
if (!userId) {
- console.debug('❌ No user ID available, redirecting to apply')
console.groupEnd()
next(PAGES.APPLY)
return
}
- // 检查用户身份
await this.identityChecker.checkUserIdentity(userId)
}
- // 4. 检查页面访问权限
- const hasPermission = permissionManager.hasPagePermission(to.path)
-
- if (hasPermission) {
- console.debug('✅ Permission granted, allowing access')
+ if (permissionManager.hasPagePermission(to.path)) {
console.groupEnd()
next()
- } else {
- console.debug('❌ Permission denied')
- const defaultPage = permissionManager.getDefaultPage()
- console.debug('🔄 Redirecting to default page:', defaultPage)
- console.groupEnd()
- next(defaultPage)
+ return
}
+
+ const defaultPage = permissionManager.getDefaultPage()
+ console.groupEnd()
+ next(defaultPage)
} catch (error) {
- console.error('❌ Route guard error:', error)
+ console.error('Route guard error:', error)
console.groupEnd()
- // 根据错误类型决定重定向目标
- if (error.message && error.message.includes('connection')) {
- next(PAGES.NOT_APP) // 连接相关错误
- } else {
- next(PAGES.APPLY) // 其他错误
+ const message = String(error?.message || '').toLowerCase()
+ if (message.includes('connection') || message.includes('app required')) {
+ next(PAGES.NOT_APP)
+ return
}
+
+ next(PAGES.APPLY)
}
}
- /**
- * 检查是否为公共页面(无需APP连接和权限验证)
- * @param {string} path - 页面路径
- * @returns {boolean}
- */
isPublicPage(path) {
- //排行榜
- const RANKING_PAGES = [
- '/top-list', //排行榜
- '/weekly-star', //每周明星
- '/ranking', //总排行榜
- '/games-king', //游戏排行榜
- '/couple', //情侣排行榜
- ]
-
- // 活动页面
- const ACTIVITIES = [
- '/invitation-to-register', //邀请新用户注册页面
- '/invitation/invite-new-user', //邀请新用户
- '/recharge-reward', //充值奖励
- '/login-reward', // 登录奖励
-
- '/activities/lucky-dollars-season4', // 幸运美金活动
- '/activities/lesser-bairam', // 开斋节打榜
- '/activities/lucky-dollars-season3', // 斋月打榜
- '/activities/spring-festival', // 春节打榜
- ]
-
- const publicPages = [
- PAGES.APPLY, // 申请页面 - 重要:申请页面不需要权限检查
- PAGES.NOT_APP, // 错误页面 - APP连接失败时的页面
- PAGES.MAP, // 地图选择页面
- ...RANKING_PAGES, //排行榜
- ...ACTIVITIES, // 活动页面
- '/404', // 404页面
- '/error', // 通用错误页面
-
- '/recharge', //搜索充值对象页面
- '/recharge-pay-way', //充值方式页面
- '/recharge-guide', //充值引导页面
- '/recharge-agency-recruit', //充值代理介绍页面
-
- '/pay-result', //支付结果页
- '/search-payee', //搜索收款人
- '/cash-withdraw', // 提现页面
- '/cash-out', // 活动提现页面
- '/cash-out-details', //提现详情页面
- '/KYC', //提现资料提交
- '/bank-card', //选择银行卡
-
- '/good-ID', //靓号申请页
- ]
- return publicPages.includes(path)
+ return isPublicPath(path)
}
- /**
- * 手动触发身份检查和重定向
- * @param {Object} router - Vue Router 实例
- * @param {string} userId - 用户ID
- */
async checkAndRedirect(router, userId) {
try {
- console.debug('🔄 Manual identity check and redirect')
-
- // 检查身份
const primaryRole = await this.identityChecker.checkUserIdentity(userId)
-
- // 获取当前页面
const currentPath = router.currentRoute.value.path
- // 检查当前页面权限
- const hasPermission = permissionManager.hasPagePermission(currentPath)
-
- if (!hasPermission) {
- // 重定向到默认页面
+ if (!permissionManager.hasPagePermission(currentPath)) {
await this.pageRedirector.redirectToCorrectPage(router, currentPath, currentPath)
}
return primaryRole
} catch (error) {
- console.error('❌ Manual check and redirect failed:', 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')
+ 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()
diff --git a/src/utils/runtimeSecurity.js b/src/utils/runtimeSecurity.js
new file mode 100644
index 0000000..c2fe39a
--- /dev/null
+++ b/src/utils/runtimeSecurity.js
@@ -0,0 +1,130 @@
+import { isDebugMode } from './env.js'
+import { getCurrentHashPath, isPublicPath } from '@/config/security.js'
+
+const DEVTOOLS_WIDTH_THRESHOLD = 160
+const SECURITY_REDIRECT = '/#/not_app?message=Please%20open%20this%20page%20inside%20the%20official%20app'
+
+function isEditableElement(target) {
+ return Boolean(
+ target?.closest?.(
+ 'input, textarea, select, [contenteditable=""], [contenteditable="true"], [data-allow-browser-tools]',
+ ),
+ )
+}
+
+function shouldProtectCurrentPage() {
+ return !isPublicPath(getCurrentHashPath())
+}
+
+function hasAppBridge() {
+ return Boolean(window.app || window.webkit || window.FlutterPageControl)
+}
+
+function isSuspiciousAutomation() {
+ const ua = navigator.userAgent || ''
+
+ return Boolean(
+ navigator.webdriver ||
+ window.__playwright__binding__ ||
+ window.__nightmare ||
+ window.callPhantom ||
+ /HeadlessChrome|PhantomJS|puppeteer|Playwright/i.test(ua),
+ )
+}
+
+function isDevtoolsOpen() {
+ return (
+ window.outerWidth - window.innerWidth > DEVTOOLS_WIDTH_THRESHOLD ||
+ window.outerHeight - window.innerHeight > DEVTOOLS_WIDTH_THRESHOLD
+ )
+}
+
+function redirectToNotApp() {
+ if (window.location.hash.startsWith('#/not_app')) {
+ return
+ }
+
+ window.location.replace(SECURITY_REDIRECT)
+}
+
+function guardShortcut(event) {
+ if (!shouldProtectCurrentPage() || isEditableElement(event.target)) {
+ return
+ }
+
+ const key = event.key?.toLowerCase()
+ const withCtrlOrMeta = event.ctrlKey || event.metaKey
+ const withShift = event.shiftKey
+
+ if (
+ event.key === 'F12' ||
+ (withCtrlOrMeta && withShift && ['i', 'j', 'c'].includes(key)) ||
+ (withCtrlOrMeta && ['u', 's'].includes(key))
+ ) {
+ event.preventDefault()
+ event.stopPropagation()
+ }
+}
+
+function guardPointerEvent(event) {
+ if (!shouldProtectCurrentPage() || isEditableElement(event.target)) {
+ return
+ }
+
+ event.preventDefault()
+ event.stopPropagation()
+}
+
+function guardSelectionEvent(event) {
+ if (!shouldProtectCurrentPage() || isEditableElement(event.target)) {
+ return
+ }
+
+ event.preventDefault()
+}
+
+function syncSecurityState() {
+ const shouldBlur = shouldProtectCurrentPage() && !hasAppBridge() && isDevtoolsOpen()
+ document.documentElement.classList.toggle('security-devtools-open', shouldBlur)
+}
+
+function injectSecurityStyle() {
+ if (document.getElementById('runtime-security-style')) {
+ return
+ }
+
+ const style = document.createElement('style')
+ style.id = 'runtime-security-style'
+ style.textContent = `
+ html.security-devtools-open body {
+ filter: blur(10px);
+ pointer-events: none;
+ user-select: none;
+ }
+ `
+
+ document.head.appendChild(style)
+}
+
+export function installRuntimeSecurity() {
+ if (isDebugMode()) {
+ return
+ }
+
+ injectSecurityStyle()
+
+ if (shouldProtectCurrentPage() && isSuspiciousAutomation() && !hasAppBridge()) {
+ redirectToNotApp()
+ return
+ }
+
+ document.addEventListener('keydown', guardShortcut, true)
+ document.addEventListener('contextmenu', guardPointerEvent, true)
+ document.addEventListener('dragstart', guardPointerEvent, true)
+ document.addEventListener('copy', guardSelectionEvent, true)
+ document.addEventListener('selectstart', guardSelectionEvent, true)
+ window.addEventListener('resize', syncSecurityState, { passive: true })
+ window.addEventListener('hashchange', syncSecurityState, { passive: true })
+
+ syncSecurityState()
+}
diff --git a/src/views/Activities/LesserBairam/index.vue b/src/views/Activities/LesserBairam/index.vue
index 1ce6e1e..0d7b59d 100644
--- a/src/views/Activities/LesserBairam/index.vue
+++ b/src/views/Activities/LesserBairam/index.vue
@@ -4,7 +4,7 @@
-
+
{{ $t('loading') }}...
@@ -13,9 +13,9 @@
-
+
-
+
-
+
@@ -12,11 +15,14 @@
-
+
diff --git a/src/views/Activities/LuckyDollars/Season3/index.vue b/src/views/Activities/LuckyDollars/Season3/index.vue
index 6635986..ee1d316 100644
--- a/src/views/Activities/LuckyDollars/Season3/index.vue
+++ b/src/views/Activities/LuckyDollars/Season3/index.vue
@@ -4,7 +4,7 @@
-
+
{{ $t('loading') }}...
@@ -16,9 +16,9 @@
style="width: 100vw; min-height: 100vh; overflow: hidden; position: relative"
>
-
+
-
+
@@ -1913,6 +1913,7 @@ import { setDocumentDirection } from '@/locales/i18n'
import { viewUserInfo } from '@/utils/appBridge.js'
import { useLangStore } from '@/stores/lang'
import { getPngUrl } from '@/config/imagePaths.js'
+import { toCssBackgroundImage } from '@/utils/protectedAssets.js'
import { preloadImages } from '@/utils/image/imagePreloader.js'
import { setUserInfo } from '@/utils/userStore.js'
import { formatUTCCustom } from '@/utils/utcFormat.js'
diff --git a/src/views/Activities/LuckyDollars/Season4/index.vue b/src/views/Activities/LuckyDollars/Season4/index.vue
index 612c6dc..2cc4fa1 100644
--- a/src/views/Activities/LuckyDollars/Season4/index.vue
+++ b/src/views/Activities/LuckyDollars/Season4/index.vue
@@ -1,28 +1,29 @@
-
+
-
+
-
+
@@ -499,11 +500,11 @@
white-space: nowrap;
text-overflow: ellipsis;
"
- class="UserNickname"
+ class="s4-x3"
>
{{ listItem?.userName }}
-
+
{{ $t('user_id_prefix') }} {{ listItem?.specialAccount || listItem?.account }}
@@ -519,7 +520,7 @@
align-items: center;
"
>
-
+
${{ listItem?.amount }}
@@ -617,7 +618,7 @@
@@ -991,7 +992,7 @@
v-smart-img
src="/src/assets/icon/Azizi/helpWhite.png"
alt=""
- class="ruleBt"
+ class="s4-x5"
@click="openPopup('help')"
/>
@@ -1075,7 +1076,7 @@
{{ t('transfer_to_recharge_agent') }}

{{ t('exchange_gold_coins') }}

{{ t('go_to_withdraw') }}

{{ myRanking?.userName }}
@@ -1644,7 +1645,7 @@
+
+
diff --git a/src/views/Ranking/Couple/index.vue b/src/views/Ranking/Couple/index.vue
index 1a7a697..a326207 100644
--- a/src/views/Ranking/Couple/index.vue
+++ b/src/views/Ranking/Couple/index.vue
@@ -1,9 +1,10 @@
-
+
@@ -14,11 +15,12 @@
-
+
![]()
{
display: none;
}
-.topList {
+.cpl-x1 {
display: flex;
flex-direction: row-reverse;
align-items: center;
@@ -2164,7 +2166,7 @@ textarea::placeholder {
color: #ff79a1;
}
-.couple-item-info {
+.cpl-x2 {
flex: 1;
min-width: 0;
padding-left: 4vw;
@@ -2206,7 +2208,7 @@ textarea::placeholder {
justify-content: flex-start;
}
-[dir='rtl'] .couple-item-info {
+[dir='rtl'] .cpl-x2 {
padding-left: 0;
padding-right: 4vw;
}
diff --git a/src/views/Ranking/GamesKing/index.vue b/src/views/Ranking/GamesKing/index.vue
index e7b9b76..cdd07f0 100644
--- a/src/views/Ranking/GamesKing/index.vue
+++ b/src/views/Ranking/GamesKing/index.vue
@@ -4,7 +4,7 @@
-
+
-
+
@@ -14,7 +14,7 @@
-
+
@@ -374,7 +374,7 @@
margin: -10px 0;
padding: 10px 10%;
"
- :style="{ backgroundImage: `url(${imageUrl('border-item')})` }"
+ :style="{ backgroundImage: toCssBackgroundImage(imageUrl('border-item')) }"
>
@@ -787,6 +787,7 @@ import { useThrottle } from '@/utils/useDebounce'
import { connectToApp } from '@/utils/appConnector.js'
import { getPngUrl } from '@/config/imagePaths.js'
import { preloadImages } from '@/utils/image/imagePreloader.js'
+import { toCssBackgroundImage } from '@/utils/protectedAssets.js'
import { handleAvatarImageError, handleRewardImageError } from '@/utils/image/imageHandler.js'
import {
diff --git a/src/views/Ranking/Overall/Ranking.vue b/src/views/Ranking/Overall/Ranking.vue
index 5b7eea9..d230440 100644
--- a/src/views/Ranking/Overall/Ranking.vue
+++ b/src/views/Ranking/Overall/Ranking.vue
@@ -2,9 +2,9 @@
-
+
-
+
@@ -12,9 +12,10 @@
-
+
-
+
-
+
{{ t('ranking_weekly') }}
-
+
{{ t('ranking_monthly') }}
@@ -391,7 +392,7 @@