Spaces:
Configuration error
Configuration error
| import { NextRequest, NextResponse } from "next/server" | |
| import { getAuthUser, type AuthUser } from "./auth" | |
| import { checkRateLimit, getClientIdentifier, getRateLimitHeaders } from "./rate-limit" | |
| /** | |
| * Reusable API route wrapper that enforces authentication. | |
| * Usage: | |
| * export const POST = withAuth(async (req, user) => { ... }) | |
| */ | |
| export function withAuth( | |
| handler: (request: NextRequest, user: AuthUser) => Promise<NextResponse | Response> | |
| ) { | |
| return async (request: NextRequest) => { | |
| const user = await getAuthUser() | |
| if (!user) { | |
| return NextResponse.json( | |
| { error: "Authentication required" }, | |
| { status: 401 } | |
| ) | |
| } | |
| return handler(request, user) | |
| } | |
| } | |
| /** | |
| * Rate limit configuration presets for different route types. | |
| */ | |
| export const RATE_LIMIT_PRESETS = { | |
| /** Standard write ops: 30 req/hour guest, 100 req/hour user */ | |
| write: { guest: { max: 30, window: 3600 }, user: { max: 100, window: 3600 } }, | |
| /** Expensive ops (AI search, run): 10 req/hour guest, 50 req/hour user */ | |
| expensive: { guest: { max: 10, window: 3600 }, user: { max: 50, window: 3600 } }, | |
| /** Read-heavy: 120 req/hour guest, 300 req/hour user */ | |
| read: { guest: { max: 120, window: 3600 }, user: { max: 300, window: 3600 } }, | |
| } as const | |
| type RateLimitPreset = keyof typeof RATE_LIMIT_PRESETS | |
| /** | |
| * Reusable API route wrapper that enforces rate limiting. | |
| * Usage: | |
| * export const POST = withRateLimit(async (req) => { ... }, "write") | |
| */ | |
| export function withRateLimit( | |
| handler: (request: NextRequest) => Promise<NextResponse | Response>, | |
| preset: RateLimitPreset = "write" | |
| ) { | |
| return async (request: NextRequest) => { | |
| const user = await getAuthUser() | |
| const identifier = getClientIdentifier(request, user?.id ?? undefined) | |
| const isAuth = !!user | |
| const rateLimit = await checkRateLimit( | |
| `${preset}:${identifier}`, | |
| isAuth | |
| ) | |
| if (!rateLimit.success) { | |
| const headers = getRateLimitHeaders(rateLimit) | |
| return NextResponse.json( | |
| { error: rateLimit.error }, | |
| { | |
| status: 429, | |
| headers: Object.fromEntries(headers.entries()), | |
| } | |
| ) | |
| } | |
| const response = await handler(request) | |
| // Append rate limit headers to successful responses | |
| const rlHeaders = getRateLimitHeaders(rateLimit) | |
| rlHeaders.forEach((value, key) => { | |
| response.headers.set(key, value) | |
| }) | |
| return response | |
| } | |
| } | |
| /** | |
| * Combined: auth + rate limit wrapper. | |
| * Usage: | |
| * export const POST = withAuthAndRateLimit(async (req, user) => { ... }, "write") | |
| */ | |
| export function withAuthAndRateLimit( | |
| handler: (request: NextRequest, user: AuthUser) => Promise<NextResponse | Response>, | |
| preset: RateLimitPreset = "write" | |
| ) { | |
| return async (request: NextRequest) => { | |
| // 1. Auth check | |
| const user = await getAuthUser() | |
| if (!user) { | |
| return NextResponse.json( | |
| { error: "Authentication required" }, | |
| { status: 401 } | |
| ) | |
| } | |
| // 2. Rate limit check | |
| const identifier = getClientIdentifier(request, user.id) | |
| const rateLimit = await checkRateLimit(`${preset}:${identifier}`, true) | |
| if (!rateLimit.success) { | |
| const headers = getRateLimitHeaders(rateLimit) | |
| return NextResponse.json( | |
| { error: rateLimit.error }, | |
| { | |
| status: 429, | |
| headers: Object.fromEntries(headers.entries()), | |
| } | |
| ) | |
| } | |
| const response = await handler(request, user) | |
| // Append rate limit headers | |
| const rlHeaders = getRateLimitHeaders(rateLimit) | |
| rlHeaders.forEach((value, key) => { | |
| response.headers.set(key, value) | |
| }) | |
| return response | |
| } | |
| } | |
| /** | |
| * Sanitize user-provided text: strip HTML tags and limit length. | |
| */ | |
| export function sanitizeText(text: string, maxLength: number = 10000): string { | |
| return text | |
| .replace(/<[^>]*>/g, "") | |
| .trim() | |
| .slice(0, maxLength) | |
| } | |
| /** | |
| * Clamp a number to a range. | |
| */ | |
| export function clamp(value: number, min: number, max: number): number { | |
| return Math.min(Math.max(value, min), max) | |
| } | |