fix: resolve TODO.md security/perf/test-coverage backlog

Fixes the 13-item codebase health scan backlog: wraps meal-plan
generation in a transaction, adds missing userId/GIN indexes, fixes
an IPv6-parsing gap in the webhook SSRF guard (and an identical
duplicated bug in the AI URL-import path, now consolidated onto one
implementation), paginates the collections list, dedupes the AI
recipe Zod schemas, wires up Stripe tier sync, rate-limits AI key
rotation, gets `pnpm typecheck` actually working, and adds test
coverage for the previously-untested admin/webhooks routes.

Two flagged issues (collection removeRecipeId IDOR, tier-limit race)
turned out to already be fixed/non-issues on inspection — noted in
TODO.md rather than silently dropped.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Arnaud
2026-07-02 12:12:42 +02:00
parent 2154512e54
commit d2faf98ac1
38 changed files with 7598 additions and 315 deletions
+15 -7
View File
@@ -1,5 +1,6 @@
import { NextRequest, NextResponse } from "next/server";
import crypto from "node:crypto";
import { db, users, eq } from "@epicure/db";
// Stripe webhook handler — verifies stripe-signature header using HMAC-SHA256.
// Handles:
@@ -74,16 +75,23 @@ export async function POST(req: NextRequest) {
return NextResponse.json({ error: "Invalid JSON" }, { status: 400 });
}
// TODO: wire up DB calls when Stripe billing is fully configured
switch (event.type) {
case "checkout.session.completed":
// upgrade user to pro
console.log("[stripe-webhook] checkout.session.completed", event.data.object["id"]);
case "checkout.session.completed": {
// client_reference_id is set to our internal userId when the Checkout Session is created.
const userId = event.data.object["client_reference_id"];
const customerId = event.data.object["customer"];
if (typeof userId === "string" && typeof customerId === "string") {
await db.update(users).set({ tier: "pro", stripeCustomerId: customerId }).where(eq(users.id, userId));
}
break;
case "customer.subscription.deleted":
// downgrade user to free
console.log("[stripe-webhook] customer.subscription.deleted", event.data.object["id"]);
}
case "customer.subscription.deleted": {
const customerId = event.data.object["customer"];
if (typeof customerId === "string") {
await db.update(users).set({ tier: "free" }).where(eq(users.stripeCustomerId, customerId));
}
break;
}
default:
// ignore unhandled event types
break;