@@ -4,6 +4,60 @@ import { database, dbPath } from './db.js';
44
55const port = Number ( process . env . PORT || 4000 ) ;
66
7+ const LOGIN_RATE_LIMIT_MAX_ATTEMPTS = Number ( process . env . LOGIN_RATE_LIMIT_MAX_ATTEMPTS || 5 ) ;
8+ const LOGIN_RATE_LIMIT_WINDOW_MS = Number ( process . env . LOGIN_RATE_LIMIT_WINDOW_MS || 15 * 60 * 1000 ) ;
9+ const LOGIN_RATE_LIMIT_BLOCK_MS = Number ( process . env . LOGIN_RATE_LIMIT_BLOCK_MS || 15 * 60 * 1000 ) ;
10+ const loginAttempts = new Map ( ) ;
11+
12+ const getLoginKey = ( req , username ) => {
13+ const forwardedFor = req . headers [ 'x-forwarded-for' ] ;
14+ const firstForwardedIp = Array . isArray ( forwardedFor )
15+ ? forwardedFor [ 0 ]
16+ : typeof forwardedFor === 'string'
17+ ? forwardedFor . split ( ',' ) [ 0 ]
18+ : '' ;
19+ const remoteIp = firstForwardedIp ?. trim ( ) || req . socket ?. remoteAddress || 'unknown-ip' ;
20+ return `${ remoteIp } :${ username } ` ;
21+ } ;
22+
23+ const getRateLimitState = ( key ) => {
24+ const now = Date . now ( ) ;
25+ const existing = loginAttempts . get ( key ) ;
26+
27+ if ( ! existing ) {
28+ const state = { count : 0 , windowStart : now , blockedUntil : 0 } ;
29+ loginAttempts . set ( key , state ) ;
30+ return state ;
31+ }
32+
33+ if ( existing . blockedUntil > 0 && existing . blockedUntil <= now ) {
34+ existing . count = 0 ;
35+ existing . windowStart = now ;
36+ existing . blockedUntil = 0 ;
37+ }
38+
39+ if ( now - existing . windowStart > LOGIN_RATE_LIMIT_WINDOW_MS ) {
40+ existing . count = 0 ;
41+ existing . windowStart = now ;
42+ }
43+
44+ return existing ;
45+ } ;
46+
47+ const clearRateLimitState = ( key ) => {
48+ loginAttempts . delete ( key ) ;
49+ } ;
50+
51+ const recordFailedLoginAttempt = ( key ) => {
52+ const now = Date . now ( ) ;
53+ const state = getRateLimitState ( key ) ;
54+ state . count += 1 ;
55+
56+ if ( state . count >= LOGIN_RATE_LIMIT_MAX_ATTEMPTS ) {
57+ state . blockedUntil = now + LOGIN_RATE_LIMIT_BLOCK_MS ;
58+ }
59+ } ;
60+
761const sendJson = ( res , statusCode , body ) => {
862 res . writeHead ( statusCode , {
963 'Content-Type' : 'application/json' ,
@@ -62,13 +116,28 @@ const server = createServer(async (req, res) => {
62116 return ;
63117 }
64118
119+ const loginKey = getLoginKey ( req , username ) ;
120+ const rateLimitState = getRateLimitState ( loginKey ) ;
121+ const now = Date . now ( ) ;
122+ if ( rateLimitState . blockedUntil > now ) {
123+ const retryAfterSeconds = Math . ceil ( ( rateLimitState . blockedUntil - now ) / 1000 ) ;
124+ sendJson ( res , 429 , {
125+ error : 'Too many failed login attempts. Please try again later.' ,
126+ retryAfterSeconds,
127+ } ) ;
128+ return ;
129+ }
130+
65131 const user = database . getUserByCredentials ( username , password ) ;
66132
67133 if ( ! user ) {
134+ recordFailedLoginAttempt ( loginKey ) ;
68135 sendJson ( res , 401 , { error : 'invalid credentials' } ) ;
69136 return ;
70137 }
71138
139+ clearRateLimitState ( loginKey ) ;
140+
72141 sendJson ( res , 200 , { token : `demo-token-${ user . id } ` , user } ) ;
73142 return ;
74143 } catch ( error ) {
0 commit comments