Files
whatsapp-inbox-platform/app/api/jobs/campaign-retry/route.ts
Wira Basalamah adde003fba
Some checks failed
CI - Production Readiness / Verify (push) Has been cancelled
chore: initial project import
2026-04-21 09:29:29 +07:00

134 lines
4.1 KiB
TypeScript

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 });
}
}