Skip to content

Commit 8632700

Browse files
author
teycir
committed
feat(security): integrate database for rate limiting
Moves database initialization to route scope for better performance. This enables persistent storage and more robust, scalable rate limiting mechanisms.
1 parent 445dd2b commit 8632700

2 files changed

Lines changed: 94 additions & 3 deletions

File tree

lib/routeHelper.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { createContainer } from './container';
44
import { createHandler, HandlerContext } from './apiHandler';
55
import { withRateLimit } from './rateLimit';
66
import { getRequestFingerprint } from './security';
7+
import { createDatabase } from './database';
78

89
interface RouteHandlerOptions {
910
rateLimit?: { limit: number; window: number };
@@ -21,20 +22,20 @@ export function createAPIRoute(
2122
return async (request: NextRequest, routeParams?: any) => {
2223
const ip = request.headers.get('CF-Connecting-IP') || 'unknown';
2324
const fingerprint = getRequestFingerprint(request);
25+
const { env } = await getCloudflareContext<{ env: CloudflareEnv }>();
2426

2527
const wrappedHandler = async () => {
2628
const apiHandler = createHandler(async (ctx: HandlerContext) => {
27-
const { env } = await getCloudflareContext<{ env: CloudflareEnv }>();
2829
const container = createContainer(env);
2930
return handler({ ...ctx, container });
3031
});
3132

32-
const { env } = await getCloudflareContext<{ env: CloudflareEnv }>();
3333
return apiHandler({ request, ip, env });
3434
};
3535

3636
if (options.rateLimit) {
37-
return withRateLimit(request, wrappedHandler, { ...options.rateLimit, key: fingerprint });
37+
const db = createDatabase(env);
38+
return withRateLimit(request, wrappedHandler, { ...options.rateLimit, key: fingerprint, db });
3839
}
3940

4041
return wrappedHandler();

tests/unit/securityDB.test.ts

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
import { describe, it, expect, beforeEach } from '@jest/globals';
2+
import { SealDatabase } from '../../lib/database';
3+
4+
describe('DB-backed Security', () => {
5+
let mockDB: any;
6+
let db: SealDatabase;
7+
8+
beforeEach(() => {
9+
const store = new Map<string, any>();
10+
11+
mockDB = {
12+
prepare: (sql: string) => ({
13+
bind: (...args: any[]) => ({
14+
run: async () => {
15+
if (sql.includes('INSERT INTO rate_limits')) {
16+
store.set(args[0], { count: 1, reset_at: args[1] });
17+
return { success: true };
18+
}
19+
if (sql.includes('UPDATE rate_limits')) {
20+
const existing = store.get(args[0]);
21+
if (existing) existing.count++;
22+
return { success: true };
23+
}
24+
if (sql.includes('INSERT INTO nonces')) {
25+
if (store.has(`nonce:${args[0]}`)) throw new Error('Duplicate');
26+
store.set(`nonce:${args[0]}`, args[1]);
27+
return { success: true };
28+
}
29+
return { success: true };
30+
},
31+
first: async () => {
32+
if (sql.includes('rate_limits')) {
33+
return store.get(args[0]) || null;
34+
}
35+
return null;
36+
},
37+
}),
38+
}),
39+
};
40+
41+
db = new SealDatabase(mockDB);
42+
});
43+
44+
it('should enforce rate limits', async () => {
45+
const key = 'test-fingerprint';
46+
47+
const r1 = await db.checkRateLimit(key, 2, 60000);
48+
expect(r1.allowed).toBe(true);
49+
expect(r1.remaining).toBe(1);
50+
51+
const r2 = await db.checkRateLimit(key, 2, 60000);
52+
expect(r2.allowed).toBe(true);
53+
expect(r2.remaining).toBeGreaterThanOrEqual(0);
54+
55+
const r3 = await db.checkRateLimit(key, 2, 60000);
56+
expect(r3.allowed).toBe(false);
57+
});
58+
59+
it('should reset rate limits after window', async () => {
60+
const key = 'test-fingerprint';
61+
const now = Date.now();
62+
63+
mockDB.prepare = (sql: string) => ({
64+
bind: (...args: any[]) => ({
65+
run: async () => ({ success: true }),
66+
first: async () => {
67+
if (sql.includes('rate_limits')) {
68+
return { count: 2, reset_at: now - 1000 }; // Expired
69+
}
70+
return null;
71+
},
72+
}),
73+
});
74+
75+
const result = await db.checkRateLimit(key, 2, 60000);
76+
expect(result.allowed).toBe(true);
77+
expect(result.remaining).toBe(1);
78+
});
79+
80+
it('should reject replay attacks', async () => {
81+
const nonce = 'test-nonce-123';
82+
const expiresAt = Date.now() + 300000;
83+
84+
const first = await db.storeNonce(nonce, expiresAt);
85+
expect(first).toBe(true);
86+
87+
const replay = await db.storeNonce(nonce, expiresAt);
88+
expect(replay).toBe(false);
89+
});
90+
});

0 commit comments

Comments
 (0)