Private
Public Access
1
0
Files
linux_patch_manager/frontend/src/App.tsx
Echo 86a6c714d4 feat: Complete Azure SSO implementation (v0.1.3)
- Add SSO session cleanup task (10-min expiry, 60s purge interval)
- Change callback to redirect to frontend with tokens as query params
- Add sso_callback_url to SecurityConfig with serde default
- Add SsoCallbackPage.tsx for handling SSO callback redirects
- Add /auth/sso/callback public route to App.tsx
- Add Sign in with Microsoft Azure button to LoginPage
- Replace insecure decode_jwt_payload with verify_id_token
- Implement JWKS caching (1-hour TTL) and RSA signature verification
- Validate iss, aud, exp claims on id_token
- Add jsonwebtoken dependency to pm-web crate
- Update config.example.toml with sso_callback_url setting
- Add sso_callback_url to settings response (read-only from TOML)
2026-05-12 17:01:20 +00:00

121 lines
4.4 KiB
TypeScript

import { useEffect } from 'react'
import { Routes, Route, Navigate } from 'react-router-dom'
import { CssBaseline, ThemeProvider, CircularProgress, Box } from '@mui/material'
import { darkTheme } from './theme/theme'
import { useAuthStore } from './store/authStore'
import AppLayout from './components/AppLayout'
import LoginPage from './pages/LoginPage'
import SsoCallbackPage from './pages/SsoCallbackPage'
import MfaSetupPage from './pages/MfaSetupPage'
import HostsPage from './pages/HostsPage'
import HostDetailPage from './pages/HostDetailPage'
import GroupsPage from './pages/GroupsPage'
import UsersPage from './pages/UsersPage'
import DashboardPage from './pages/DashboardPage'
import PatchDeploymentPage from './pages/PatchDeploymentPage'
import JobsPage from './pages/JobsPage'
import MaintenanceWindowsPage from './pages/MaintenanceWindowsPage'
import CertificatesPage from './pages/CertificatesPage'
import ReportsPage from './pages/ReportsPage'
import SettingsPage from './pages/SettingsPage'
import ProfilePage from './pages/ProfilePage'
function RequireAuth({ children }: { children: React.ReactNode }) {
const isAuthenticated = useAuthStore((s) => s.isAuthenticated)
const isRestoring = useAuthStore((s) => s.isRestoring)
if (isRestoring) {
return (
<Box sx={{ display: 'flex', justifyContent: 'center', alignItems: 'center', height: '100vh' }}>
<CircularProgress />
</Box>
)
}
return isAuthenticated ? <>{children}</> : <Navigate to="/login" replace />
}
/**
* Waits for Zustand persist to finish rehydrating from localStorage,
* then calls restoreSession() so it can see the persisted refreshToken.
* Includes a safety timeout in case anything hangs.
*/
function AuthRestorer({ children }: { children: React.ReactNode }) {
const restoreSession = useAuthStore((s) => s.restoreSession)
useEffect(() => {
let cancelled = false
// Safety timeout: force isRestoring=false if restoration doesn't complete in 15s
const timeout = setTimeout(() => {
if (!cancelled) {
console.warn('[auth] Restoration timeout — forcing isRestoring=false')
useAuthStore.setState({ isRestoring: false })
}
}, 15_000)
const doRestore = () => {
if (!cancelled) restoreSession()
}
let unsub: (() => void) | undefined
// Only call restoreSession AFTER Zustand has rehydrated the persisted state
if (useAuthStore.persist.hasHydrated()) {
console.warn('[auth] Store already hydrated, restoring session')
doRestore()
} else {
console.warn('[auth] Waiting for Zustand hydration...')
unsub = useAuthStore.persist.onFinishHydration(() => {
console.warn('[auth] Hydration complete, restoring session')
doRestore()
})
}
return () => {
cancelled = true
clearTimeout(timeout)
unsub?.()
}
}, [restoreSession])
return <>{children}</>
}
function App() {
return (
<ThemeProvider theme={darkTheme}>
<CssBaseline />
<AuthRestorer>
<Routes>
{/* Public */}
<Route path="/login" element={<LoginPage />} />
<Route path="/auth/sso/callback" element={<SsoCallbackPage />} />
{/* Protected — wrapped in AppLayout with sidebar navigation */}
<Route element={<RequireAuth><AppLayout /></RequireAuth>}>
<Route path="/" element={<Navigate to="/dashboard" replace />} />
<Route path="/mfa/setup" element={<MfaSetupPage />} />
<Route path="/dashboard" element={<DashboardPage />} />
<Route path="/hosts" element={<HostsPage />} />
<Route path="/hosts/:id" element={<HostDetailPage />} />
<Route path="/groups" element={<GroupsPage />} />
<Route path="/users" element={<UsersPage />} />
<Route path="/jobs" element={<JobsPage />} />
<Route path="/deployment" element={<PatchDeploymentPage />} />
<Route path="/maintenance" element={<MaintenanceWindowsPage />} />
<Route path="/reports" element={<ReportsPage />} />
<Route path="/certificates" element={<CertificatesPage />} />
<Route path="/settings" element={<SettingsPage />} />
<Route path="/profile" element={<ProfilePage />} />
</Route>
<Route path="*" element={<Navigate to="/dashboard" replace />} />
</Routes>
</AuthRestorer>
</ThemeProvider>
)
}
export default App