From d81bc52547a1368c203627635ece799e79009050 Mon Sep 17 00:00:00 2001 From: kone Date: Tue, 12 May 2026 04:31:07 +0800 Subject: [PATCH] chore: prepare 0.1.127 release --- backend/cmd/server/VERSION | 2 +- backend/cmd/server/main.go | 16 -------- backend/cmd/server/wire_gen.go | 2 +- backend/internal/config/config.go | 9 +++++ backend/internal/repository/ent.go | 12 +++--- backend/internal/service/update_service.go | 31 ++++++++++---- backend/internal/service/wire.go | 4 +- deploy/.env.example | 4 ++ deploy/config.example.yaml | 3 ++ deploy/docker-compose.local.yml | 2 + deploy/docker-compose.yml | 2 + frontend/src/components/layout/AppSidebar.vue | 40 ++++++++++++++----- frontend/src/router/__tests__/guards.spec.ts | 31 ++++++++++++++ frontend/src/router/index.ts | 21 ++++++++-- frontend/src/router/meta.d.ts | 6 +++ frontend/src/types/index.ts | 2 +- 16 files changed, 141 insertions(+), 46 deletions(-) diff --git a/backend/cmd/server/VERSION b/backend/cmd/server/VERSION index 9e7e837e..74799d81 100644 --- a/backend/cmd/server/VERSION +++ b/backend/cmd/server/VERSION @@ -1 +1 @@ -0.1.126 +0.1.127 diff --git a/backend/cmd/server/main.go b/backend/cmd/server/main.go index 46edcb69..e32b4484 100644 --- a/backend/cmd/server/main.go +++ b/backend/cmd/server/main.go @@ -76,22 +76,6 @@ func main() { return } - // Check if setup is needed - if setup.NeedsSetup() { - // Check if auto-setup is enabled (for Docker deployment) - if setup.AutoSetupEnabled() { - log.Println("Auto setup mode enabled...") - if err := setup.AutoSetupFromEnv(); err != nil { - log.Fatalf("Auto setup failed: %v", err) - } - // Continue to main server after auto-setup - } else { - log.Println("First run detected, starting setup wizard...") - runSetupServer() - return - } - } - // Normal server mode runMainServer() } diff --git a/backend/cmd/server/wire_gen.go b/backend/cmd/server/wire_gen.go index a5501181..69211e07 100644 --- a/backend/cmd/server/wire_gen.go +++ b/backend/cmd/server/wire_gen.go @@ -203,7 +203,7 @@ func initializeApplication(buildInfo handler.BuildInfo) (*Application, error) { updateCache := repository.NewUpdateCache(redisClient) gitHubReleaseClient := repository.ProvideGitHubReleaseClient(configConfig) serviceBuildInfo := provideServiceBuildInfo(buildInfo) - updateService := service.ProvideUpdateService(updateCache, gitHubReleaseClient, serviceBuildInfo) + updateService := service.ProvideUpdateService(updateCache, gitHubReleaseClient, serviceBuildInfo, configConfig) idempotencyRepository := repository.NewIdempotencyRepository(client, db) systemOperationLockService := service.ProvideSystemOperationLockService(idempotencyRepository, configConfig) systemHandler := handler.ProvideSystemHandler(updateService, systemOperationLockService) diff --git a/backend/internal/config/config.go b/backend/internal/config/config.go index d42828d8..8fd823ac 100644 --- a/backend/internal/config/config.go +++ b/backend/internal/config/config.go @@ -148,6 +148,8 @@ type GeminiTierQuotaConfig struct { } type UpdateConfig struct { + // GitHubRepo 用于在线更新的 GitHub 仓库,格式 owner/repo + GitHubRepo string `mapstructure:"github_repo"` // ProxyURL 用于访问 GitHub 的代理地址 // 支持 http/https/socks5/socks5h 协议 // 例如: "http://127.0.0.1:7890", "socks5://127.0.0.1:1080" @@ -988,6 +990,8 @@ type DatabaseConfig struct { Password string `mapstructure:"password"` DBName string `mapstructure:"dbname"` SSLMode string `mapstructure:"sslmode"` + // AutoMigrate controls whether startup applies embedded SQL migrations. + AutoMigrate bool `mapstructure:"auto_migrate"` // 连接池配置(性能优化:可配置化连接池参数) // MaxOpenConns: 最大打开连接数,控制数据库连接上限,防止资源耗尽 MaxOpenConns int `mapstructure:"max_open_conns"` @@ -1543,6 +1547,7 @@ func setDefaults() { viper.SetDefault("database.password", "postgres") viper.SetDefault("database.dbname", "sub2api") viper.SetDefault("database.sslmode", "prefer") + viper.SetDefault("database.auto_migrate", true) viper.SetDefault("database.max_open_conns", 256) viper.SetDefault("database.max_idle_conns", 128) viper.SetDefault("database.conn_max_lifetime_minutes", 30) @@ -1606,6 +1611,10 @@ func setDefaults() { viper.SetDefault("pricing.update_interval_hours", 24) viper.SetDefault("pricing.hash_check_interval_minutes", 10) + // Update + viper.SetDefault("update.github_repo", "man209111-cpu/sub2api") + viper.SetDefault("update.proxy_url", "") + // Timezone (default to Asia/Shanghai for Chinese users) viper.SetDefault("timezone", "Asia/Shanghai") diff --git a/backend/internal/repository/ent.go b/backend/internal/repository/ent.go index 64d32192..80402463 100644 --- a/backend/internal/repository/ent.go +++ b/backend/internal/repository/ent.go @@ -54,14 +54,14 @@ func InitEnt(cfg *config.Config) (*ent.Client, *sql.DB, error) { } applyDBPoolSettings(drv.DB(), cfg) - // 确保数据库 schema 已准备就绪。 - // SQL 迁移文件是 schema 的权威来源(source of truth)。 - // 这种方式比 Ent 的自动迁移更可控,支持复杂的迁移场景。 migrationCtx, cancel := context.WithTimeout(context.Background(), 10*time.Minute) defer cancel() - if err := applyMigrationsFS(migrationCtx, drv.DB(), migrations.FS); err != nil { - _ = drv.Close() // 迁移失败时关闭驱动,避免资源泄露 - return nil, nil, err + if cfg.Database.AutoMigrate { + // SQL migration files are the source of truth for schema changes. + if err := applyMigrationsFS(migrationCtx, drv.DB(), migrations.FS); err != nil { + _ = drv.Close() + return nil, nil, err + } } // 创建 Ent 客户端,绑定到已配置的数据库驱动。 diff --git a/backend/internal/service/update_service.go b/backend/internal/service/update_service.go index 34ad4610..28026805 100644 --- a/backend/internal/service/update_service.go +++ b/backend/internal/service/update_service.go @@ -20,9 +20,9 @@ import ( ) const ( - updateCacheKey = "update_check_cache" - updateCacheTTL = 1200 // 20 minutes - githubRepo = "Wei-Shaw/sub2api" + updateCacheKey = "update_check_cache" + updateCacheTTL = 1200 // 20 minutes + defaultGitHubRepo = "man209111-cpu/sub2api" // Security: allowed download domains for updates allowedDownloadHost = "github.com" @@ -51,15 +51,21 @@ type UpdateService struct { githubClient GitHubReleaseClient currentVersion string buildType string // "source" for manual builds, "release" for CI builds + githubRepo string } // NewUpdateService creates a new UpdateService -func NewUpdateService(cache UpdateCache, githubClient GitHubReleaseClient, version, buildType string) *UpdateService { +func NewUpdateService(cache UpdateCache, githubClient GitHubReleaseClient, version, buildType, githubRepo string) *UpdateService { + githubRepo = strings.TrimSpace(githubRepo) + if githubRepo == "" { + githubRepo = defaultGitHubRepo + } return &UpdateService{ cache: cache, githubClient: githubClient, currentVersion: version, buildType: buildType, + githubRepo: githubRepo, } } @@ -72,6 +78,7 @@ type UpdateInfo struct { Cached bool `json:"cached"` Warning string `json:"warning,omitempty"` BuildType string `json:"build_type"` // "source" or "release" + GitHubRepo string `json:"github_repo"` } // ReleaseInfo contains GitHub release details @@ -129,6 +136,7 @@ func (s *UpdateService) CheckUpdate(ctx context.Context, force bool) (*UpdateInf HasUpdate: false, Warning: err.Error(), BuildType: s.buildType, + GitHubRepo: s.githubRepo, }, nil } @@ -274,7 +282,7 @@ func (s *UpdateService) Rollback() error { } func (s *UpdateService) fetchLatestRelease(ctx context.Context) (*UpdateInfo, error) { - release, err := s.githubClient.FetchLatestRelease(ctx, githubRepo) + release, err := s.githubClient.FetchLatestRelease(ctx, s.githubRepo) if err != nil { return nil, err } @@ -301,8 +309,9 @@ func (s *UpdateService) fetchLatestRelease(ctx context.Context) (*UpdateInfo, er HTMLURL: release.HTMLURL, Assets: assets, }, - Cached: false, - BuildType: s.buildType, + Cached: false, + BuildType: s.buildType, + GitHubRepo: s.githubRepo, }, nil } @@ -477,11 +486,16 @@ func (s *UpdateService) getFromCache(ctx context.Context) (*UpdateInfo, error) { Latest string `json:"latest"` ReleaseInfo *ReleaseInfo `json:"release_info"` Timestamp int64 `json:"timestamp"` + GitHubRepo string `json:"github_repo"` } if err := json.Unmarshal([]byte(data), &cached); err != nil { return nil, err } + if strings.TrimSpace(cached.GitHubRepo) != s.githubRepo { + return nil, fmt.Errorf("cache repo mismatch") + } + if time.Now().Unix()-cached.Timestamp > updateCacheTTL { return nil, fmt.Errorf("cache expired") } @@ -493,6 +507,7 @@ func (s *UpdateService) getFromCache(ctx context.Context) (*UpdateInfo, error) { ReleaseInfo: cached.ReleaseInfo, Cached: true, BuildType: s.buildType, + GitHubRepo: s.githubRepo, }, nil } @@ -501,10 +516,12 @@ func (s *UpdateService) saveToCache(ctx context.Context, info *UpdateInfo) { Latest string `json:"latest"` ReleaseInfo *ReleaseInfo `json:"release_info"` Timestamp int64 `json:"timestamp"` + GitHubRepo string `json:"github_repo"` }{ Latest: info.LatestVersion, ReleaseInfo: info.ReleaseInfo, Timestamp: time.Now().Unix(), + GitHubRepo: s.githubRepo, } data, _ := json.Marshal(cacheData) diff --git a/backend/internal/service/wire.go b/backend/internal/service/wire.go index f0f5ff14..e3c6fd30 100644 --- a/backend/internal/service/wire.go +++ b/backend/internal/service/wire.go @@ -31,8 +31,8 @@ func ProvidePricingService(cfg *config.Config, remoteClient PricingRemoteClient) } // ProvideUpdateService creates UpdateService with BuildInfo -func ProvideUpdateService(cache UpdateCache, githubClient GitHubReleaseClient, buildInfo BuildInfo) *UpdateService { - return NewUpdateService(cache, githubClient, buildInfo.Version, buildInfo.BuildType) +func ProvideUpdateService(cache UpdateCache, githubClient GitHubReleaseClient, buildInfo BuildInfo, cfg *config.Config) *UpdateService { + return NewUpdateService(cache, githubClient, buildInfo.Version, buildInfo.BuildType, cfg.Update.GitHubRepo) } // ProvideEmailQueueService creates EmailQueueService with default worker count diff --git a/deploy/.env.example b/deploy/.env.example index b38c6305..237d815b 100644 --- a/deploy/.env.example +++ b/deploy/.env.example @@ -401,6 +401,10 @@ OPS_ENABLED=true # ----------------------------------------------------------------------------- # Update Configuration (在线更新配置) # ----------------------------------------------------------------------------- +# GitHub repository used for online update checks and binary downloads +# 在线更新检查和二进制下载使用的 GitHub 仓库 +UPDATE_GITHUB_REPO=man209111-cpu/sub2api + # Proxy URL for accessing GitHub (used for online updates and pricing data) # 用于访问 GitHub 的代理地址(用于在线更新和定价数据获取) # Supports: http, https, socks5, socks5h diff --git a/deploy/config.example.yaml b/deploy/config.example.yaml index 0d61b710..188b5fcf 100644 --- a/deploy/config.example.yaml +++ b/deploy/config.example.yaml @@ -1081,6 +1081,9 @@ gemini: # Update Configuration (在线更新配置) # ============================================================================= update: + # GitHub repository used for online update checks and binary downloads. + # 在线更新检查和二进制下载使用的 GitHub 仓库。 + github_repo: "man209111-cpu/sub2api" # Proxy URL for accessing GitHub (used for online updates and pricing data) # 用于访问 GitHub 的代理地址(用于在线更新和定价数据获取) # Supports: http, https, socks5, socks5h diff --git a/deploy/docker-compose.local.yml b/deploy/docker-compose.local.yml index ca915112..12b60767 100644 --- a/deploy/docker-compose.local.yml +++ b/deploy/docker-compose.local.yml @@ -144,6 +144,8 @@ services: # ======================================================================= # Update Configuration (在线更新配置) # ======================================================================= + # GitHub repo for online update checks and binary downloads + - UPDATE_GITHUB_REPO=${UPDATE_GITHUB_REPO:-man209111-cpu/sub2api} # Proxy for accessing GitHub (online updates + pricing data) # Examples: http://host:port, socks5://host:port - UPDATE_PROXY_URL=${UPDATE_PROXY_URL:-} diff --git a/deploy/docker-compose.yml b/deploy/docker-compose.yml index a022f9ce..405c0da0 100644 --- a/deploy/docker-compose.yml +++ b/deploy/docker-compose.yml @@ -140,6 +140,8 @@ services: # ======================================================================= # Update Configuration (在线更新配置) # ======================================================================= + # GitHub repo for online update checks and binary downloads + - UPDATE_GITHUB_REPO=${UPDATE_GITHUB_REPO:-man209111-cpu/sub2api} # Proxy for accessing GitHub (online updates + pricing data) # Examples: http://host:port, socks5://host:port - UPDATE_PROXY_URL=${UPDATE_PROXY_URL:-} diff --git a/frontend/src/components/layout/AppSidebar.vue b/frontend/src/components/layout/AppSidebar.vue index 3d7f1604..475775f5 100644 --- a/frontend/src/components/layout/AppSidebar.vue +++ b/frontend/src/components/layout/AppSidebar.vue @@ -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 }) } diff --git a/frontend/src/router/__tests__/guards.spec.ts b/frontend/src/router/__tests__/guards.spec.ts index 076b943d..e21ee4f6 100644 --- a/frontend/src/router/__tests__/guards.spec.ts +++ b/frontend/src/router/__tests__/guards.spec.ts @@ -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('简易模式受限路由', () => { diff --git a/frontend/src/router/index.ts b/frontend/src/router/index.ts index 49bab903..5ab3e86e 100644 --- a/frontend/src/router/index.ts +++ b/frontend/src/router/index.ts @@ -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 } } diff --git a/frontend/src/router/meta.d.ts b/frontend/src/router/meta.d.ts index 5c468016..792ababd 100644 --- a/frontend/src/router/meta.d.ts +++ b/frontend/src/router/meta.d.ts @@ -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 */ diff --git a/frontend/src/types/index.ts b/frontend/src/types/index.ts index ec7d0636..45ef9945 100644 --- a/frontend/src/types/index.ts +++ b/frontend/src/types/index.ts @@ -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