chore: prepare 0.1.127 release
This commit is contained in:
@@ -194,6 +194,7 @@ interface NavItem {
|
||||
icon: unknown
|
||||
iconSvg?: string
|
||||
hideInSimpleMode?: boolean
|
||||
requiresSuperAdmin?: boolean
|
||||
children?: NavItem[]
|
||||
/**
|
||||
* When true, the parent item only toggles the expand/collapse state and
|
||||
@@ -224,6 +225,22 @@ function applyFeatureFlags(items: NavItem[]): NavItem[] {
|
||||
return out
|
||||
}
|
||||
|
||||
function applyRoleVisibility(items: NavItem[]): NavItem[] {
|
||||
const out: NavItem[] = []
|
||||
for (const item of items) {
|
||||
if (item.requiresSuperAdmin && !authStore.isSuperAdmin) continue
|
||||
if (item.children) {
|
||||
const children = applyRoleVisibility(item.children)
|
||||
if (children.length > 0) {
|
||||
out.push({ ...item, children })
|
||||
}
|
||||
} else {
|
||||
out.push(item)
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
const route = useRoute()
|
||||
@@ -719,25 +736,26 @@ const adminNavItems = computed((): NavItem[] => {
|
||||
{ path: '/admin/dashboard', label: t('nav.dashboard'), icon: DashboardIcon },
|
||||
{ path: '/admin/ops', label: t('nav.ops'), icon: ChartIcon, featureFlag: flagOpsMonitoring },
|
||||
{ path: '/admin/users', label: t('nav.users'), icon: UsersIcon, hideInSimpleMode: true },
|
||||
{ path: '/admin/groups', label: t('nav.groups'), icon: FolderIcon, hideInSimpleMode: true },
|
||||
{ path: '/admin/groups', label: t('nav.groups'), icon: FolderIcon, hideInSimpleMode: true, requiresSuperAdmin: true },
|
||||
{
|
||||
path: '/admin/channels',
|
||||
label: t('nav.channelManagement'),
|
||||
icon: ChannelIcon,
|
||||
hideInSimpleMode: true,
|
||||
requiresSuperAdmin: true,
|
||||
expandOnly: true,
|
||||
children: [
|
||||
{ path: '/admin/channels/pricing', label: t('nav.channelPricing'), icon: PriceTagIcon },
|
||||
{ path: '/admin/channels/monitor', label: t('nav.channelMonitor'), icon: SignalIcon, featureFlag: flagChannelMonitor },
|
||||
],
|
||||
},
|
||||
{ path: '/admin/subscriptions', label: t('nav.subscriptions'), icon: CreditCardIcon, hideInSimpleMode: true },
|
||||
{ path: '/admin/accounts', label: t('nav.accounts'), icon: GlobeIcon },
|
||||
{ path: '/admin/subscriptions', label: t('nav.subscriptions'), icon: CreditCardIcon, hideInSimpleMode: true, requiresSuperAdmin: true },
|
||||
{ path: '/admin/accounts', label: t('nav.accounts'), icon: GlobeIcon, requiresSuperAdmin: true },
|
||||
{ path: '/admin/announcements', label: t('nav.announcements'), icon: BellIcon },
|
||||
{ path: '/admin/proxies', label: t('nav.proxies'), icon: ServerIcon },
|
||||
{ path: '/admin/proxies', label: t('nav.proxies'), icon: ServerIcon, requiresSuperAdmin: true },
|
||||
{ path: '/admin/risk-control', label: t('nav.riskControl'), icon: ShieldIcon, hideInSimpleMode: true, featureFlag: flagRiskControl },
|
||||
{ path: '/admin/redeem', label: t('nav.redeemCodes'), icon: TicketIcon, hideInSimpleMode: true },
|
||||
{ path: '/admin/promo-codes', label: t('nav.promoCodes'), icon: GiftIcon, hideInSimpleMode: true },
|
||||
{ path: '/admin/redeem', label: t('nav.redeemCodes'), icon: TicketIcon, hideInSimpleMode: true, requiresSuperAdmin: true },
|
||||
{ path: '/admin/promo-codes', label: t('nav.promoCodes'), icon: GiftIcon, hideInSimpleMode: true, requiresSuperAdmin: true },
|
||||
{
|
||||
path: '/admin/affiliates',
|
||||
label: t('nav.affiliateManagement'),
|
||||
@@ -767,20 +785,24 @@ const adminNavItems = computed((): NavItem[] => {
|
||||
{ path: '/admin/usage', label: t('nav.usage'), icon: ChartIcon }
|
||||
]
|
||||
|
||||
const visible = applyFeatureFlags(baseItems)
|
||||
const visible = applyRoleVisibility(applyFeatureFlags(baseItems))
|
||||
|
||||
// 简单模式下,在系统设置前插入 API密钥
|
||||
if (authStore.isSimpleMode) {
|
||||
const filtered = visible.filter(item => !item.hideInSimpleMode)
|
||||
filtered.push({ path: '/keys', label: t('nav.apiKeys'), icon: KeyIcon })
|
||||
filtered.push({ path: '/admin/settings', label: t('nav.settings'), icon: CogIcon })
|
||||
if (authStore.isSuperAdmin) {
|
||||
filtered.push({ path: '/admin/settings', label: t('nav.settings'), icon: CogIcon })
|
||||
}
|
||||
for (const cm of customMenuItemsForAdmin.value) {
|
||||
filtered.push({ path: `/custom/${cm.id}`, label: cm.label, icon: null, iconSvg: cm.icon_svg })
|
||||
}
|
||||
return filtered
|
||||
}
|
||||
|
||||
visible.push({ path: '/admin/settings', label: t('nav.settings'), icon: CogIcon })
|
||||
if (authStore.isSuperAdmin) {
|
||||
visible.push({ path: '/admin/settings', label: t('nav.settings'), icon: CogIcon })
|
||||
}
|
||||
for (const cm of customMenuItemsForAdmin.value) {
|
||||
visible.push({ path: `/custom/${cm.id}`, label: cm.label, icon: null, iconSvg: cm.icon_svg })
|
||||
}
|
||||
|
||||
@@ -50,6 +50,7 @@ vi.mock('@/api/auth', () => ({
|
||||
interface MockAuthState {
|
||||
isAuthenticated: boolean
|
||||
isAdmin: boolean
|
||||
isSuperAdmin?: boolean
|
||||
isSimpleMode: boolean
|
||||
backendModeEnabled: boolean
|
||||
hasPendingAuthSession: boolean
|
||||
@@ -65,6 +66,7 @@ function simulateGuard(
|
||||
): string | null {
|
||||
const requiresAuth = toMeta.requiresAuth !== false
|
||||
const requiresAdmin = toMeta.requiresAdmin === true
|
||||
const isSuperAdmin = authState.isSuperAdmin ?? authState.isAdmin
|
||||
|
||||
// 不需要认证的路由
|
||||
if (!requiresAuth) {
|
||||
@@ -108,6 +110,10 @@ function simulateGuard(
|
||||
return '/dashboard'
|
||||
}
|
||||
|
||||
if (toMeta.requiresSuperAdmin && !isSuperAdmin) {
|
||||
return '/admin/dashboard'
|
||||
}
|
||||
|
||||
// 简易模式限制
|
||||
if (authState.isSimpleMode) {
|
||||
const restrictedPaths = [
|
||||
@@ -249,6 +255,31 @@ describe('路由守卫逻辑', () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe('已认证 useradmin', () => {
|
||||
const authState: MockAuthState = {
|
||||
isAuthenticated: true,
|
||||
isAdmin: true,
|
||||
isSuperAdmin: false,
|
||||
isSimpleMode: false,
|
||||
backendModeEnabled: false,
|
||||
hasPendingAuthSession: false,
|
||||
}
|
||||
|
||||
it('访问管理页面允许通过', () => {
|
||||
const redirect = simulateGuard('/admin/users', { requiresAdmin: true }, authState)
|
||||
expect(redirect).toBeNull()
|
||||
})
|
||||
|
||||
it('访问仅超级管理员页面会重定向到后台首页', () => {
|
||||
const redirect = simulateGuard(
|
||||
'/admin/settings',
|
||||
{ requiresAdmin: true, requiresSuperAdmin: true },
|
||||
authState
|
||||
)
|
||||
expect(redirect).toBe('/admin/dashboard')
|
||||
})
|
||||
})
|
||||
|
||||
// --- 简易模式 ---
|
||||
|
||||
describe('简易模式受限路由', () => {
|
||||
|
||||
@@ -399,6 +399,7 @@ const routes: RouteRecordRaw[] = [
|
||||
meta: {
|
||||
requiresAuth: true,
|
||||
requiresAdmin: true,
|
||||
requiresSuperAdmin: true,
|
||||
title: 'Group Management',
|
||||
titleKey: 'admin.groups.title',
|
||||
descriptionKey: 'admin.groups.description'
|
||||
@@ -415,6 +416,7 @@ const routes: RouteRecordRaw[] = [
|
||||
meta: {
|
||||
requiresAuth: true,
|
||||
requiresAdmin: true,
|
||||
requiresSuperAdmin: true,
|
||||
title: 'Channel Management',
|
||||
titleKey: 'admin.channels.title',
|
||||
descriptionKey: 'admin.channels.description'
|
||||
@@ -427,6 +429,7 @@ const routes: RouteRecordRaw[] = [
|
||||
meta: {
|
||||
requiresAuth: true,
|
||||
requiresAdmin: true,
|
||||
requiresSuperAdmin: true,
|
||||
title: 'Channel Monitor',
|
||||
titleKey: 'admin.channelMonitor.title',
|
||||
descriptionKey: 'admin.channelMonitor.description'
|
||||
@@ -450,6 +453,7 @@ const routes: RouteRecordRaw[] = [
|
||||
meta: {
|
||||
requiresAuth: true,
|
||||
requiresAdmin: true,
|
||||
requiresSuperAdmin: true,
|
||||
title: 'Subscription Management',
|
||||
titleKey: 'admin.subscriptions.title',
|
||||
descriptionKey: 'admin.subscriptions.description'
|
||||
@@ -462,6 +466,7 @@ const routes: RouteRecordRaw[] = [
|
||||
meta: {
|
||||
requiresAuth: true,
|
||||
requiresAdmin: true,
|
||||
requiresSuperAdmin: true,
|
||||
title: 'Account Management',
|
||||
titleKey: 'admin.accounts.title',
|
||||
descriptionKey: 'admin.accounts.description'
|
||||
@@ -486,6 +491,7 @@ const routes: RouteRecordRaw[] = [
|
||||
meta: {
|
||||
requiresAuth: true,
|
||||
requiresAdmin: true,
|
||||
requiresSuperAdmin: true,
|
||||
title: 'Proxy Management',
|
||||
titleKey: 'admin.proxies.title',
|
||||
descriptionKey: 'admin.proxies.description'
|
||||
@@ -498,6 +504,7 @@ const routes: RouteRecordRaw[] = [
|
||||
meta: {
|
||||
requiresAuth: true,
|
||||
requiresAdmin: true,
|
||||
requiresSuperAdmin: true,
|
||||
title: 'Redeem Code Management',
|
||||
titleKey: 'admin.redeem.title',
|
||||
descriptionKey: 'admin.redeem.description'
|
||||
@@ -510,6 +517,7 @@ const routes: RouteRecordRaw[] = [
|
||||
meta: {
|
||||
requiresAuth: true,
|
||||
requiresAdmin: true,
|
||||
requiresSuperAdmin: true,
|
||||
title: 'Promo Code Management',
|
||||
titleKey: 'admin.promo.title',
|
||||
descriptionKey: 'admin.promo.description'
|
||||
@@ -522,6 +530,7 @@ const routes: RouteRecordRaw[] = [
|
||||
meta: {
|
||||
requiresAuth: true,
|
||||
requiresAdmin: true,
|
||||
requiresSuperAdmin: true,
|
||||
title: 'System Settings',
|
||||
titleKey: 'admin.settings.title',
|
||||
descriptionKey: 'admin.settings.description'
|
||||
@@ -765,13 +774,19 @@ router.beforeEach((to, _from, next) => {
|
||||
return
|
||||
}
|
||||
|
||||
// Check admin requirement (requires admin role, not useradmin)
|
||||
if (requiresAdmin && !authStore.isSuperAdmin) {
|
||||
// Check admin requirement (admin and useradmin can access admin routes)
|
||||
if (requiresAdmin && !authStore.isAdmin) {
|
||||
// User is authenticated but not admin, redirect to user dashboard
|
||||
next('/dashboard')
|
||||
return
|
||||
}
|
||||
|
||||
// Check full admin requirement (admin only, excluding useradmin)
|
||||
if (to.meta.requiresSuperAdmin && !authStore.isSuperAdmin) {
|
||||
next('/admin/dashboard')
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
// Check payment requirement (internal payment system only)
|
||||
if (to.meta.requiresPayment) {
|
||||
@@ -785,7 +800,7 @@ router.beforeEach((to, _from, next) => {
|
||||
if (to.meta.requiresRiskControl) {
|
||||
const riskControlEnabled = appStore.cachedPublicSettings?.risk_control_enabled === true
|
||||
if (!riskControlEnabled) {
|
||||
next(authStore.isAdmin ? '/admin/settings' : '/dashboard')
|
||||
next(authStore.isSuperAdmin ? '/admin/settings' : '/dashboard')
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
Vendored
+6
@@ -19,6 +19,12 @@ declare module 'vue-router' {
|
||||
*/
|
||||
requiresAdmin?: boolean
|
||||
|
||||
/**
|
||||
* Whether this route requires the full admin role, excluding useradmin
|
||||
* @default false
|
||||
*/
|
||||
requiresSuperAdmin?: boolean
|
||||
|
||||
/**
|
||||
* Page title for this route
|
||||
*/
|
||||
|
||||
@@ -84,7 +84,7 @@ export interface User {
|
||||
linuxdo_bound?: boolean
|
||||
oidc_bound?: boolean
|
||||
wechat_bound?: boolean
|
||||
role: 'admin' | 'user' // User role for authorization
|
||||
role: 'admin' | 'user' | 'useradmin' // User role for authorization
|
||||
balance: number // User balance for API usage
|
||||
concurrency: number // Allowed concurrent requests
|
||||
rpm_limit?: number // User-level RPM cap (0 = unlimited); effective as fallback when group has no rpm_limit
|
||||
|
||||
Reference in New Issue
Block a user