# End-to-End Rate Limiting Verification ## Overview This document provides comprehensive verification steps for the Supabase-based rate limiting implementation that replaces the old in-memory solution. ## Prerequisites - Development server running (`npm run dev`) - Supabase migration applied (rate_limits table exists) - Access to Supabase dashboard at https://app.supabase.com/project/mxadgucxhmstlzsbgmoz ## Rate Limiting Configuration - **Window**: 1 hour (3600000 ms) - **Max Requests**: 5 per hour - **Identifier**: Client IP address - **Behavior**: Fail-open on errors (doesn't block users on system errors) ## Verification Scenarios ### Scenario 1: Basic Rate Limiting Flow **Objective**: Verify rate limiting works for single IP address **Steps**: 1. Navigate to http://localhost:3000/en/contact 2. Fill out the contact form with valid data: - Name: Test User - Email: test@example.com - Message: Test message #1 3. Submit the form - ✅ Expected: Success message appears, form clears - ✅ Expected: No rate limit warning visible yet 4. Submit 4 more times (requests #2-5) - ✅ Expected: Each submission succeeds - ✅ Expected: After 3rd submission, yellow warning appears: "You have 2 attempts remaining" - ✅ Expected: After 4th submission, warning updates: "You have 1 attempt remaining" 5. Submit 6th time (rate limit exceeded) - ✅ Expected: Red error message appears - ✅ Expected: Message includes "Too many requests" or similar - ✅ Expected: Message shows time until reset (e.g., "Please try again in 1 hour") - ✅ Expected: Form submission blocked ### Scenario 2: Database Persistence **Objective**: Verify rate limits persist in Supabase database **Steps**: 1. After completing Scenario 1, open Supabase dashboard 2. Navigate to Table Editor: https://app.supabase.com/project/mxadgucxhmstlzsbgmoz/editor 3. Select `rate_limits` table 4. Find the record with your IP identifier **Verify record contains**: - `identifier`: Should be your IP address (or 'unknown-ip' in dev) - `count`: Should be 5 (or 6 if you tried after rate limit) - `window_start`: Should be recent timestamp (within last hour) - `created_at`: Should match or be close to window_start - `updated_at`: Should be time of last request ### Scenario 3: Page Refresh Persistence **Objective**: Verify rate limiting persists across page refreshes (major improvement over in-memory solution) **Steps**: 1. After being rate limited in Scenario 1 2. Refresh the browser page (F5 or Cmd+R) 3. Try to submit the form again - ✅ Expected: Still shows rate limit error - ✅ Expected: Time countdown continues from where it was - ❌ Old behavior (in-memory): Would reset and allow submissions again ### Scenario 4: Multiple Browser Sessions **Objective**: Verify rate limiting works across different browser sessions **Steps**: 1. After being rate limited in Chrome 2. Open the same contact page in Firefox or Incognito mode 3. Try to submit the form - ✅ Expected: Still rate limited (same IP address) - Note: In production, different browsers/incognito share the same public IP ### Scenario 5: Rate Limit Reset **Objective**: Verify rate limit resets after time window expires **Option A: Wait for Natural Reset** (1 hour wait) 1. Note the exact time you hit rate limit 2. Wait 1 hour 3. Try to submit the form - ✅ Expected: Submission succeeds - ✅ Expected: New rate limit window starts **Option B: Manual Database Reset** (Instant) 1. Open Supabase SQL Editor: https://app.supabase.com/project/mxadgucxhmstlzsbgmoz/sql 2. Run this query to reset your rate limit: ```sql DELETE FROM rate_limits WHERE identifier = 'unknown-ip'; -- Replace 'unknown-ip' with your actual IP if known ``` 3. Refresh the contact page 4. Submit the form - ✅ Expected: Submission succeeds - ✅ Expected: Fresh rate limit window starts ### Scenario 6: Multi-Locale Support **Objective**: Verify rate limiting works across different locales **Steps**: 1. Reset rate limit (Option B above) 2. Submit 3 forms at http://localhost:3000/en/contact (English) 3. Navigate to http://localhost:3000/de/contact (German) 4. Submit 2 more forms - ✅ Expected: Rate limit kicks in on 6th submission total - ✅ Expected: Error message appears in German 5. Navigate to http://localhost:3000/sr/contact (Serbian) - ✅ Expected: Still rate limited - ✅ Expected: Error message appears in Serbian ### Scenario 7: API Headers Verification **Objective**: Verify API returns correct rate limiting headers **Using curl**: ```bash # First request curl -X POST http://localhost:3000/api/contact \ -H "Content-Type: application/json" \ -d '{"name":"Test","email":"test@example.com","message":"Test"}' \ -i # Check for headers: # - X-RateLimit-Remaining: 4 # - Status: 200 # After 5 requests, 6th should return: # - X-RateLimit-Remaining: 0 # - Retry-After: (number of seconds) # - Status: 429 ``` **Using browser DevTools**: 1. Open DevTools (F12) 2. Go to Network tab 3. Submit the contact form 4. Click on the `/api/contact` request 5. Check Response Headers: - ✅ `X-RateLimit-Remaining` should decrement with each request - ✅ `Retry-After` should appear when rate limited (429) ### Scenario 8: Error Handling **Objective**: Verify system fails gracefully on errors **Test invalid data**: 1. Reset rate limit 2. Submit form with invalid email: "notanemail" - ✅ Expected: 400 error with validation message - ✅ Expected: Does NOT count against rate limit 3. Submit form with empty fields - ✅ Expected: Client-side validation prevents submission - ✅ Expected: Error messages appear on form fields ### Scenario 9: Concurrent Requests **Objective**: Verify rate limiting handles concurrent requests correctly **Using the test script**: ```bash # Run 7 concurrent requests bash ./scripts/test-concurrent-rate-limit.sh ``` **Expected behavior**: - First 5 requests: Should succeed (200) - Requests 6-7: Should be rate limited (429) - All requests should be tracked correctly in database ## Automated Verification Script Run the automated test script: ```bash bash ./scripts/verify-e2e-rate-limiting.sh ``` This script will: 1. Check that the dev server is running 2. Send multiple API requests to test rate limiting 3. Verify database records in Supabase 4. Report success/failure for each scenario ## Database Cleanup To clean up test data after verification: ```sql -- Remove all rate limit records older than 1 hour DELETE FROM rate_limits WHERE window_start < NOW() - INTERVAL '1 hour'; -- Or remove all records (fresh start) TRUNCATE TABLE rate_limits; ``` ## Success Criteria All scenarios must pass: - ✅ Rate limiting kicks in after 5 requests - ✅ Rate limit persists across page refreshes - ✅ Rate limit persists across browser sessions - ✅ Database stores correct data - ✅ API returns correct HTTP status codes (200, 429, 400) - ✅ API returns correct headers (X-RateLimit-Remaining, Retry-After) - ✅ UI shows appropriate messages in all languages - ✅ Warning appears when attempts are low - ✅ Error handling works correctly - ✅ Rate limit resets after time window ## Comparison with Old Implementation | Feature | Old (In-Memory) | New (Supabase) | |---------|----------------|----------------| | Persistence | ❌ Reset on refresh | ✅ Persists in database | | Serverless | ❌ Doesn't work | ✅ Works perfectly | | Cross-instance | ❌ Each instance separate | ✅ Shared across all instances | | Bypassable | ❌ Trivially (refresh page) | ✅ Cannot bypass | | Production-ready | ❌ No | ✅ Yes | ## Troubleshooting ### Issue: Rate limiting doesn't work **Check**: 1. Is the dev server running? (`npm run dev`) 2. Is the migration applied? (Check Supabase dashboard) 3. Are Supabase env vars set? (Check `.env.local`) ### Issue: Always getting rate limited **Solution**: Reset the database record: ```sql DELETE FROM rate_limits WHERE identifier = 'unknown-ip'; ``` ### Issue: Rate limit doesn't persist **Check**: 1. Is the migration actually applied? (Query the table) 2. Are there any errors in the server console? 3. Check Supabase logs for database errors ### Issue: Wrong IP being tracked **Context**: In development, IP might be 'unknown-ip' **Expected**: In production on Vercel, x-forwarded-for header will contain real IP ## Next Steps After completing all verification scenarios: 1. Document any issues found 2. Complete subtask-5-2: Verify serverless compatibility 3. Mark subtask-5-1 as completed in implementation_plan.json 4. Commit changes with descriptive message