diff --git a/backend/src/app.ts b/backend/src/app.ts index e8c81c3..22066b4 100644 --- a/backend/src/app.ts +++ b/backend/src/app.ts @@ -8,6 +8,9 @@ import { errorHandler, notFoundHandler } from './middleware/error.middleware'; const app: Application = express(); +// Trust proxy (required for correct IP detection behind Traefik/Nginx) +app.set('trust proxy', 1); + // Security middleware app.use(helmet()); @@ -17,7 +20,7 @@ app.use(cors({ credentials: true, })); -// Rate limiting +// Rate limiting - general API routes const limiter = rateLimit({ windowMs: environment.rateLimit.windowMs, max: environment.rateLimit.max, @@ -26,6 +29,16 @@ const limiter = rateLimit({ legacyHeaders: false, }); +// Rate limiting - auth routes (more generous to avoid blocking logins) +const authLimiter = rateLimit({ + windowMs: 15 * 60 * 1000, // 15 minutes + max: 30, // 30 auth attempts per window + message: 'Zu viele Anmeldeversuche. Bitte versuchen Sie es später erneut.', + standardHeaders: true, + legacyHeaders: false, +}); + +app.use('/api/auth', authLimiter); app.use('/api', limiter); // Body parsing middleware diff --git a/frontend/src/components/auth/LoginCallback.tsx b/frontend/src/components/auth/LoginCallback.tsx index 0b58efa..d321f51 100644 --- a/frontend/src/components/auth/LoginCallback.tsx +++ b/frontend/src/components/auth/LoginCallback.tsx @@ -34,10 +34,13 @@ const LoginCallback: React.FC = () => { navigate('/dashboard', { replace: true }); } catch (err) { console.error('Login callback error:', err); + const is429 = err && typeof err === 'object' && 'status' in err && (err as any).status === 429; setError( - err instanceof Error - ? err.message - : 'Anmeldung fehlgeschlagen. Bitte versuchen Sie es erneut.' + is429 + ? 'Zu viele Anmeldeversuche. Bitte warten Sie einige Minuten und versuchen Sie es erneut.' + : err instanceof Error + ? err.message + : 'Anmeldung fehlgeschlagen. Bitte versuchen Sie es erneut.' ); } }; diff --git a/frontend/src/contexts/AuthContext.tsx b/frontend/src/contexts/AuthContext.tsx index 62acc61..4a897bb 100644 --- a/frontend/src/contexts/AuthContext.tsx +++ b/frontend/src/contexts/AuthContext.tsx @@ -91,7 +91,12 @@ export const AuthProvider: React.FC = ({ children }) => { }); // Show error notification - notification.showError('Anmeldung fehlgeschlagen. Bitte versuchen Sie es erneut.'); + const is429 = error && typeof error === 'object' && 'status' in error && (error as any).status === 429; + notification.showError( + is429 + ? 'Zu viele Anmeldeversuche. Bitte warten Sie einige Minuten und versuchen Sie es erneut.' + : 'Anmeldung fehlgeschlagen. Bitte versuchen Sie es erneut.' + ); throw error; } }, [notification]); diff --git a/frontend/src/services/api.ts b/frontend/src/services/api.ts index 6cb8ef4..22a9b46 100644 --- a/frontend/src/services/api.ts +++ b/frontend/src/services/api.ts @@ -38,7 +38,7 @@ class ApiService { // Response interceptor: Handle errors this.axiosInstance.interceptors.response.use( (response) => response, - (error: AxiosError) => { + async (error: AxiosError) => { if (error.response?.status === 401) { // Clear tokens and redirect to login console.warn('Unauthorized request, redirecting to login'); @@ -46,6 +46,22 @@ class ApiService { removeUser(); window.location.href = '/login'; } + + // Retry on 429 (Too Many Requests) with exponential backoff + if (error.response?.status === 429 && error.config) { + const config = error.config as AxiosRequestConfig & { _retryCount?: number }; + const retryCount = config._retryCount || 0; + const maxRetries = 3; + + if (retryCount < maxRetries) { + config._retryCount = retryCount + 1; + const delay = Math.pow(2, retryCount) * 1000; // 1s, 2s, 4s + console.warn(`Rate limited (429). Retrying in ${delay}ms (attempt ${retryCount + 1}/${maxRetries})...`); + await new Promise((resolve) => setTimeout(resolve, delay)); + return this.axiosInstance.request(config); + } + } + return Promise.reject(this.handleError(error)); } );