import { Suspense, lazy, useEffect, useState, type ReactNode } from 'react' import { BrowserRouter, Navigate, Route, Routes, useLocation } from 'react-router-dom' import ErrorBoundary from './components/ErrorBoundary' import { ConfigProvider, Spin } from 'antd' import deDE from 'antd/locale/de_DE' import enUS from 'antd/locale/en_US' import { QueryClient, QueryClientProvider } from '@tanstack/react-query' import { useTranslation } from 'react-i18next' import AppLayout from './components/Layout/AppLayout' import apiClient, { isEnvelope } from './api/client' import { useAuthStore, type SessionUser } from './stores/auth' const LoginPage = lazy(() => import('./pages/Login')) const ResetPasswordPage = lazy(() => import('./pages/ResetPassword')) const SetupPage = lazy(() => import('./pages/Setup')) const DashboardPage = lazy(() => import('./pages/Dashboard')) const DomainsPage = lazy(() => import('./pages/Domains')) const DomainDetailPage = lazy(() => import('./pages/Domains/Detail')) const BackendsPage = lazy(() => import('./pages/Backends')) const BackendDetailPage = lazy(() => import('./pages/Backends/Detail')) const RoutingRulesPage = lazy(() => import('./pages/RoutingRules')) const NetworksPage = lazy(() => import('./pages/Networks')) const IPAddressesPage = lazy(() => import('./pages/IPAddresses')) const SSLPage = lazy(() => import('./pages/SSL')) const FirewallPage = lazy(() => import('./pages/Firewall')) const WireguardPage = lazy(() => import('./pages/Wireguard')) const ForwardProxyPage = lazy(() => import('./pages/ForwardProxy')) const DNSPage = lazy(() => import('./pages/DNS')) const DHCPPage = lazy(() => import('./pages/DHCP')) const NTPPage = lazy(() => import('./pages/NTP')) const ClusterPage = lazy(() => import('./pages/Cluster')) const FirewallLivePage = lazy(() => import('./pages/FirewallLive')) const LogsPage = lazy(() => import('./pages/Logs')) const AuditPage = lazy(() => import('./pages/Audit')) const BackupsPage = lazy(() => import('./pages/Backups')) const DiagnosticsPage = lazy(() => import('./pages/Diagnostics')) const AlertsPage = lazy(() => import('./pages/Alerts')) const LicensePage = lazy(() => import('./pages/License')) const SettingsPage = lazy(() => import('./pages/Settings')) const UsersPage = lazy(() => import('./pages/Users')) const CrowdSecPage = lazy(() => import('./pages/CrowdSec')) const WAFPage = lazy(() => import('./pages/WAF')) const queryClient = new QueryClient({ defaultOptions: { queries: { retry: 1, refetchOnWindowFocus: false }, }, }) // Theme tokens 1:1 wie mail-gateway/enconf — Body-Density bekommen // wir über AntD `size="small"` auf den Tables (DataTable hat das per // default), nicht über kleinere Theme-Tokens. const antdTheme = { token: { colorPrimary: '#0EA5E9', borderRadius: 6, borderRadiusLG: 8, fontSize: 13, fontWeightStrong: 600, fontFamily: "'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif", colorBgContainer: '#FFFFFF', colorBgLayout: '#F8FAFC', colorBorder: '#E2E8F0', colorText: '#0F172A', colorTextSecondary: '#64748B', controlHeight: 34, }, components: { Tabs: { itemColor: '#334155', itemHoverColor: '#0F172A', itemSelectedColor: '#0EA5E9', inkBarColor: '#0EA5E9', cardBg: '#F1F5F9', titleFontSize: 13, }, }, } function RequireAuth({ children }: { children: ReactNode }) { const user = useAuthStore((s) => s.user) const setUser = useAuthStore((s) => s.set) const location = useLocation() // Wenn kein Store-User da ist (z.B. direkt nach SSO-Callback: Cookie // gesetzt, sessionStorage leer — oder Hard-Refresh), einmal /auth/me // probieren, bevor wir nach /login umleiten. const [checking, setChecking] = useState(user === null) useEffect(() => { if (user !== null) { setChecking(false) return } let cancelled = false apiClient.get('/auth/me') .then((r) => { if (!cancelled && isEnvelope(r.data)) setUser(r.data.data as SessionUser) }) .catch(() => { /* 401 → Interceptor leitet auf /login */ }) .finally(() => { if (!cancelled) setChecking(false) }) return () => { cancelled = true } }, [user, setUser]) if (user) return <>{children} if (checking) { return (
) } return } function SetupGate({ children }: { children: ReactNode }) { const location = useLocation() useEffect(() => { if (location.pathname.startsWith('/setup')) return apiClient.get('/setup/status').then((r) => { const env = r.data if (isEnvelope(env)) { const data = env.data as { completed?: boolean } | undefined if (data && data.completed === false) { window.location.replace('/setup') } } }).catch(() => { /* setup-gate is best-effort */ }) }, [location.pathname]) return <>{children} } export default function App() { const { i18n } = useTranslation() const antdLocale = i18n.language?.startsWith('de') ? deDE : enUS return ( }> useAuthStore.getState().set(u)} />} /> useAuthStore.getState().set(u)} />} /> } /> }> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> ) } // Resets the ErrorBoundary on every route change so a render error on // one page never permanently blocks navigation to another page. function LocationKeyBoundary({ children }: { children: ReactNode }) { const { pathname } = useLocation() return {children} }