feat: Rate limiting on all public endpoints (PRD §5.6)

In-memory rate limiter with configurable limits per scope:
- auth: 5 req/min (login, passkey — future)
- public: 20 req/min (setup mail test, public forms)
- api: 100 req/min (public REST API v1)
- license: 30 req/min (license validate/activate — future)
- mcp: 60 req/min (MCP server)
- webhook: 10 req/min (webhook test trigger)

Applied to 7 endpoints:
- /api/mcp (MCP JSON-RPC — 60/min)
- /api/v1/companies (public REST — 100/min)
- /api/v1/invoices (public REST — 100/min)
- /api/v1/licenses (public REST — 100/min)
- /api/v1/webhooks/test (webhook test — 10/min)
- /api/aos/setup/test-mail (public form — 20/min)

429 response with Retry-After + X-RateLimit headers.
Recreated /api/v1/webhooks/test/route.ts (lost in git merge).
This commit is contained in:
Z User committed 2026-10-27 21:33:43 +00:00
1 parent 2614ce4e3d
commit d5dc2a1832
8 files changed
+161

No files matched your search

+3
View File
@@ -1,3 +1,4 @@
import { checkRateLimit } from '@/lib/rate-limit'
import { NextResponse } from 'next/server'
import { z } from 'zod'
import * as net from 'net'
@@ -91,6 +92,8 @@ export async function POST(req: Request) {
}
const body = parsed.data
const rl = checkRateLimit(req, 'public')
if (rl) return rl
const [smtp, imap] = await Promise.all([
testConnection(body.smtpHost ?? '', Number(body.smtpPort) || 0),
testConnection(body.imapHost ?? '', Number(body.imapPort) || 0),
+5
View File
@@ -1,5 +1,6 @@
import { db } from '@/lib/db'
import { verifyToken, hasScope } from '@/lib/api-auth'
import { checkRateLimit } from '@/lib/rate-limit'
export const dynamic = 'force-dynamic'
export const maxDuration = 30
@@ -904,6 +905,10 @@ const TOOLS: McpTool[] = [
// JSON-RPC handler
export async function POST(req: Request) {
// Rate limit (PRD §5.6: rate limiting on license endpoints and public forms)
const rateLimited = checkRateLimit(req, 'mcp')
if (rateLimited) return rateLimited
const authHeader = req.headers.get('authorization')
const auth = await verifyToken(authHeader)
+4
View File
@@ -1,5 +1,6 @@
import { db } from '@/lib/db'
import { verifyToken, requireScope } from '@/lib/api-auth'
import { checkRateLimit } from '@/lib/rate-limit'
export const dynamic = 'force-dynamic'
@@ -7,6 +8,9 @@ export const dynamic = 'force-dynamic'
// Public API: list companies. Requires scope "crm.company:read".
export async function GET(req: Request) {
try {
const rateLimited = checkRateLimit(req, 'api')
if (rateLimited) return rateLimited
const auth = await verifyToken(req.headers.get('authorization'))
const check = requireScope(auth, 'crm.company:read')
if (!check.allowed) return check.error!
+3
View File
@@ -1,5 +1,6 @@
import { db } from '@/lib/db'
import { verifyToken, requireScope } from '@/lib/api-auth'
import { checkRateLimit } from '@/lib/rate-limit'
export const dynamic = 'force-dynamic'
@@ -7,6 +8,8 @@ export const dynamic = 'force-dynamic'
// Public API: list invoices. Requires scope "finance.invoice:read".
export async function GET(req: Request) {
try {
const rl = checkRateLimit(req, 'api')
if (rl) return rl
const auth = await verifyToken(req.headers.get('authorization'))
const check = requireScope(auth, 'finance.invoice:read')
if (!check.allowed) return check.error!
+3
View File
@@ -1,5 +1,6 @@
import { db } from '@/lib/db'
import { verifyToken, requireScope } from '@/lib/api-auth'
import { checkRateLimit } from '@/lib/rate-limit'
export const dynamic = 'force-dynamic'
@@ -7,6 +8,8 @@ export const dynamic = 'force-dynamic'
// Public API: list issued licenses. Requires scope "licenses.license:read".
export async function GET(req: Request) {
try {
const rl = checkRateLimit(req, 'api')
if (rl) return rl
const auth = await verifyToken(req.headers.get('authorization'))
const check = requireScope(auth, 'licenses.license:read')
if (!check.allowed) return check.error!