Files
opencrm/frontend/src/App.tsx
T
duffyduck 956bc394b8 Rate-Limit-Sperren: Admin-UI zum Freigeben
Bei zu vielen Login-Fehlversuchen war ohne Container-Restart kein Weg
zurück. Jetzt sehen Admins die aktiven Sperren und können einzeln
freigeben.

Backend:
- GET  /api/settings/rate-limits/active (settings:read)
  Liest SecurityEvent RATE_LIMIT_HIT der letzten 15 Min, gruppiert nach
  IP, liefert lastEmail/limiters/hitCount/lastHit.
- POST /api/settings/rate-limits/reset (settings:update)
  Body { ipAddress } → ruft loginRateLimiter.resetKey + passwordReset-
  RateLimiter.resetKey auf (express-rate-limit v7), audited als
  UPDATE auf resourceType=RateLimit.

Frontend:
- Neue Seite /settings/rate-limits: Tabelle mit IP/Email/Limiter/Hits/
  Letzter-Hit/Aktion. Auto-Refresh alle 15s. Freigeben-Button pro IP.
- Kachel in Settings-Übersicht (orange, ShieldOff-Icon, settings:read).

Live-verifiziert: 11 failed Logins → 429 ab dem 11.; Liste zeigt
IP + Email; POST /reset → 200; danach wieder 401 statt 429; Audit-Log
„Rate-Limit für IP 127.0.0.1 manuell freigegeben" angelegt.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-17 01:20:43 +02:00

259 lines
11 KiB
TypeScript

