Skip to content

Commit ebfae76

Browse files
committed
add rate-limiting for non-admin users
1 parent fcfa1aa commit ebfae76

4 files changed

Lines changed: 57 additions & 0 deletions

File tree

apps/app/server/api/chats.post.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import type { UIMessage } from 'ai'
22
import { db, schema } from '@nuxthub/db'
33
import { z } from 'zod'
44
import type { CreateChatBody, CreateChatResponse } from '#shared/types/chat'
5+
import { checkRateLimit, incrementRateLimit } from '../utils/rate-limit'
56

67
const bodySchema = z.object({
78
id: z.string(),
@@ -22,6 +23,13 @@ export default defineEventHandler(async (event) => {
2223
throw createError({ statusCode: 403, statusMessage: 'Admin access required', data: { why: 'Admin chat mode requires the admin role', fix: 'Contact an administrator to be granted access' } })
2324
}
2425

26+
if (user.role !== 'admin') {
27+
const rateLimit = await checkRateLimit(user.id)
28+
if (!rateLimit.allowed) {
29+
throw createError({ statusCode: 429, statusMessage: 'Rate limit exceeded', data: { why: `You have reached the daily limit of ${rateLimit.limit} messages`, fix: 'Wait until tomorrow or contact an administrator' } })
30+
}
31+
}
32+
2533
const [chat] = await db.insert(schema.chats).values({
2634
id,
2735
title: '',
@@ -40,5 +48,9 @@ export default defineEventHandler(async (event) => {
4048
parts: message.parts
4149
})
4250

51+
if (user.role !== 'admin') {
52+
await incrementRateLimit(user.id)
53+
}
54+
4355
return chat satisfies CreateChatResponse
4456
})

apps/app/server/api/chats/[id].post.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import { generateTitle } from '../../utils/chat/generate-title'
1111
import { getAgentConfig } from '../../utils/agent-config'
1212
import { KV_KEYS } from '../../utils/sandbox/types'
1313
import { adminTools } from '../../utils/chat/admin-tools'
14+
import { checkRateLimit, incrementRateLimit } from '../../utils/rate-limit'
1415

1516
defineRouteMeta({
1617
openAPI: {
@@ -64,6 +65,14 @@ export default defineEventHandler(async (event) => {
6465
throw createError({ statusCode: 403, statusMessage: 'Admin access required', data: { why: 'This chat is in admin mode and requires the admin role', fix: 'Contact an administrator to be granted access' } })
6566
}
6667

68+
let rateLimitInfo: { allowed: boolean, remaining: number, limit: number } | undefined
69+
if (user.role !== 'admin') {
70+
rateLimitInfo = await checkRateLimit(user.id)
71+
if (!rateLimitInfo.allowed) {
72+
throw createError({ statusCode: 429, statusMessage: 'Rate limit exceeded', data: { why: `You have reached the daily limit of ${rateLimitInfo.limit} messages`, fix: 'Wait until tomorrow or contact an administrator' } })
73+
}
74+
}
75+
6776
const lastMessage = messages[messages.length - 1]
6877
if (lastMessage?.role === 'user' && messages.length > 1) {
6978
await db.insert(schema.messages).values({
@@ -221,6 +230,10 @@ export default defineEventHandler(async (event) => {
221230
}
222231
}
223232

233+
if (user.role !== 'admin') {
234+
await incrementRateLimit(user.id)
235+
}
236+
224237
requestLog.set({
225238
outcome: 'success',
226239
responseMessageCount: responseMessages.length,
@@ -230,6 +243,11 @@ export default defineEventHandler(async (event) => {
230243
},
231244
})
232245

246+
if (rateLimitInfo) {
247+
setHeader(event, 'X-RateLimit-Limit', String(rateLimitInfo.limit))
248+
setHeader(event, 'X-RateLimit-Remaining', String(rateLimitInfo.remaining - 1))
249+
}
250+
233251
return createUIMessageStreamResponse({ stream })
234252
} catch (error) {
235253
requestLog.error(error instanceof Error ? error : new Error(String(error)))
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import { kv } from '@nuxthub/kv'
2+
import { KV_KEYS } from './sandbox/types'
3+
4+
const DAILY_MESSAGE_LIMIT = 15
5+
6+
function todayKey(): string {
7+
return new Date().toISOString().slice(0, 10)
8+
}
9+
10+
export async function checkRateLimit(userId: string): Promise<{ allowed: boolean, remaining: number, limit: number }> {
11+
const date = todayKey()
12+
const count = await kv.get<number>(KV_KEYS.rateLimit(userId, date)) ?? 0
13+
14+
if (count >= DAILY_MESSAGE_LIMIT) {
15+
return { allowed: false, remaining: 0, limit: DAILY_MESSAGE_LIMIT }
16+
}
17+
18+
return { allowed: true, remaining: DAILY_MESSAGE_LIMIT - count, limit: DAILY_MESSAGE_LIMIT }
19+
}
20+
21+
export async function incrementRateLimit(userId: string): Promise<void> {
22+
const date = todayKey()
23+
const key = KV_KEYS.rateLimit(userId, date)
24+
const count = await kv.get<number>(key) ?? 0
25+
await kv.set(key, count + 1, { ttl: 86400 })
26+
}

apps/app/server/utils/sandbox/types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,4 +36,5 @@ export const KV_KEYS = {
3636
AGENT_CONFIG_CACHE: 'agent:config-cache',
3737
session: (sessionId: string) => `session:${sessionId}`,
3838
ACTIVE_SANDBOX_SESSION: 'sandbox:active-session',
39+
rateLimit: (userId: string, date: string) => `ratelimit:${userId}:${date}`,
3940
} as const

0 commit comments

Comments
 (0)