86 lines
2.2 KiB
TypeScript
86 lines
2.2 KiB
TypeScript
interface Entry
|
|
{
|
|
count: number;
|
|
windowStart: number;
|
|
}
|
|
|
|
function makeRateLimiter( maxAttempts: number, windowMs: number )
|
|
{
|
|
const store = new Map<string, Entry>();
|
|
|
|
return function check( ip: string ): { blocked: boolean }
|
|
{
|
|
const now = Date.now();
|
|
let entry = store.get( ip );
|
|
|
|
if ( !entry || now - entry.windowStart > windowMs )
|
|
entry = { count: 0, windowStart: now };
|
|
|
|
entry.count++;
|
|
store.set( ip, entry );
|
|
|
|
return { blocked: entry.count > maxAttempts };
|
|
};
|
|
}
|
|
|
|
// forgot-password — strict, with escalating delay to prevent email spam
|
|
const FORGOT_WINDOW_MS = 20 * 60 * 1000;
|
|
const FORGOT_MAX_ATTEMPTS = 20;
|
|
const forgotStore = new Map<string, Entry>();
|
|
|
|
function forgotDelayMs( count: number ): number
|
|
{
|
|
if ( count <= 5 ) return 5_000;
|
|
if ( count <= 10 ) return 15_000;
|
|
return 30_000;
|
|
}
|
|
|
|
export function checkForgotPasswordRate( ip: string ): { blocked: boolean; delay: number }
|
|
{
|
|
const now = Date.now();
|
|
let entry = forgotStore.get( ip );
|
|
|
|
if ( !entry || now - entry.windowStart > FORGOT_WINDOW_MS )
|
|
entry = { count: 0, windowStart: now };
|
|
|
|
entry.count++;
|
|
forgotStore.set( ip, entry );
|
|
|
|
if ( entry.count > FORGOT_MAX_ATTEMPTS ) return { blocked: true, delay: 0 };
|
|
return { blocked: false, delay: forgotDelayMs( entry.count ) };
|
|
}
|
|
|
|
// login — 10 attempts per 15 min, small delay after 5
|
|
const loginStore = new Map<string, Entry>();
|
|
const LOGIN_WINDOW_MS = 15 * 60 * 1000;
|
|
const LOGIN_MAX_ATTEMPTS = 10;
|
|
|
|
function loginDelayMs( count: number ): number
|
|
{
|
|
if ( count <= 5 ) return 0;
|
|
return 3_000;
|
|
}
|
|
|
|
export function checkLoginRate( ip: string ): { blocked: boolean; delay: number }
|
|
{
|
|
const now = Date.now();
|
|
let entry = loginStore.get( ip );
|
|
|
|
if ( !entry || now - entry.windowStart > LOGIN_WINDOW_MS )
|
|
entry = { count: 0, windowStart: now };
|
|
|
|
entry.count++;
|
|
loginStore.set( ip, entry );
|
|
|
|
if ( entry.count > LOGIN_MAX_ATTEMPTS ) return { blocked: true, delay: 0 };
|
|
return { blocked: false, delay: loginDelayMs( entry.count ) };
|
|
}
|
|
|
|
// register — 5 attempts per hour, no delay
|
|
export const checkRegisterRate = makeRateLimiter( 5, 60 * 60 * 1000 );
|
|
|
|
export function sleep( ms: number ): Promise<void>
|
|
{
|
|
return new Promise( resolve => setTimeout( resolve, ms ) );
|
|
}
|