import { NextRequest, NextResponse } from "next/server"; import crypto from "node:crypto"; import { db, cookingHistory, eq, and, isNotNull, isNull } from "@epicure/db"; import { sendEmail, notificationEmailHtml } from "@/lib/email"; import { sendPushNotification } from "@/lib/push"; import { isLeftoverExpiringSoon } from "@/lib/leftover-match"; import { getMessages, formatMessage } from "@/lib/i18n/server"; // Internal cron endpoint — triggered daily by a cron container (see // compose.prod.yml / cron/crontab). Not part of the public API surface; // protected by a shared secret rather than user auth. // // For every cooking_history row tied to a batch-cook dish, checks whether it // expires soon (see lib/leftover-match.ts) and hasn't already been reminded // about, then sends one push + email and marks it reminded so it never fires // twice for the same cooked dish. function isAuthorized(req: NextRequest): boolean { const secret = process.env["CRON_SECRET"]; if (!secret) return false; const header = req.headers.get("authorization"); if (!header?.startsWith("Bearer ")) return false; const provided = header.slice("Bearer ".length); const a = Buffer.from(provided); const b = Buffer.from(secret); if (a.length !== b.length) return false; return crypto.timingSafeEqual(a, b); } export async function POST(req: NextRequest) { if (!isAuthorized(req)) { return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); } const candidates = await db.query.cookingHistory.findMany({ where: and(isNotNull(cookingHistory.batchDishId), isNull(cookingHistory.expiryReminderSentAt)), with: { batchDish: { columns: { id: true, name: true, fridgeDays: true } }, recipe: { columns: { id: true, title: true } }, user: { columns: { id: true, email: true, locale: true } }, }, }); let sent = 0; for (const log of candidates) { if (!log.batchDish || !log.user) continue; if (!isLeftoverExpiringSoon(log.cookedAt, log.batchDish.fridgeDays)) continue; const messages = getMessages(log.user.locale); const template = messages.notifications.detail.leftoverExpiring; const title = messages.notifications.pushTitle.leftoverExpiring; const body = formatMessage(template, { dish: log.batchDish.name, title: log.recipe.title }); const url = `/recipes/${log.recipe.id}`; await Promise.all([ sendPushNotification(log.user.id, { title, body, url }).catch((err) => { console.error("[leftover-reminders] push failed", err); }), log.user.email ? sendEmail({ to: log.user.email, subject: title, html: notificationEmailHtml(title, body, `${process.env["BETTER_AUTH_URL"] ?? "http://localhost:3000"}${url}`), }).catch((err) => { console.error("[leftover-reminders] email failed", err); }) : Promise.resolve(), ]); await db.update(cookingHistory) .set({ expiryReminderSentAt: new Date() }) .where(eq(cookingHistory.id, log.id)); sent++; } return NextResponse.json({ ok: true, checked: candidates.length, sent }); }