File tree Expand file tree Collapse file tree
Expand file tree Collapse file tree Original file line number Diff line number Diff line change @@ -18,6 +18,27 @@ function clientIp(req: NextRequest) {
1818 return req . headers . get ( "x-real-ip" ) ?? undefined ;
1919}
2020
21+ const RATE_LIMIT_WINDOW_MS = 10 * 60 * 1000 ;
22+ const RATE_LIMIT_MAX = 5 ;
23+ const rateLimitHits = new Map < string , number [ ] > ( ) ;
24+
25+ function isRateLimited ( key : string ) {
26+ const now = Date . now ( ) ;
27+ const recent = ( rateLimitHits . get ( key ) ?? [ ] ) . filter (
28+ ( t ) => now - t < RATE_LIMIT_WINDOW_MS ,
29+ ) ;
30+ recent . push ( now ) ;
31+ rateLimitHits . set ( key , recent ) ;
32+
33+ if ( rateLimitHits . size > 5000 ) {
34+ for ( const [ k , hits ] of rateLimitHits ) {
35+ if ( now - hits [ hits . length - 1 ] > RATE_LIMIT_WINDOW_MS ) rateLimitHits . delete ( k ) ;
36+ }
37+ }
38+
39+ return recent . length > RATE_LIMIT_MAX ;
40+ }
41+
2142export async function POST ( req : NextRequest ) {
2243 const key = apiKey ( ) ;
2344 if ( ! key ) {
@@ -38,6 +59,10 @@ export async function POST(req: NextRequest) {
3859
3960 const ip = clientIp ( req ) ;
4061
62+ if ( isRateLimited ( ip ?? "unknown" ) ) {
63+ return NextResponse . json ( { error : "Too many requests" } , { status : 429 } ) ;
64+ }
65+
4166 const res = await fetch (
4267 `https://api.airtable.com/v0/${ BASE_ID } /${ encodeURIComponent ( TABLE_NAME ) } ` ,
4368 {
Original file line number Diff line number Diff line change @@ -26,6 +26,11 @@ export function ParentsEmailSignup() {
2626 headers : { "Content-Type" : "application/json" } ,
2727 body : JSON . stringify ( { email : email . trim ( ) } ) ,
2828 } ) ;
29+ if ( res . status === 429 ) {
30+ setStatus ( "error" ) ;
31+ setErrorMsg ( "Too many attempts — try again later" ) ;
32+ return ;
33+ }
2934 if ( ! res . ok ) throw new Error ( ) ;
3035 setStatus ( "success" ) ;
3136 setEmail ( "" ) ;
You can’t perform that action at this time.
0 commit comments