From 7115ce84841a6faea68d343d6d91e6cfc8dbbc3a Mon Sep 17 00:00:00 2001 From: charles-gauthereau Date: Mon, 25 May 2026 15:03:21 +0200 Subject: [PATCH] feat(api-v1): add GET /agents and POST /agents --- app/api/v1/agents/route.ts | 93 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 93 insertions(+) create mode 100644 app/api/v1/agents/route.ts diff --git a/app/api/v1/agents/route.ts b/app/api/v1/agents/route.ts new file mode 100644 index 00000000..26451d36 --- /dev/null +++ b/app/api/v1/agents/route.ts @@ -0,0 +1,93 @@ +import { NextResponse } from "next/server"; +import { withApiKey, ApiKeyContext } from "@/lib/api-v1/middleware"; +import { getAccessibleAgentIds } from "@/lib/api-v1/acl"; +import { db } from "@/db"; +import * as drizzleDb from "@/db"; +import { inArray, and, eq, count } from "drizzle-orm"; +import { z } from "zod"; +import { slugify } from "@/utils/slugify"; +import { logger } from "@/lib/logger"; + +const log = logger.child({ module: "api/v1/agents" }); + +export const GET = withApiKey(async (_req: Request, ctx: ApiKeyContext) => { + try { + const agentIds = await getAccessibleAgentIds(ctx.userId); + + if (agentIds.length === 0) { + return NextResponse.json({ data: [] }); + } + + const agents = await db.query.agent.findMany({ + where: and( + inArray(drizzleDb.schemas.agent.id, agentIds), + eq(drizzleDb.schemas.agent.isArchived, false) + ), + }); + + return NextResponse.json({ data: agents }); + } catch (error) { + log.error({ error }, "Error in GET /api/v1/agents"); + return NextResponse.json({ error: "Internal server error" }, { status: 500 }); + } +}); + +const CreateAgentSchema = z.object({ + name: z.string().min(1, "name is required"), + description: z.string().min(1, "description is required"), + organizationId: z.string().uuid("organizationId must be a valid UUID"), +}); + +export const POST = withApiKey(async (req: Request, ctx: ApiKeyContext) => { + try { + let body: unknown; + try { + body = await req.json(); + } catch { + return NextResponse.json({ error: "Invalid JSON body" }, { status: 422 }); + } + + const parsed = CreateAgentSchema.safeParse(body); + if (!parsed.success) { + return NextResponse.json( + { error: parsed.error.issues[0].message }, + { status: 422 } + ); + } + + const { name, description, organizationId } = parsed.data; + + if (!ctx.orgIds.includes(organizationId)) { + return NextResponse.json({ error: "Forbidden" }, { status: 403 }); + } + + const slug = slugify(name); + + const [existing] = await db + .select({ count: count() }) + .from(drizzleDb.schemas.agent) + .where(eq(drizzleDb.schemas.agent.slug, slug)); + + if (existing.count > 0) { + return NextResponse.json( + { error: "An agent with this name already exists" }, + { status: 422 } + ); + } + + const [createdAgent] = await db + .insert(drizzleDb.schemas.agent) + .values({ name, description, slug, organizationId }) + .returning(); + + await db.insert(drizzleDb.schemas.organizationAgent).values({ + organizationId, + agentId: createdAgent.id, + }); + + return NextResponse.json({ data: createdAgent }, { status: 201 }); + } catch (error) { + log.error({ error }, "Error in POST /api/v1/agents"); + return NextResponse.json({ error: "Internal server error" }, { status: 500 }); + } +});