import { Routes, Route, Navigate } from 'react-router-dom';
import { useQuery } from '@tanstack/react-query';
import { useAuth } from './context/AuthContext';
import { gdprApi } from './services/api';
import { Shield } from 'lucide-react';
import ScrollToTop from './components/ScrollToTop';
import Layout from './components/layout/Layout';
import Login from './pages/Login';
import PasswordResetRequest from './pages/PasswordResetRequest';
import PasswordResetConfirm from './pages/PasswordResetConfirm';
import ChangeInitialPassword from './pages/ChangeInitialPassword';
import Dashboard from './pages/Dashboard';
import CustomerList from './pages/customers/CustomerList';
import CustomerDetail from './pages/customers/CustomerDetail';
import CustomerForm from './pages/customers/CustomerForm';
import ContractList from './pages/contracts/ContractList';
import ContractDetail from './pages/contracts/ContractDetail';
import ContractForm from './pages/contracts/ContractForm';
import ContractCockpit from './pages/contracts/ContractCockpit';
import TaskList from './pages/tasks/TaskList';
import PlatformList from './pages/platforms/PlatformList';
import CancellationPeriodList from './pages/settings/CancellationPeriodList';
import ContractDurationList from './pages/settings/ContractDurationList';
import ProviderList from './pages/settings/ProviderList';
import ContractCategoryList from './pages/settings/ContractCategoryList';
import ViewSettings from './pages/settings/ViewSettings';
import PortalSettings from './pages/settings/PortalSettings';
import DeadlineSettings from './pages/settings/DeadlineSettings';
import EmailProviders from './pages/settings/EmailProviders';
import DatabaseBackup from './pages/settings/DatabaseBackup';
import FactoryDefaults from './pages/settings/FactoryDefaults';
import AuditLogs from './pages/settings/AuditLogs';
import EmailLogPage from './pages/settings/EmailLogs';
import Monitoring from './pages/settings/Monitoring';
import RateLimits from './pages/settings/RateLimits';
import GDPRDashboard from './pages/settings/GDPRDashboard';
import UserList from './pages/users/UserList';
import Settings from './pages/Settings';
import DatabaseStructure from './pages/developer/DatabaseStructure';
import ConsentPage from './pages/public/ConsentPage';
import PrivacyPolicyEditor from './pages/settings/PrivacyPolicyEditor';
import PortalPrivacy from './pages/portal/PortalPrivacy';
import AuthorizationTemplateEditor from './pages/settings/AuthorizationTemplateEditor';
import ImprintEditor from './pages/settings/ImprintEditor';
import PdfTemplates from './pages/settings/PdfTemplates';
import WebsitePrivacyPolicyEditor from './pages/settings/WebsitePrivacyPolicyEditor';
import PortalImprint from './pages/portal/PortalImprint';
import PortalWebsitePrivacy from './pages/portal/PortalWebsitePrivacy';
import PortalAuthorizations from './pages/portal/PortalAuthorizations';
import PortalProfile from './pages/portal/PortalProfile';
import PortalMeters from './pages/portal/PortalMeters';
function ProtectedRoute({ children }: { children: React.ReactNode }) {
const { isAuthenticated, isLoading, user } = useAuth();
if (isLoading) {
return (
<div className="min-h-screen flex items-center justify-center">
<div className="text-gray-500">Laden...</div>
</div>
);
}
if (!isAuthenticated) {
return <Navigate to="/login" replace />;
}
// Force-Change-Password-Flow: nach Einmalpasswort-Login muss der Kunde
// zwingend ein eigenes Passwort vergeben, bevor er irgendwohin sonst
// navigieren darf.
if (user?.mustChangePassword) {
return <Navigate to="/change-initial-password" replace />;
}
return <>{children}</>;
}
function ChangeInitialPasswordGate() {
const { isAuthenticated, isLoading, user } = useAuth();
if (isLoading) {
return (
<div className="min-h-screen flex items-center justify-center">
<div className="text-gray-500">Laden...</div>
</div>
);
}
if (!isAuthenticated) return <Navigate to="/login" replace />;
// Wer nicht im Einmalpasswort-Flow ist, hat hier nichts zu suchen.
if (!user?.mustChangePassword) return <Navigate to="/" replace />;
return <ChangeInitialPassword />;
}
function PortalConsentGate({ children }: { children: React.ReactNode }) {
const { isCustomerPortal } = useAuth();
// Nicht-Portal-Benutzer werden nicht eingeschränkt
if (!isCustomerPortal) return <>{children}</>;
// Portal-Kunden: Consent-Check läuft über Layout ConsentBanner + hier
return <PortalConsentCheck>{children}</PortalConsentCheck>;
}
function PortalConsentCheck({ children }: { children: React.ReactNode }) {
const { data, isLoading, isError } = useQuery({
queryKey: ['my-consent-status'],
queryFn: () => gdprApi.getMyConsentStatus(),
staleTime: 30_000,
retry: 1,
});
if (isLoading) return <div className="text-center py-8 text-gray-500">Laden...</div>;
// Bei Fehler oder fehlender Einwilligung: sperren
const hasConsent = (!isError && data?.data?.hasConsent) ?? false;
if (!hasConsent) {
return (
<div className="flex flex-col items-center justify-center py-16 px-6 text-center">
<div className="bg-amber-50 border border-amber-200 rounded-full p-4 mb-4">
<Shield className="w-8 h-8 text-amber-500" />
</div>
<h2 className="text-xl font-semibold text-gray-800 mb-2">
Datenschutz-Einwilligung erforderlich
</h2>
<p className="text-sm text-gray-600 mb-6 max-w-md">
Dieser Bereich ist erst verfügbar, wenn Sie unserer Datenschutzerklärung zugestimmt haben.
</p>
<a
href="/privacy"
className="inline-flex items-center gap-2 px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors"
>
<Shield className="w-4 h-4" />
Jetzt zur Datenschutzerklärung
</a>
</div>
);
}
return <>{children}</>;
}
function DeveloperRoute({ children }: { children: React.ReactNode }) {
const { hasPermission, developerMode } = useAuth();
// Require both developer permission AND developer mode enabled
if (!hasPermission('developer:access') || !developerMode) {
return <Navigate to="/" replace />;
}
return <>{children}</>;
}
function App() {
const { isAuthenticated, isLoading } = useAuth();
if (isLoading) {
return (
<div className="min-h-screen flex items-center justify-center">
<div className="text-gray-500">Laden...</div>
</div>
);
}
return (
<>
<ScrollToTop />
<Routes>
{/* Öffentliche Routes (OHNE Authentifizierung) */}
<Route path="/datenschutz/:hash" element={<ConsentPage />} />
<Route
path="/login"
element={isAuthenticated ? <Navigate to="/" replace /> : <Login />}
/>
{/* Passwort-Reset (öffentlich, kein Auth-Check) */}
<Route path="/password-reset/request" element={<PasswordResetRequest />} />
<Route path="/password-reset" element={<PasswordResetConfirm />} />
{/* Einmalpasswort → eigenes Passwort vergeben (eingeloggt, eigene Gate-Logik) */}
<Route path="/change-initial-password" element={<ChangeInitialPasswordGate />} />
<Route
path="/"
element={
<ProtectedRoute>
<Layout />
</ProtectedRoute>
}
>
<Route index element={<Dashboard />} />
{/* Customers */}
<Route path="customers" element={<CustomerList />} />
<Route path="customers/new" element={<CustomerForm />} />
<Route path="customers/:id" element={<CustomerDetail />} />
<Route path="customers/:id/edit" element={<CustomerForm />} />
{/* Contracts */}
<Route path="contracts" element={<PortalConsentGate><ContractList /></PortalConsentGate>} />
<Route path="contracts/cockpit" element={<ContractCockpit />} />
<Route path="contracts/new" element={<ContractForm />} />
<Route path="contracts/:id" element={<PortalConsentGate><ContractDetail /></PortalConsentGate>} />
<Route path="contracts/:id/edit" element={<ContractForm />} />
{/* Tasks / Support-Anfragen */}
<Route path="tasks" element={<TaskList />} />
{/* Portal: Meine Daten, Datenschutz, Vollmachten */}
<Route path="my-profile" element={<PortalProfile />} />
<Route path="my-meters" element={<PortalMeters />} />
<Route path="privacy" element={<PortalPrivacy />} />
<Route path="imprint" element={<PortalImprint />} />
<Route path="website-privacy" element={<PortalWebsitePrivacy />} />
<Route path="authorizations" element={<PortalAuthorizations />} />
{/* Settings */}
<Route path="settings" element={<Settings />} />
<Route path="settings/users" element={<UserList />} />
<Route path="settings/platforms" element={<PlatformList />} />
<Route path="settings/cancellation-periods" element={<CancellationPeriodList />} />
<Route path="settings/contract-durations" element={<ContractDurationList />} />
<Route path="settings/providers" element={<ProviderList />} />
<Route path="settings/contract-categories" element={<ContractCategoryList />} />
<Route path="settings/view" element={<ViewSettings />} />
<Route path="settings/portal" element={<PortalSettings />} />
<Route path="settings/deadlines" element={<DeadlineSettings />} />
<Route path="settings/email-providers" element={<EmailProviders />} />
<Route path="settings/database-backup" element={<DatabaseBackup />} />
<Route path="settings/factory-defaults" element={<FactoryDefaults />} />
<Route path="settings/audit-logs" element={<AuditLogs />} />
<Route path="settings/email-logs" element={<EmailLogPage />} />
<Route path="settings/monitoring" element={<Monitoring />} />
<Route path="settings/rate-limits" element={<RateLimits />} />
<Route path="settings/gdpr" element={<GDPRDashboard />} />
<Route path="settings/privacy-policy" element={<PrivacyPolicyEditor />} />
<Route path="settings/authorization-template" element={<AuthorizationTemplateEditor />} />
<Route path="settings/pdf-templates" element={<PdfTemplates />} />
<Route path="settings/imprint" element={<ImprintEditor />} />
<Route path="settings/website-privacy-policy" element={<WebsitePrivacyPolicyEditor />} />
{/* Redirect old users route */}
<Route path="users" element={<Navigate to="/settings/users" replace />} />
{/* Redirect old platforms route */}
<Route path="platforms" element={<Navigate to="/settings/platforms" replace />} />
{/* Developer */}
<Route path="developer/database" element={<DeveloperRoute><DatabaseStructure /></DeveloperRoute>} />
</Route>
<Route path="*" element={<Navigate to="/" replace />} />
</Routes>
</>
);
}
export default App;