feat: rate limiting connexion (5 essais / 15 min)

This commit is contained in:
2026-06-06 17:24:19 +02:00
parent dbb155cb3a
commit 2acf899465
2 changed files with 68 additions and 0 deletions
+36
View File
@@ -0,0 +1,36 @@
import {
checkRateLimit,
recordFailedAttempt,
clearAttempts,
_resetForTests,
} from '@/lib/rate-limit'
beforeEach(() => _resetForTests())
describe('checkRateLimit', () => {
it('autorise une nouvelle clé', () => {
expect(checkRateLimit('ip:user@test.com')).toBe(true)
})
it('autorise après moins de 5 tentatives', () => {
const key = 'ip:user@test.com'
recordFailedAttempt(key)
recordFailedAttempt(key)
recordFailedAttempt(key)
recordFailedAttempt(key)
expect(checkRateLimit(key)).toBe(true)
})
it('bloque après 5 tentatives', () => {
const key = 'ip:user@test.com'
for (let i = 0; i < 5; i++) recordFailedAttempt(key)
expect(checkRateLimit(key)).toBe(false)
})
it('débloque après clearAttempts', () => {
const key = 'ip:user@test.com'
for (let i = 0; i < 5; i++) recordFailedAttempt(key)
clearAttempts(key)
expect(checkRateLimit(key)).toBe(true)
})
})
+32
View File
@@ -0,0 +1,32 @@
const MAX_ATTEMPTS = 5
const BLOCK_DURATION_MS = 15 * 60 * 1000 // 15 minutes
type Entry = { count: number; blockedAt: number | null }
let attempts = new Map<string, Entry>()
export function checkRateLimit(key: string): boolean {
const now = Date.now()
const entry = attempts.get(key)
if (!entry) return true
if (entry.blockedAt !== null) {
if (now - entry.blockedAt < BLOCK_DURATION_MS) return false
attempts.delete(key)
}
return true
}
export function recordFailedAttempt(key: string): void {
const entry = attempts.get(key) ?? { count: 0, blockedAt: null }
entry.count += 1
if (entry.count >= MAX_ATTEMPTS) entry.blockedAt = Date.now()
attempts.set(key, entry)
}
export function clearAttempts(key: string): void {
attempts.delete(key)
}
/** Réinitialisation pour les tests uniquement */
export function _resetForTests(): void {
attempts = new Map()
}