Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
73 changes: 25 additions & 48 deletions src/app/api/cron/process-followups/route.ts
Original file line number Diff line number Diff line change
@@ -1,38 +1,32 @@
import { NextRequest, NextResponse } from "next/server";
import { processDueReplyFollowups } from "@/lib/replyFollowup";
import { NextRequest, NextResponse } from 'next/server';
import { processDueReplyFollowups } from '@/lib/replyFollowup';
import { verifyCronAuth } from '@/lib/cronAuth';

/**
* POST /api/cron/process-followups
* /api/cron/process-followups
*
* Cron endpoint for processing scheduled silent-reply follow-ups. Mirrors the
* auth shape of /api/cron/process-queue (Bearer CRON_SECRET) so the same cron
* runner can hit both.
* Cron endpoint for processing scheduled silent-reply follow-ups. Accepts
* both GET (used by Vercel Cron) and POST (manual triggers). Auth is
* `Authorization: Bearer $CRON_SECRET` for both. Mirrors process-queue.
*
* Recommended frequency: every 5 minutes.
*/

export async function POST(request: NextRequest) {
try {
const authHeader = request.headers.get("authorization");
const cronSecret = process.env.CRON_SECRET;

if (!cronSecret) {
console.error("CRON_SECRET not configured");
return NextResponse.json(
{ success: false, error: "Server misconfiguration" },
{ status: 500 }
);
}

if (!authHeader || authHeader !== `Bearer ${cronSecret}`) {
return NextResponse.json(
{ success: false, error: "Unauthorized" },
{ status: 401 }
);
}
async function handle(request: NextRequest) {
const auth = verifyCronAuth({
authorizationHeader: request.headers.get('authorization'),
cronSecret: process.env.CRON_SECRET,
});
if (!auth.ok) {
if (auth.status === 500) console.error('CRON_SECRET not configured');
return NextResponse.json(
{ success: false, error: auth.error },
{ status: auth.status }
);
}

try {
const result = await processDueReplyFollowups(50);

return NextResponse.json({
success: true,
result: {
Expand All @@ -44,34 +38,17 @@ export async function POST(request: NextRequest) {
},
});
} catch (error) {
console.error("Error in follow-up processing cron:", error);

console.error('Error in follow-up processing cron:', error);
return NextResponse.json(
{
success: false,
error: "Failed to process follow-ups",
details: error instanceof Error ? error.message : "Unknown error",
error: 'Failed to process follow-ups',
details: error instanceof Error ? error.message : 'Unknown error',
},
{ status: 500 }
);
}
}

export async function GET(request: NextRequest) {
const authHeader = request.headers.get("authorization");
const cronSecret = process.env.CRON_SECRET;

if (!cronSecret || !authHeader || authHeader !== `Bearer ${cronSecret}`) {
return NextResponse.json(
{ success: false, error: "Unauthorized" },
{ status: 401 }
);
}

return NextResponse.json({
success: true,
message: "Follow-up processing cron endpoint is healthy",
endpoint: "/api/cron/process-followups",
method: "POST",
});
}
export const GET = handle;
export const POST = handle;
70 changes: 21 additions & 49 deletions src/app/api/cron/process-queue/route.ts
Original file line number Diff line number Diff line change
@@ -1,41 +1,32 @@
import { NextRequest, NextResponse } from 'next/server';
import { processEmailQueue } from '@/lib/emailQueueProcessor';
import { verifyCronAuth } from '@/lib/cronAuth';

/**
* POST /api/cron/process-queue
* /api/cron/process-queue
*
* Cron endpoint for processing the email queue.
* Protected by CRON_SECRET environment variable.
* Cron endpoint for processing the email queue. Accepts both GET (used by
* Vercel Cron, which only sends GET) and POST (manual triggers / external
* runners). Auth is `Authorization: Bearer $CRON_SECRET` for both.
*
* Configure with Vercel Cron, GitHub Actions, or external cron service:
* - Recommended frequency: Every 5 minutes
* - Header: Authorization: Bearer YOUR_CRON_SECRET
* Recommended frequency: every 5 minutes.
*/

export async function POST(request: NextRequest) {
try {
// Verify CRON_SECRET
const authHeader = request.headers.get('authorization');
const cronSecret = process.env.CRON_SECRET;

if (!cronSecret) {
console.error('CRON_SECRET not configured');
return NextResponse.json(
{ success: false, error: 'Server misconfiguration' },
{ status: 500 }
);
}

if (!authHeader || authHeader !== `Bearer ${cronSecret}`) {
return NextResponse.json(
{ success: false, error: 'Unauthorized' },
{ status: 401 }
);
}
async function handle(request: NextRequest) {
const auth = verifyCronAuth({
authorizationHeader: request.headers.get('authorization'),
cronSecret: process.env.CRON_SECRET,
});
if (!auth.ok) {
if (auth.status === 500) console.error('CRON_SECRET not configured');
return NextResponse.json(
{ success: false, error: auth.error },
{ status: auth.status }
);
}

// Process the queue (default batch size: 50 for cron jobs)
try {
const result = await processEmailQueue(50);

return NextResponse.json({
success: true,
result: {
Expand All @@ -48,7 +39,6 @@ export async function POST(request: NextRequest) {
});
} catch (error) {
console.error('Error in queue processing cron:', error);

return NextResponse.json(
{
success: false,
Expand All @@ -60,23 +50,5 @@ export async function POST(request: NextRequest) {
}
}

// Support GET for health checks
export async function GET(request: NextRequest) {
// Verify CRON_SECRET for GET as well
const authHeader = request.headers.get('authorization');
const cronSecret = process.env.CRON_SECRET;

if (!cronSecret || !authHeader || authHeader !== `Bearer ${cronSecret}`) {
return NextResponse.json(
{ success: false, error: 'Unauthorized' },
{ status: 401 }
);
}

return NextResponse.json({
success: true,
message: 'Queue processing cron endpoint is healthy',
endpoint: '/api/cron/process-queue',
method: 'POST',
});
}
export const GET = handle;
export const POST = handle;
70 changes: 21 additions & 49 deletions src/app/api/cron/refresh-tokens/route.ts
Original file line number Diff line number Diff line change
@@ -1,41 +1,32 @@
import { NextRequest, NextResponse } from 'next/server';
import { refreshExpiringTokens } from '@/lib/tokenRefreshJob';
import { verifyCronAuth } from '@/lib/cronAuth';

/**
* POST /api/cron/refresh-tokens
* /api/cron/refresh-tokens
*
* Cron endpoint for refreshing expiring OAuth tokens.
* Protected by CRON_SECRET environment variable.
* Cron endpoint for refreshing expiring OAuth tokens. Accepts both GET (used
* by Vercel Cron) and POST (manual triggers). Auth is
* `Authorization: Bearer $CRON_SECRET` for both.
*
* Configure with Vercel Cron, GitHub Actions, or external cron service:
* - Recommended frequency: Every 30 minutes
* - Header: Authorization: Bearer YOUR_CRON_SECRET
* Recommended frequency: every 30 minutes.
*/

export async function POST(request: NextRequest) {
try {
// Verify CRON_SECRET
const authHeader = request.headers.get('authorization');
const cronSecret = process.env.CRON_SECRET;

if (!cronSecret) {
console.error('CRON_SECRET not configured');
return NextResponse.json(
{ success: false, error: 'Server misconfiguration' },
{ status: 500 }
);
}

if (!authHeader || authHeader !== `Bearer ${cronSecret}`) {
return NextResponse.json(
{ success: false, error: 'Unauthorized' },
{ status: 401 }
);
}
async function handle(request: NextRequest) {
const auth = verifyCronAuth({
authorizationHeader: request.headers.get('authorization'),
cronSecret: process.env.CRON_SECRET,
});
if (!auth.ok) {
if (auth.status === 500) console.error('CRON_SECRET not configured');
return NextResponse.json(
{ success: false, error: auth.error },
{ status: auth.status }
);
}

// Refresh expiring tokens
try {
const result = await refreshExpiringTokens();

return NextResponse.json({
success: true,
result: {
Expand All @@ -46,7 +37,6 @@ export async function POST(request: NextRequest) {
});
} catch (error) {
console.error('Error in token refresh cron:', error);

return NextResponse.json(
{
success: false,
Expand All @@ -58,23 +48,5 @@ export async function POST(request: NextRequest) {
}
}

// Support GET for health checks
export async function GET(request: NextRequest) {
// Verify CRON_SECRET for GET as well
const authHeader = request.headers.get('authorization');
const cronSecret = process.env.CRON_SECRET;

if (!cronSecret || !authHeader || authHeader !== `Bearer ${cronSecret}`) {
return NextResponse.json(
{ success: false, error: 'Unauthorized' },
{ status: 401 }
);
}

return NextResponse.json({
success: true,
message: 'Token refresh cron endpoint is healthy',
endpoint: '/api/cron/refresh-tokens',
method: 'POST',
});
}
export const GET = handle;
export const POST = handle;
30 changes: 30 additions & 0 deletions src/lib/cronAuth.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
export type CronAuthFailure =
| { ok: false; status: 500; error: 'Server misconfiguration' }
| { ok: false; status: 401; error: 'Unauthorized' }

export type CronAuthSuccess = { ok: true }

export type CronAuthResult = CronAuthFailure | CronAuthSuccess

/**
* Validate the `Authorization: Bearer <CRON_SECRET>` header used by every
* `/api/cron/*` route. Pure function so route handlers stay thin and the
* auth contract is unit-testable.
*
* Vercel Cron sends GET requests with the project's `CRON_SECRET` injected
* automatically — the same env var that POST callers use, so a single helper
* covers both methods.
*/
export function verifyCronAuth(input: {
authorizationHeader: string | null
cronSecret: string | undefined
}): CronAuthResult {
if (!input.cronSecret) {
return { ok: false, status: 500, error: 'Server misconfiguration' }
}
const expected = `Bearer ${input.cronSecret}`
if (!input.authorizationHeader || input.authorizationHeader !== expected) {
return { ok: false, status: 401, error: 'Unauthorized' }
}
return { ok: true }
}
Loading