adaa837564
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.
40 lines
1.4 KiB
TypeScript
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 });
|
|
}
|