Files
Epicure/apps/web/app/api/internal/cron/chat-cleanup/route.ts
T
Arnaud adaa837564 feat: clear chat history manually, auto-expire old history after 90 days
Manual: DELETE /api/v1/ai/chat-history (scoped by recipeId or scope=general,
matching GET's scoping) plus a trash-icon button + confirm dialog in both
chat panels' headers.

Automatic: new internal cron endpoint (chat-cleanup), same shared-secret
pattern as the existing leftover-reminders/weekly-digest crons, deletes
any chat_messages older than 90 days. Wired into cron/crontab (daily,
03:00 UTC) and the Dockerfile's cron stage.

Verified locally: cleared a real conversation through the actual UI and
confirmed it didn't come back on reopen (not just cleared client-side);
inserted a 100-day-old and a 5-day-old message directly, called the cron
endpoint with the real shared-secret check, confirmed only the old one
was deleted and the recent one survived; confirmed the endpoint 401s with
no/wrong secret.
2026-07-12 23:27:05 +02:00

40 lines
1.4 KiB
TypeScript

import { NextRequest, NextResponse } from "next/server";
import crypto from "node:crypto";
import { db, chatMessages, lt } from "@epicure/db";
// 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.
//
// AI chat history (both the per-recipe chat and the general cooking
// assistant) has no size cap and no per-user retention setting — this just
// deletes anything past a fixed retention window so the table doesn't grow
// unbounded.
const RETENTION_DAYS = 90;
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 cutoff = new Date(Date.now() - RETENTION_DAYS * 24 * 60 * 60 * 1000);
const deleted = await db.delete(chatMessages).where(lt(chatMessages.createdAt, cutoff)).returning({ id: chatMessages.id });
return NextResponse.json({ ok: true, deleted: deleted.length });
}