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).
50 lines
1.7 KiB
TypeScript
Executable File
50 lines
1.7 KiB
TypeScript
Executable File
import { db } from '@/lib/db'
|
|
import { verifyToken, requireScope } from '@/lib/api-auth'
|
|
import { checkRateLimit } from '@/lib/rate-limit'
|
|
|
|
export const dynamic = 'force-dynamic'
|
|
|
|
// GET /api/v1/licenses
|
|
// 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!
|
|
|
|
const { searchParams } = new URL(req.url)
|
|
const limit = Math.min(Number(searchParams.get('limit') ?? 50), 200)
|
|
const status = searchParams.get('status')
|
|
|
|
const where = status ? { status } : {}
|
|
const licenses = await db.license.findMany({
|
|
where,
|
|
take: limit,
|
|
orderBy: { createdAt: 'desc' },
|
|
include: { company: true, activations: true },
|
|
})
|
|
|
|
return Response.json({
|
|
data: licenses.map((l) => ({
|
|
id: l.id,
|
|
key: l.key,
|
|
product: l.product,
|
|
plan: l.plan,
|
|
status: l.status,
|
|
maxActivations: l.maxActivations,
|
|
validUntil: l.validUntil,
|
|
createdAt: l.createdAt,
|
|
company: { id: l.company.id, name: l.company.name },
|
|
activations: l.activations.map((a) => ({ domain: a.domain, environment: a.environment, version: a.version, lastCheck: a.lastCheck })),
|
|
})),
|
|
pagination: { total: licenses.length, limit },
|
|
token: auth.token!.name,
|
|
})
|
|
} catch (err) {
|
|
const msg = err instanceof Error ? err.message : 'internal server error'
|
|
return Response.json({ error: msg }, { status: 500 })
|
|
}
|
|
}
|