import { createHmac } from 'crypto'; import { NextRequest, NextResponse } from 'next/server'; import { eq } from 'drizzle-orm'; import { db } from '@/lib/db'; import { jobs } from '@/lib/db/schema'; import { getUsersWithDueReviews } from '@/lib/db/queries'; import { reviewDigestEmail } from '@/lib/email/templates'; import { sendEmail } from '@/lib/email'; const BASE_URL = process.env.AUTH_URL ?? 'http://localhost:3000'; export function signUserId(userId: string): string { const secret = process.env.UNSUBSCRIBE_SECRET ?? 'dev-secret-change-in-prod'; return createHmac('sha256', secret).update(userId).digest('hex'); } export async function GET(request: NextRequest): Promise { // Gate by env flag — feature is opt-in if (process.env.REVIEW_DIGEST_ENABLED !== 'true') { return NextResponse.json({ skipped: true, reason: 'REVIEW_DIGEST_ENABLED is not set' }); } // Auth: Vercel Cron sends Authorization: Bearer const cronSecret = process.env.CRON_SECRET; if (cronSecret) { const auth = request.headers.get('Authorization'); if (auth !== `Bearer ${cronSecret}`) { return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); } } const now = new Date(); const dateKey = now.toISOString().slice(0, 10); // yyyy-mm-dd const recipients = await getUsersWithDueReviews(now); let sent = 0; let skipped = 0; for (const recipient of recipients) { const idempotencyKey = `digest:${recipient.userId}:${dateKey}`; // Idempotency: skip if already sent today const [existing] = await db .select({ id: jobs.id, status: jobs.status, attempts: jobs.attempts }) .from(jobs) .where(eq(jobs.idempotencyKey, idempotencyKey)) .limit(1); if (existing?.status === 'done') { skipped++; continue; } // Insert or update job row if (!existing) { await db.insert(jobs).values({ type: 'review-digest', idempotencyKey, status: 'running', payloadJson: { userId: recipient.userId, date: dateKey }, attempts: 1, }).onConflictDoNothing(); } else { await db .update(jobs) .set({ status: 'running', attempts: (existing.attempts ?? 0) + 1 }) .where(eq(jobs.id, existing.id)); } const sig = signUserId(recipient.userId); const unsubscribeUrl = `${BASE_URL}/api/unsubscribe?userId=${encodeURIComponent(recipient.userId)}&sig=${sig}`; const reviewUrl = `${BASE_URL}/review`; const email = reviewDigestEmail({ name: recipient.name, dueCount: recipient.dueCount, topConcepts: recipient.topConcepts, reviewUrl, unsubscribeUrl, locale: recipient.locale, }); await sendEmail({ to: recipient.email, ...email }); await db .update(jobs) .set({ status: 'done' }) .where(eq(jobs.idempotencyKey, idempotencyKey)); sent++; } return NextResponse.json({ sent, skipped, total: recipients.length, date: dateKey }); }