diff --git a/frontend/src/router/__tests__/guards.spec.ts b/frontend/src/router/__tests__/guards.spec.ts index 076b943d..a27aaa12 100644 --- a/frontend/src/router/__tests__/guards.spec.ts +++ b/frontend/src/router/__tests__/guards.spec.ts @@ -1,5 +1,6 @@ import { describe, it, expect, vi, beforeEach } from 'vitest' import { setActivePinia, createPinia } from 'pinia' +import { resolveCompletedSetupRedirectPath } from '@/router/setupRedirect' // Mock 导航加载状态 vi.mock('@/composables/useNavigationLoading', () => { @@ -53,6 +54,7 @@ interface MockAuthState { isSimpleMode: boolean backendModeEnabled: boolean hasPendingAuthSession: boolean + setupNeedsSetup?: boolean } /** @@ -66,6 +68,10 @@ function simulateGuard( const requiresAuth = toMeta.requiresAuth !== false const requiresAdmin = toMeta.requiresAdmin === true + if (toPath === '/setup' && authState.setupNeedsSetup === false) { + return resolveCompletedSetupRedirectPath(authState.isAuthenticated, authState.isAdmin) + } + // 不需要认证的路由 if (!requiresAuth) { if ( @@ -378,6 +384,32 @@ describe('路由守卫逻辑', () => { expect(redirect).toBeNull() }) + it('unauthenticated: initialized /setup redirects to /login', () => { + const authState: MockAuthState = { + isAuthenticated: false, + isAdmin: false, + isSimpleMode: false, + backendModeEnabled: true, + hasPendingAuthSession: false, + setupNeedsSetup: false, + } + const redirect = simulateGuard('/setup', { requiresAuth: false }, authState) + expect(redirect).toBe('/login') + }) + + it('admin: initialized /setup redirects to /admin/dashboard', () => { + const authState: MockAuthState = { + isAuthenticated: true, + isAdmin: true, + isSimpleMode: false, + backendModeEnabled: true, + hasPendingAuthSession: false, + setupNeedsSetup: false, + } + const redirect = simulateGuard('/setup', { requiresAuth: false }, authState) + expect(redirect).toBe('/admin/dashboard') + }) + it('admin: /admin/dashboard is allowed', () => { const authState: MockAuthState = { isAuthenticated: true, diff --git a/frontend/src/router/index.ts b/frontend/src/router/index.ts index 656421cc..13905af5 100644 --- a/frontend/src/router/index.ts +++ b/frontend/src/router/index.ts @@ -9,6 +9,8 @@ import { useAppStore } from '@/stores/app' import { useAdminSettingsStore } from '@/stores/adminSettings' import { useNavigationLoadingState } from '@/composables/useNavigationLoading' import { useRoutePrefetch } from '@/composables/useRoutePrefetch' +import { getSetupStatus } from '@/api/setup' +import { resolveCompletedSetupRedirectPath } from './setupRedirect' import { resolveDocumentTitle } from './title' /** @@ -694,7 +696,7 @@ function isBackendModePublicRouteAllowed(path: string, hasPendingAuthSession: bo return false } -router.beforeEach((to, _from, next) => { +router.beforeEach(async (to, _from, next) => { // 开始导航加载状态 navigationLoading.startNavigation() @@ -729,6 +731,18 @@ router.beforeEach((to, _from, next) => { const requiresAuth = to.meta.requiresAuth !== false // Default to true const requiresAdmin = to.meta.requiresAdmin === true + if (to.path === '/setup') { + try { + const status = await getSetupStatus() + if (!status.needs_setup) { + next(resolveCompletedSetupRedirectPath(authStore.isAuthenticated, authStore.isAdmin)) + return + } + } catch { + // If setup status cannot be determined, keep the setup page reachable. + } + } + // If route doesn't require auth, allow access if (!requiresAuth) { // If already authenticated and trying to access login/register, redirect to appropriate dashboard diff --git a/frontend/src/router/setupRedirect.ts b/frontend/src/router/setupRedirect.ts new file mode 100644 index 00000000..0169928d --- /dev/null +++ b/frontend/src/router/setupRedirect.ts @@ -0,0 +1,7 @@ +export function resolveCompletedSetupRedirectPath(isAuthenticated: boolean, isAdmin: boolean): string { + if (!isAuthenticated) { + return '/login' + } + + return isAdmin ? '/admin/dashboard' : '/dashboard' +}