mirror of
https://github.com/yusufipk/RepoHub.git
synced 2026-09-11 18:46:07 +00:00
feat: introduce API rate limiting for API routes and cap package query limits.
This commit is contained in:
@@ -4,26 +4,26 @@ import { PackageService } from '@/services/packageService'
|
||||
export async function GET(request: NextRequest) {
|
||||
try {
|
||||
const { searchParams } = new URL(request.url)
|
||||
|
||||
|
||||
// Parse query parameters
|
||||
const filter = {
|
||||
platform_id: searchParams.get('platform_id') || undefined,
|
||||
category_id: searchParams.get('category_id') ?
|
||||
category_id: searchParams.get('category_id') ?
|
||||
parseInt(searchParams.get('category_id')!) : undefined,
|
||||
type: searchParams.get('type') as 'gui' | 'cli' | undefined,
|
||||
repository: searchParams.get('repository') as 'official' | 'third-party' | 'aur' | undefined,
|
||||
search: searchParams.get('search') || undefined,
|
||||
limit: searchParams.get('limit') ?
|
||||
parseInt(searchParams.get('limit')!) : undefined,
|
||||
offset: searchParams.get('offset') ?
|
||||
limit: searchParams.get('limit') ?
|
||||
Math.min(parseInt(searchParams.get('limit')!), 100) : undefined,
|
||||
offset: searchParams.get('offset') ?
|
||||
parseInt(searchParams.get('offset')!) : undefined,
|
||||
sort_by: searchParams.get('sort_by') as
|
||||
sort_by: searchParams.get('sort_by') as
|
||||
'name' | 'popularity_score' | 'last_updated' | 'downloads_count' | undefined,
|
||||
sort_order: searchParams.get('sort_order') as 'asc' | 'desc' | undefined
|
||||
}
|
||||
|
||||
const result = await PackageService.getMany(filter)
|
||||
|
||||
|
||||
return NextResponse.json({
|
||||
packages: result.packages,
|
||||
total: result.total,
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
import { LRUCache } from 'lru-cache'
|
||||
|
||||
type Options = {
|
||||
uniqueTokenPerInterval?: number
|
||||
interval?: number
|
||||
}
|
||||
|
||||
export default function rateLimit(options?: Options) {
|
||||
const tokenCache = new LRUCache({
|
||||
max: options?.uniqueTokenPerInterval || 500,
|
||||
ttl: options?.interval || 60000,
|
||||
})
|
||||
|
||||
return {
|
||||
check: (res: Response | null, limit: number, token: string) =>
|
||||
new Promise<void>((resolve, reject) => {
|
||||
const tokenCount = (tokenCache.get(token) as number[]) || [0]
|
||||
if (tokenCount[0] === 0) {
|
||||
tokenCache.set(token, tokenCount)
|
||||
}
|
||||
tokenCount[0] += 1
|
||||
|
||||
const currentUsage = tokenCount[0]
|
||||
const isRateLimited = currentUsage >= limit
|
||||
|
||||
// If we had a response object we could set headers, but for Next.js middleware
|
||||
// we just want to know if we should block.
|
||||
|
||||
if (isRateLimited) {
|
||||
reject()
|
||||
} else {
|
||||
resolve()
|
||||
}
|
||||
}),
|
||||
}
|
||||
}
|
||||
+21
-4
@@ -1,7 +1,25 @@
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import rateLimit from '@/lib/rate-limit'
|
||||
|
||||
const limiter = rateLimit({
|
||||
interval: 60 * 1000, // 60 seconds
|
||||
uniqueTokenPerInterval: 500, // Max 500 users per second
|
||||
})
|
||||
|
||||
export async function middleware(request: NextRequest) {
|
||||
// Only rate limit API routes
|
||||
if (request.nextUrl.pathname.startsWith('/api')) {
|
||||
try {
|
||||
// 100 requests per minute per IP
|
||||
await limiter.check(null, 50, request.ip ?? 'CACHE_TOKEN')
|
||||
} catch {
|
||||
return NextResponse.json(
|
||||
{ error: 'Too Many Requests' },
|
||||
{ status: 429 }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export function middleware(request: NextRequest) {
|
||||
// Middleware temporarily disabled - just pass through
|
||||
return NextResponse.next()
|
||||
}
|
||||
|
||||
@@ -9,11 +27,10 @@ export const config = {
|
||||
matcher: [
|
||||
/*
|
||||
* Match all request paths except for the ones starting with:
|
||||
* - api (API routes)
|
||||
* - _next/static (static files)
|
||||
* - _next/image (image optimization files)
|
||||
* - favicon.ico (favicon file)
|
||||
*/
|
||||
'/((?!api|_next/static|_next/image|favicon.ico).*)',
|
||||
'/((?!_next/static|_next/image|favicon.ico).*)',
|
||||
],
|
||||
}
|
||||
|
||||
@@ -93,7 +93,8 @@ export class PackageService {
|
||||
LIMIT $${paramIndex++} OFFSET $${paramIndex++}
|
||||
`
|
||||
|
||||
values.push(limit, offset)
|
||||
const cappedLimit = Math.min(limit, 100)
|
||||
values.push(cappedLimit, offset)
|
||||
const packagesResult = await query(packagesQuery, values)
|
||||
|
||||
// Transform the results
|
||||
@@ -146,7 +147,7 @@ export class PackageService {
|
||||
`
|
||||
|
||||
const result = await query(packageQuery, [id])
|
||||
|
||||
|
||||
if (result.rows.length === 0) {
|
||||
return null
|
||||
}
|
||||
@@ -213,9 +214,9 @@ export class PackageService {
|
||||
popularity_score, last_seen_at
|
||||
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, NOW())
|
||||
RETURNING *`,
|
||||
[id, name, description, version, platform_id, category_id,
|
||||
license_id, type, repository, homepage_url, download_url,
|
||||
popularity_score]
|
||||
[id, name, description, version, platform_id, category_id,
|
||||
license_id, type, repository, homepage_url, download_url,
|
||||
popularity_score]
|
||||
)
|
||||
|
||||
const createdPackage = await this.getById(result.rows[0].id)
|
||||
@@ -271,7 +272,7 @@ export class PackageService {
|
||||
|
||||
const values = tags.map((tag, index) => `($1, $${index + 2})`).join(', ')
|
||||
const params = [packageId, ...tags]
|
||||
|
||||
|
||||
await query(
|
||||
`INSERT INTO package_tags (package_id, tag) VALUES ${values} ON CONFLICT DO NOTHING`,
|
||||
params
|
||||
|
||||
Reference in New Issue
Block a user