Files
Epicure/apps/web/app/api/v1/ai-keys/route.ts
T
Arnaud d2faf98ac1 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>
2026-07-02 12:12:42 +02:00

61 lines
1.8 KiB
TypeScript

import { NextResponse } from "next/server";
import { z } from "zod";
import { db, userAiKeys, eq, and } from "@epicure/db";
import { requireSession } from "@/lib/api-auth";
import { encrypt } from "@/lib/encrypt";
import { applyRateLimit } from "@/lib/rate-limit";
const VALID_PROVIDERS = ["openai", "anthropic", "openrouter", "ollama"] as const;
const PostSchema = z.object({
provider: z.enum(VALID_PROVIDERS),
apiKey: z.string().min(1).max(500),
});
export async function GET() {
const { session, response } = await requireSession();
if (response) return response;
const keys = await db.query.userAiKeys.findMany({
where: eq(userAiKeys.userId, session!.user.id),
columns: { provider: true, createdAt: true },
});
return NextResponse.json({ keys });
}
export async function POST(req: Request) {
const { session, response } = await requireSession();
if (response) return response;
const limited = await applyRateLimit(`rl:ai-keys:${session!.user.id}`, 5, 3600);
if (limited) return limited;
const body = PostSchema.safeParse(await req.json());
if (!body.success) return NextResponse.json({ error: body.error.flatten() }, { status: 400 });
const { provider, apiKey } = body.data;
const userId = session!.user.id;
const existing = await db.query.userAiKeys.findFirst({
where: and(eq(userAiKeys.userId, userId), eq(userAiKeys.provider, provider)),
});
const encryptedKey = encrypt(apiKey);
if (existing) {
await db.update(userAiKeys)
.set({ encryptedKey })
.where(and(eq(userAiKeys.userId, userId), eq(userAiKeys.provider, provider)));
} else {
await db.insert(userAiKeys).values({
id: crypto.randomUUID(),
userId,
provider,
encryptedKey,
});
}
return NextResponse.json({ ok: true });
}