chore: prepare 0.1.127 release

This commit is contained in:
kone
2026-05-12 04:31:07 +08:00
parent 02006feeea
commit d81bc52547
16 changed files with 141 additions and 46 deletions
+31 -9
View File
@@ -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('简易模式受限路由', () => {
+18 -3
View File
@@ -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
}
}
+6
View File
@@ -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
*/
+1 -1
View File
@@ -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