first commit

This commit is contained in:
Awuqing
2026-03-17 13:29:09 +08:00
commit eadd3f8961
219 changed files with 22394 additions and 0 deletions

View File

@@ -0,0 +1,61 @@
import { render, screen } from '@testing-library/react'
import { MemoryRouter, Routes, Route } from 'react-router-dom'
import { ProtectedRoute } from './ProtectedRoute'
import { useAuthStore } from '../stores/auth'
describe('ProtectedRoute', () => {
beforeEach(() => {
useAuthStore.setState({
token: '',
user: null,
status: 'anonymous',
bootstrapped: true,
})
})
it('redirects anonymous users to login', () => {
render(
<MemoryRouter initialEntries={['/dashboard']}>
<Routes>
<Route path="/login" element={<div>login page</div>} />
<Route
path="/dashboard"
element={
<ProtectedRoute>
<div>dashboard page</div>
</ProtectedRoute>
}
/>
</Routes>
</MemoryRouter>,
)
expect(screen.getByText('login page')).toBeInTheDocument()
})
it('renders protected content for authenticated users', () => {
useAuthStore.setState({
token: 'token',
user: { id: 1, username: 'admin', displayName: 'Admin', role: 'admin' },
status: 'authenticated',
bootstrapped: true,
})
render(
<MemoryRouter initialEntries={['/dashboard']}>
<Routes>
<Route
path="/dashboard"
element={
<ProtectedRoute>
<div>dashboard page</div>
</ProtectedRoute>
}
/>
</Routes>
</MemoryRouter>,
)
expect(screen.getByText('dashboard page')).toBeInTheDocument()
})
})

View File

@@ -0,0 +1,24 @@
import { ReactNode } from 'react'
import { Navigate, useLocation } from 'react-router-dom'
import { useAuthStore } from '../stores/auth'
import { FullPageLoading } from '../components/FullPageLoading'
interface ProtectedRouteProps {
children: ReactNode
}
export function ProtectedRoute({ children }: ProtectedRouteProps) {
const status = useAuthStore((state) => state.status)
const bootstrapped = useAuthStore((state) => state.bootstrapped)
const location = useLocation()
if (!bootstrapped || status === 'loading') {
return <FullPageLoading tip="正在验证登录状态..." />
}
if (status !== 'authenticated') {
return <Navigate to="/login" replace state={{ from: location.pathname }} />
}
return <>{children}</>
}

40
web/src/router/index.tsx Normal file
View File

@@ -0,0 +1,40 @@
import { Navigate, Route, Routes } from 'react-router-dom'
import { AppLayout } from '../layouts/AppLayout'
import { DashboardPage } from '../pages/dashboard/DashboardPage'
import { LoginPage } from '../pages/login/LoginPage'
import { NotificationsPage } from '../pages/notifications/NotificationsPage'
import { BackupRecordsPage } from '../pages/backup-records/BackupRecordsPage'
import { BackupTasksPage } from '../pages/backup-tasks/BackupTasksPage'
import { GoogleDriveCallbackPage } from '../pages/storage-targets/GoogleDriveCallbackPage'
import { StorageTargetsPage } from '../pages/storage-targets/StorageTargetsPage'
import { SettingsPage } from '../pages/settings/SettingsPage'
import NodesPage from '../pages/nodes/NodesPage'
import { ProtectedRoute } from './ProtectedRoute'
export function RouterView() {
return (
<Routes>
<Route path="/login" element={<LoginPage />} />
<Route
path="/"
element={
<ProtectedRoute>
<AppLayout />
</ProtectedRoute>
}
>
<Route index element={<Navigate to="/dashboard" replace />} />
<Route path="dashboard" element={<DashboardPage />} />
<Route path="backup/tasks" element={<BackupTasksPage />} />
<Route path="backup/records" element={<BackupRecordsPage />} />
<Route path="storage-targets" element={<StorageTargetsPage />} />
<Route path="storage-targets/google-drive/callback" element={<GoogleDriveCallbackPage />} />
<Route path="settings" element={<SettingsPage />} />
<Route path="settings/notifications" element={<NotificationsPage />} />
<Route path="nodes" element={<NodesPage />} />
<Route path="system-info" element={<Navigate to="/settings" replace />} />
</Route>
<Route path="*" element={<Navigate to="/" replace />} />
</Routes>
)
}