import { NextRequest, NextResponse } from "next/server"; import { getRequestAuditContext } from "@/lib/audit"; import { getCampaignRetryState, runCampaignRetryBatch } from "@/lib/campaign-dispatch-service"; import { consumeRateLimit, getRateLimitHeaders } from "@/lib/rate-limit"; type JobPayload = { tenantId?: string; campaignId?: string; recipientBatchSize?: number; maxCampaigns?: number; }; function isAuthorized(req: NextRequest) { const expected = process.env.CAMPAIGN_RETRY_JOB_TOKEN?.trim(); if (!expected) { return process.env.NODE_ENV !== "production"; } const tokenFromHeader = req.headers.get("authorization")?.trim() || req.headers.get("x-cron-token")?.trim(); const tokenFromQuery = new URL(req.url).searchParams.get("token")?.trim(); const token = tokenFromHeader || tokenFromQuery; if (!token) { return false; } return token === expected || token === `Bearer ${expected}`; } function resolveNumber(raw: string | undefined, fallback: number) { const value = Number(raw?.trim()); if (!Number.isInteger(value) || value <= 0) { return fallback; } return value; } export async function GET(req: NextRequest) { const { ipAddress: requestIpAddress } = await getRequestAuditContext(); const retryRate = consumeRateLimit(requestIpAddress || "unknown", { scope: "campaign_retry_job_get", limit: resolveNumber(process.env.CAMPAIGN_RETRY_JOB_RATE_LIMIT_GET, 60), windowMs: resolveNumber(process.env.CAMPAIGN_RETRY_JOB_RATE_LIMIT_WINDOW_MS, 60 * 1000) }); if (!retryRate.allowed) { return NextResponse.json( { ok: false, error: "Too many requests. Please retry later." }, { status: 429, headers: getRateLimitHeaders(retryRate) } ); } if (!isAuthorized(req)) { return NextResponse.json({ ok: false, error: "Unauthorized" }, { status: 401 }); } const state = await getCampaignRetryState(); const now = new Date(); const lockedUntil = state?.lockedUntil ? new Date(state.lockedUntil) : null; const health = { isLocked: Boolean(lockedUntil && lockedUntil > now), isStaleLock: Boolean(lockedUntil && lockedUntil <= now), lastRunStartedAt: state?.lastRunStartedAt ?? null, lastRunCompletedAt: state?.lastRunCompletedAt ?? null, lastRunStatus: state?.lastRunStatus ?? null }; return NextResponse.json({ ok: true, state, health }); } export async function POST(req: NextRequest) { const { ipAddress: requestIpAddress } = await getRequestAuditContext(); const retryRate = consumeRateLimit(requestIpAddress || "unknown", { scope: "campaign_retry_job_post", limit: resolveNumber(process.env.CAMPAIGN_RETRY_JOB_RATE_LIMIT_POST, 20), windowMs: resolveNumber(process.env.CAMPAIGN_RETRY_JOB_RATE_LIMIT_WINDOW_MS, 60 * 1000) }); if (!retryRate.allowed) { return NextResponse.json( { ok: false, error: "Too many requests. Please retry later." }, { status: 429, headers: getRateLimitHeaders(retryRate) } ); } if (!isAuthorized(req)) { return NextResponse.json({ ok: false, error: "Unauthorized" }, { status: 401 }); } let payload: unknown = {}; try { payload = (await req.json()) as unknown; } catch { payload = {}; } const safePayload = payload as JobPayload; const tenantId = safePayload?.tenantId?.trim?.() || undefined; const campaignId = safePayload?.campaignId?.trim?.() || undefined; const recipientBatchSize = Number.isInteger(safePayload?.recipientBatchSize) ? safePayload?.recipientBatchSize : undefined; const maxCampaigns = Number.isInteger(safePayload?.maxCampaigns) ? safePayload?.maxCampaigns : undefined; const { ipAddress, userAgent } = await getRequestAuditContext(); try { const result = await runCampaignRetryBatch({ campaignId, tenantId, actorIpAddress: ipAddress, actorUserAgent: userAgent, actorUserId: null, recipientBatchSize, maxCampaigns }); return NextResponse.json({ ok: true, ...result }); } catch (error) { const message = error instanceof Error ? error.message : "Campaign retry job failed"; return NextResponse.json({ ok: false, error: message }, { status: 500 }); } }