diff --git a/src/app/api/aos/instances/route.ts b/src/app/api/aos/instances/route.ts index 1557267..0698682 100644 --- a/src/app/api/aos/instances/route.ts +++ b/src/app/api/aos/instances/route.ts @@ -321,6 +321,18 @@ export async function POST(req: Request) { }).catch(() => {}) } + // Dispatch webhook event + const { dispatchWebhook } = await import('@/lib/webhook') + dispatchWebhook('instance.provisioned', { + instanceId: instance.id, + subdomain, + url: result.url, + product, + companyId: license.companyId, + status: isDemo ? 'demo_active' : 'active', + demoExpiresAt: instance.demoExpiresAt, + }).catch(() => {}) + return NextResponse.json({ instanceId: instance.id, subdomain, diff --git a/src/app/api/aos/licenses/route.ts b/src/app/api/aos/licenses/route.ts index 656bec2..0bae19b 100755 --- a/src/app/api/aos/licenses/route.ts +++ b/src/app/api/aos/licenses/route.ts @@ -316,6 +316,20 @@ export async function POST(req: Request) { }).catch(() => {}) // fire-and-forget, don't block response } + // Dispatch webhook event + const { dispatchWebhook } = await import('@/lib/webhook') + dispatchWebhook('license.created', { + licenseId: license.id, + key, + product, + plan, + companyId: license.companyId, + companyName: company.name, + environment, + billingCycle, + netPriceCents: pricing.netPriceCents, + }).catch(() => {}) + return NextResponse.json({ license, pricing: pricing.breakdown }, { status: 201 }) } catch (err) { const msg = err instanceof Error ? err.message : 'internal server error' diff --git a/src/app/api/aos/tickets/route.ts b/src/app/api/aos/tickets/route.ts index fa26fe9..336d758 100755 --- a/src/app/api/aos/tickets/route.ts +++ b/src/app/api/aos/tickets/route.ts @@ -159,6 +159,28 @@ export async function POST(req: Request) { }, }) + // Send ticket created email + const { sendTicketCreatedEmail } = await import('@/lib/email') + if (company.email) { + sendTicketCreatedEmail({ + to: company.email, + ticketNumber: ticket.number, + subject: ticket.subject, + }).catch(() => {}) + } + + // Dispatch webhook event + const { dispatchWebhook } = await import('@/lib/webhook') + dispatchWebhook('ticket.created', { + ticketId: ticket.id, + number: ticket.number, + subject: ticket.subject, + companyId, + priority, + routedToAgency, + agencyId, + }).catch(() => {}) + return NextResponse.json({ ticket }, { status: 201 }) } catch (err) { const msg = err instanceof Error ? err.message : 'internal server error' diff --git a/src/middleware.ts b/src/middleware.ts index 92438f5..4a4346b 100644 --- a/src/middleware.ts +++ b/src/middleware.ts @@ -3,6 +3,30 @@ import { NextRequest, NextResponse } from 'next/server' // Middleware: protects all routes except public ones. // Uses a simple session cookie (HMAC-signed) for auth. // Password is set via ADMIN_PASSWORD env var. +// Also applies global rate limiting to API routes. + +// --- Simple in-memory rate limiter (per-IP, per-minute) --- +const rateLimitStore = new Map() +const RATE_LIMIT_MAX = 120 // 120 requests per minute per IP +const RATE_LIMIT_WINDOW = 60_000 // 1 minute + +function checkRateLimit(ip: string): boolean { + const now = Date.now() + const entry = rateLimitStore.get(ip) + if (!entry || entry.resetAt < now) { + rateLimitStore.set(ip, { count: 1, resetAt: now + RATE_LIMIT_WINDOW }) + return true + } + if (entry.count >= RATE_LIMIT_MAX) return false + entry.count++ + return true +} + +function getClientIp(req: NextRequest): string { + return req.headers.get('x-forwarded-for')?.split(',')[0]?.trim() + ?? req.headers.get('x-real-ip') + ?? 'unknown' +} const PUBLIC_PATHS = [ '/api/health', @@ -91,6 +115,17 @@ async function verifySession(token: string | undefined): Promise { export async function middleware(req: NextRequest) { const { pathname } = req.nextUrl + // --- Global rate limiting for API routes --- + if (pathname.startsWith('/api/')) { + const ip = getClientIp(req) + if (!checkRateLimit(ip)) { + return NextResponse.json( + { error: 'rate_limit_exceeded', message: 'Too many requests. Try again in a minute.' }, + { status: 429, headers: { 'retry-after': '60' } } + ) + } + } + // Allow public paths if (isPublic(pathname)) { return NextResponse.next()