File size: 4,440 Bytes
bcce530
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
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)
}