Files
agentlens/apps/web/src/app/api/decisions/route.ts
Vectry 61268f870f feat: user auth, API keys, Stripe billing, and dashboard scoping
- NextAuth v5 credentials auth with registration/login pages
- API key CRUD (create, list, revoke) with secure hashing
- Stripe checkout, webhooks, and customer portal integration
- Rate limiting per subscription tier
- All dashboard API endpoints scoped to authenticated user
- Prisma schema: User, Account, Session, ApiKey, plus Stripe fields
- Auth middleware protecting dashboard and API routes

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-Claude)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
2026-02-10 15:37:49 +00:00

135 lines
3.3 KiB
TypeScript

import { NextRequest, NextResponse } from "next/server";
import { prisma } from "@/lib/prisma";
import { Prisma } from "@agentlens/database";
import { auth } from "@/auth";
export async function GET(request: NextRequest) {
try {
const session = await auth();
if (!session?.user?.id) {
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
}
const { searchParams } = new URL(request.url);
const page = parseInt(searchParams.get("page") ?? "1", 10);
const limit = parseInt(searchParams.get("limit") ?? "20", 10);
const type = searchParams.get("type");
const search = searchParams.get("search");
const sort = searchParams.get("sort") ?? "newest";
// Validate pagination
if (isNaN(page) || page < 1) {
return NextResponse.json(
{ error: "Invalid page parameter. Must be a positive integer." },
{ status: 400 }
);
}
if (isNaN(limit) || limit < 1 || limit > 100) {
return NextResponse.json(
{ error: "Invalid limit parameter. Must be between 1 and 100." },
{ status: 400 }
);
}
// Validate type
const validTypes = [
"TOOL_SELECTION",
"ROUTING",
"RETRY",
"ESCALATION",
"MEMORY_RETRIEVAL",
"PLANNING",
"CUSTOM",
];
if (type && !validTypes.includes(type)) {
return NextResponse.json(
{ error: `Invalid type. Must be one of: ${validTypes.join(", ")}` },
{ status: 400 }
);
}
// Validate sort
const validSorts = ["newest", "oldest", "costliest"];
if (!validSorts.includes(sort)) {
return NextResponse.json(
{ error: `Invalid sort. Must be one of: ${validSorts.join(", ")}` },
{ status: 400 }
);
}
const where: Prisma.DecisionPointWhereInput = {
trace: { userId: session.user.id },
};
if (type) {
where.type = type as Prisma.EnumDecisionTypeFilter["equals"];
}
if (search) {
where.reasoning = {
contains: search,
mode: "insensitive",
};
}
// Build order by
let orderBy: Prisma.DecisionPointOrderByWithRelationInput;
switch (sort) {
case "oldest":
orderBy = { timestamp: "asc" };
break;
case "costliest":
orderBy = { costUsd: "desc" };
break;
case "newest":
default:
orderBy = { timestamp: "desc" };
break;
}
// Count total
const total = await prisma.decisionPoint.count({ where });
// Pagination
const skip = (page - 1) * limit;
const totalPages = Math.ceil(total / limit);
// Fetch decisions with parent trace and span
const decisions = await prisma.decisionPoint.findMany({
where,
include: {
trace: {
select: {
id: true,
name: true,
},
},
span: {
select: {
id: true,
name: true,
},
},
},
orderBy,
skip,
take: limit,
});
return NextResponse.json(
{
decisions,
total,
page,
limit,
totalPages,
},
{ status: 200 }
);
} catch (error) {
console.error("Error listing decisions:", error);
return NextResponse.json(
{ error: "Internal server error" },
{ status: 500 }
);
}
}