- 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>
135 lines
3.3 KiB
TypeScript
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 }
|
|
);
|
|
}
|
|
}
|