diff --git a/app/(protected)/admin/page.tsx b/app/(protected)/admin/page.tsx index cb5f75d..0efd268 100644 --- a/app/(protected)/admin/page.tsx +++ b/app/(protected)/admin/page.tsx @@ -1,31 +1,31 @@ -import getCurrentUserRole from "@/lib/data/current-user-role"; -import { redirect } from "next/navigation"; -import React from "react"; -import { DataTable } from "./data-table"; -import { columns } from "./columns"; +// app/(protected)/admin/page.tsx +import { Metadata } from "next"; +import { auth } from "@/auth"; import prismaDB from "@/lib/prisma"; +import UserTable from "@/components/UserTable"; -const AdminPage = async () => { - const currentUserRole = await getCurrentUserRole(); +export const metadata: Metadata = { + title: "Admin • Dashboard", + description: "Promote users to admins or block/unblock them", +}; - if (currentUserRole === "USER") { - redirect("/"); +export default async function AdminPage() { + const session = await auth(); + if (session?.user.role !== "ADMIN") { + return

Not authorized.

; } - const data = await prismaDB.user.findMany(); + // fetch all users + const users = await prismaDB.user.findMany({ + select: { id: true, name: true, email: true, role: true, isBlocked: true }, + orderBy: { email: "asc" }, + }); return ( - <> -
-
-

Admin Dashboard

-
-
- -
-
- +
+

Admin Dashboard

+ {/* Render the client-side table */} + +
); -}; - -export default AdminPage; +} diff --git a/app/(protected)/layout.tsx b/app/(protected)/layout.tsx index 46ef87f..b4181f5 100644 --- a/app/(protected)/layout.tsx +++ b/app/(protected)/layout.tsx @@ -1,3 +1,4 @@ +// app/layout.tsx import type { Metadata } from "next"; import localFont from "next/font/local"; import "../globals.css"; @@ -18,31 +19,29 @@ export const metadata: Metadata = { export default async function RootLayout({ children, -}: Readonly<{ +}: { children: React.ReactNode; -}>) { - const userSession = await auth(); +}) { + // 1️⃣ Fetch the session on the server + const session = await auth(); return ( - - - + + {/* 2️⃣ Wrap in SessionProvider and pass the session */} + + {/* 3️⃣ ThemeProvider is now a client component that handles its own props */} +
-
- + {/* 4️⃣ Header can also consume useSession() client-side */} +
{children}
-
+
- + ); diff --git a/app/(protected)/raw-deals/page.tsx b/app/(protected)/raw-deals/page.tsx index 13a1eb8..2ee0f24 100644 --- a/app/(protected)/raw-deals/page.tsx +++ b/app/(protected)/raw-deals/page.tsx @@ -1,44 +1,43 @@ +// app/(protected)/raw-deals/page.tsx import React, { Suspense } from "react"; -import DealCardSkeleton from "@/components/skeletons/DealCardSkeleton"; import { Metadata } from "next"; -import prismaDB from "@/lib/prisma"; -import DealCard from "@/components/DealCard"; -import getCurrentUserRole from "@/lib/data/current-user-role"; +import { DealType } from "@prisma/client"; + import GetDeals, { GetAllDeals } from "@/app/actions/get-deal"; +import getCurrentUserRole from "@/lib/data/current-user-role"; import SearchDeals from "@/components/SearchDeal"; +import SearchEbitdaDeals from "@/components/SearchEbitdaDeals"; import Pagination from "@/components/pagination"; -import { setTimeout } from "timers/promises"; import DealTypeFilter from "@/components/DealTypeFilter"; -import { DealType } from "@prisma/client"; -import SearchDealsSkeleton from "@/components/skeletons/SearchDealsSkeleton"; -import SearchEbitdaDeals from "@/components/SearchEbitdaDeals"; -import DealTypeFilterSkeleton from "@/components/skeletons/DealTypeFilterSkeleton"; import UserDealFilter from "@/components/UserDealFilter"; import DealContainer from "@/components/DealContainer"; +import DealCardSkeleton from "@/components/skeletons/DealCardSkeleton"; +import SearchDealsSkeleton from "@/components/skeletons/SearchDealsSkeleton"; +import DealTypeFilterSkeleton from "@/components/skeletons/DealTypeFilterSkeleton"; + export const metadata: Metadata = { title: "Raw Deals", description: "View the raw deals", }; -// After type SearchParams = Promise<{ [key: string]: string | undefined }>; -const RawDealsPage = async (props: { searchParams: SearchParams }) => { +export default async function RawDealsPage(props: { searchParams: SearchParams }) { + // parse filters & pagination from URL const searchParams = await props.searchParams; const search = searchParams?.query || ""; const currentPage = Number(searchParams?.page) || 1; const limit = Number(searchParams?.limit) || 20; const offset = (currentPage - 1) * limit; - const ebitda = searchParams?.ebitda || ""; const userId = searchParams?.userId || ""; - // Ensure dealTypes is always an array const dealTypes = typeof searchParams?.dealType === "string" ? [searchParams.dealType] : searchParams?.dealType || []; + // fetch deals + count const { data, totalPages, totalCount } = await GetAllDeals({ search, offset, @@ -48,20 +47,22 @@ const RawDealsPage = async (props: { searchParams: SearchParams }) => { userId, }); + // server‐side role check const currentUserRole = await getCurrentUserRole(); + const isAdmin = currentUserRole === "ADMIN"; return (
+ {/* — Header & filters */}
-

Raw Deals

+

Raw Deals

- Browse through our collection of unprocessed deals gathered from - various sources including manual entries, bulk uploads, external - website scraping, and AI-inferred opportunities. + Browse our unprocessed deals from manual entries, bulk uploads, + website scraping, and AI inference.

-
+

Total Deals: {totalCount} @@ -79,6 +80,7 @@ const RawDealsPage = async (props: { searchParams: SearchParams }) => {

+ }> @@ -87,6 +89,7 @@ const RawDealsPage = async (props: { searchParams: SearchParams }) => {
+ {/* — Deal list (grid or list) */}
{data.length === 0 ? (
@@ -98,15 +101,16 @@ const RawDealsPage = async (props: { searchParams: SearchParams }) => { )}
+ + {/* — Pagination */}
); -}; - -export default RawDealsPage; +} diff --git a/app/api/admin/users/[id]/promote/route.ts b/app/api/admin/users/[id]/promote/route.ts new file mode 100644 index 0000000..bbd5401 --- /dev/null +++ b/app/api/admin/users/[id]/promote/route.ts @@ -0,0 +1,29 @@ +// app/api/admin/users/[id]/promote/route.ts +import { NextResponse } from "next/server"; +import { auth } from "@/auth"; +import prismaDB from "@/lib/prisma"; + +export async function POST( + request: Request, + context: { + // `params` is now a Promise that you must await + params: Promise<{ id: string }>; + } +) { + // 1️⃣ Auth check + const session = await auth(); + if (session?.user.role !== "ADMIN") { + return new NextResponse("Forbidden", { status: 403 }); + } + + // 2️⃣ Await the params before grabbing `id` + const { id } = await context.params; + + // 3️⃣ Promote in the database + await prismaDB.user.update({ + where: { id }, + data: { role: "ADMIN" }, + }); + + return NextResponse.json({ message: "User promoted" }); +} diff --git a/app/api/deals/[id]/route.ts b/app/api/deals/[id]/route.ts new file mode 100644 index 0000000..7ebc904 --- /dev/null +++ b/app/api/deals/[id]/route.ts @@ -0,0 +1,24 @@ +// app/api/deals/[id]/route.ts +import { NextResponse } from "next/server"; +import { auth } from "@/auth"; +import prismaDB from "@/lib/prisma"; + +export async function DELETE( + request: Request, + { params }: { params: { id: string } } +) { + // 1️⃣ get the current user session (server-side) + const session = await auth(); + + // 2️⃣ block non-admins + if (session?.user.role !== "ADMIN") { + return new NextResponse("Unauthorized", { status: 403 }); + } + + // 3️⃣ proceed with delete + await prismaDB.deal.delete({ + where: { id: params.id }, + }); + + return NextResponse.json({ success: true }); +} diff --git a/auth.ts b/auth.ts index ae8c6d2..d4db996 100644 --- a/auth.ts +++ b/auth.ts @@ -11,6 +11,7 @@ const adminEmails = [ "daigbe@darkalphacapital.com", "daigbe@gmail.com", "ayan@darkalphacapital.com", + "kshah77@asu.edu" ]; declare module "next-auth" { @@ -24,92 +25,58 @@ declare module "next-auth" { export const { handlers, signIn, signOut, auth } = NextAuth({ adapter: PrismaAdapter(prismaDB), - session: { strategy: "jwt" }, + + + session: { + strategy: "jwt", + maxAge: 30 * 24 * 60 * 60, + updateAge: 0, + }, + pages: { signIn: "/auth/login", - error: "/auth/error", + error: "/auth/error", }, + events: { - //this event is only triggered when we use an OAuth provider async linkAccount({ user }) { await prismaDB.user.update({ - where: { - id: user.id, - }, - data: { - emailVerified: new Date(), - }, + where: { id: user.id }, + data: { emailVerified: new Date() }, }); }, }, callbacks: { async session({ session, token }) { - if (token.sub && session.user) { - session.user.id = token.sub; - } - - if (token.role && session.user) { - session.user.role = token.role as UserRole; - } - - if (token.image && session.user) { + if (session.user) { + session.user.id = token.sub!; + session.user.role = token.role as UserRole; session.user.image = token.image as string; } - return session; }, - async signIn({ user, account }) { - const userEmail = user.email; - const currentUser = await getCurrentUserByEmail(userEmail!); - - if (currentUser?.isBlocked) { - return false; - } + async signIn({ user }) { + const currentUser = await getCurrentUserByEmail(user.email!); + if (currentUser?.isBlocked) return false; return true; }, - async jwt({ token, user, account }) { - if (!token.sub) return token; - // If there's a user object, it means the user signed in - // Update user role on every sign in - if (user) { - const userRole = determineRole(user.email!); - token.role = userRole; - await prismaDB.user.update({ - where: { - id: user.id, - }, - data: { - role: userRole, - }, + async jwt({ token }) { + if (token.sub) { + const dbUser = await prismaDB.user.findUnique({ + where: { id: token.sub }, + select: { role: true, image: true }, }); + if (dbUser) { + token.role = dbUser.role; + token.image = dbUser.image; + } } - - const existingUser = await prismaDB.user.findUnique({ - where: { - id: token.sub, - }, - }); - - if (!existingUser) return token; - - token.image = existingUser.image; - token.id = existingUser.id; - return token; }, }, - ...authConfig, -}); -// Example implementation of determineRole function -function determineRole(userEmail: string) { - // Access user properties like email, name, etc. - if (adminEmails.includes(userEmail)) { - return UserRole.ADMIN; - } else { - return UserRole.USER; - } -} + ...authConfig, +}); \ No newline at end of file diff --git a/components/DealCard.tsx b/components/DealCard.tsx index b014ccb..2b763f7 100644 --- a/components/DealCard.tsx +++ b/components/DealCard.tsx @@ -29,42 +29,45 @@ import { useToast } from "@/hooks/use-toast"; import DeleteDealFromDB from "@/app/actions/delete-deal"; import { cn } from "@/lib/utils"; -const DealCard = ({ - deal, - userRole, - className, - showActions = true, - showScreenButton = true, -}: { +interface DealCardProps { deal: Deal; userRole: UserRole; className?: string; showActions?: boolean; showScreenButton?: boolean; -}) => { +} + +export default function DealCard({ + deal, + userRole, + className, + showActions = true, + showScreenButton = true, +}: DealCardProps) { const editLink = `/raw-deals/${deal.id}/edit`; const detailLink = `/raw-deals/${deal.id}`; const screenLink = `/raw-deals/${deal.id}/screen`; - const { toast } = useToast(); - const formatCurrency = (amount: number) => { - return new Intl.NumberFormat("en-US", { + + const formatCurrency = (amount: number) => + new Intl.NumberFormat("en-US", { style: "currency", currency: "USD", notation: "compact", maximumFractionDigits: 1, }).format(amount); - }; const handleDelete = async () => { try { const response = await DeleteDealFromDB(deal.dealType, deal.id); toast({ - title: response.type === "success" ? "Deal Deleted" : "Error", + title: + response.type === "success" ? "Deal Deleted" : "Error", description: response.message, - variant: response.type === "success" ? "default" : "destructive", + variant: + response.type === "success" ? "default" : "destructive", }); - } catch (error) { + } catch { toast({ title: "Error", description: "Failed to delete deal", @@ -85,8 +88,10 @@ const DealCard = ({ {deal.dealCaption} + {showActions && (
+ {/* Edit always available */} @@ -106,6 +111,8 @@ const DealCard = ({ + + {/* Delete only for admins */} {userRole === "ADMIN" && ( @@ -129,6 +136,7 @@ const DealCard = ({ )}
+ } @@ -165,6 +173,7 @@ const DealCard = ({ /> )} + + {/* bulk actions (only in list view) */} {viewMode === "list" && (
- - - - - - - - Are you sure you want to delete these? - - - This action cannot be undone. This will permanently delete - these deals from the database. - - - - Cancel - + + + + + + + Are you sure you want to delete these? + + + This action cannot be undone. + + + + Cancel + + {isDeleting ? "Deleting..." : "Continue"} + + + + + )} + {/* screen selected stays available to everyone */} + + + + {!isAdmin && ( + + + Promote to Admin + + )} + + + {user.isBlocked ? "Unblock User" : "Block User"} + + + + ); +} diff --git a/components/UserTable.tsx b/components/UserTable.tsx new file mode 100644 index 0000000..b9b2fef --- /dev/null +++ b/components/UserTable.tsx @@ -0,0 +1,136 @@ +// components/UserTable.tsx +"use client"; + +import { useState } from "react"; +import { User } from "@prisma/client"; +import { Input } from "@/components/ui/input"; +import { Button } from "@/components/ui/button"; +import { + Popover, + PopoverTrigger, + PopoverContent, +} from "@/components/ui/popover"; +import { Checkbox } from "@/components/ui/checkbox"; +import { + Table, + TableHeader, + TableBody, + TableRow, + TableHead, + TableCell, +} from "@/components/ui/table"; +import UserActionMenu from "@/components/UserActionMenu"; + +interface UserTableProps { + initialUsers: Pick[]; +} + +export default function UserTable({ initialUsers }: UserTableProps) { + const [filter, setFilter] = useState(""); + const [visibleCols, setVisibleCols] = useState({ + select: true, + name: true, + email: true, + role: true, + blocked: true, + actions: true, + }); + + const filtered = initialUsers.filter((u) => + u.email.toLowerCase().includes(filter.toLowerCase()), + ); + + function toggleCol(key: keyof typeof visibleCols) { + setVisibleCols((v) => ({ ...v, [key]: !v[key] })); + } + + return ( +
+ {/* Filter & Columns controls */} +
+ setFilter(e.target.value)} + className="max-w-sm" + /> + + + + + + +
+ {( + [ + ["select", "Select"], + ["name", "Name"], + ["email", "Email"], + ["role", "Role"], + ["blocked", "Blocked"], + ["actions","Actions"], + ] as [keyof typeof visibleCols, string][] + ).map(([key, label]) => ( + + ))} +
+
+
+
+ + {/* Users table */} + + + + {visibleCols.select && } + {visibleCols.name && Name} + {visibleCols.email && Email} + {visibleCols.role && Role} + {visibleCols.blocked && Blocked} + {visibleCols.actions && ( + Actions + )} + + + + + {filtered.map((user) => ( + + {visibleCols.select && } + {visibleCols.name && {user.name ?? "—"}} + {visibleCols.email && {user.email}} + {visibleCols.role && {user.role}} + {visibleCols.blocked && ( + + + {user.isBlocked ? "Yes" : "No"} + + + )} + {visibleCols.actions && ( + + + + )} + + ))} + +
+
+ ); +} diff --git a/package-lock.json b/package-lock.json index 1cba5d7..86e7c0e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -214,17 +214,16 @@ } }, "node_modules/@auth/core": { - "version": "0.37.2", - "resolved": "https://registry.npmjs.org/@auth/core/-/core-0.37.2.tgz", - "integrity": "sha512-kUvzyvkcd6h1vpeMAojK2y7+PAV5H+0Cc9+ZlKYDFhDY31AlvsB+GW5vNO4qE3Y07KeQgvNO9U0QUx/fN62kBw==", + "version": "0.39.1", + "resolved": "https://registry.npmjs.org/@auth/core/-/core-0.39.1.tgz", + "integrity": "sha512-McD8slui0oOA1pjR5sPjLPl5Zm//nLP/8T3kr8hxIsvNLvsiudYvPHhDFPjh1KcZ2nFxCkZmP6bRxaaPd/AnLA==", + "license": "ISC", "dependencies": { "@panva/hkdf": "^1.2.1", - "@types/cookie": "0.6.0", - "cookie": "0.7.1", - "jose": "^5.9.3", - "oauth4webapi": "^3.0.0", - "preact": "10.11.3", - "preact-render-to-string": "5.2.3" + "jose": "^6.0.6", + "oauth4webapi": "^3.3.0", + "preact": "10.24.3", + "preact-render-to-string": "6.5.11" }, "peerDependencies": { "@simplewebauthn/browser": "^9.0.1", @@ -243,6 +242,15 @@ } } }, + "node_modules/@auth/core/node_modules/jose": { + "version": "6.0.11", + "resolved": "https://registry.npmjs.org/jose/-/jose-6.0.11.tgz", + "integrity": "sha512-QxG7EaliDARm1O1S8BGakqncGT9s25bKL1WSf6/oa17Tkqwi8D2ZNglqCF+DsYF88/rV66Q/Q2mFAy697E1DUg==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, "node_modules/@auth/prisma-adapter": { "version": "2.7.4", "resolved": "https://registry.npmjs.org/@auth/prisma-adapter/-/prisma-adapter-2.7.4.tgz", @@ -284,25 +292,6 @@ } } }, - "node_modules/@auth/prisma-adapter/node_modules/preact": { - "version": "10.24.3", - "resolved": "https://registry.npmjs.org/preact/-/preact-10.24.3.tgz", - "integrity": "sha512-Z2dPnBnMUfyQfSQ+GBdsGa16hz35YmLmtTLhM169uW944hYL6xzTYkJjC07j+Wosz733pMWx0fgON3JNw1jJQA==", - "license": "MIT", - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/preact" - } - }, - "node_modules/@auth/prisma-adapter/node_modules/preact-render-to-string": { - "version": "6.5.11", - "resolved": "https://registry.npmjs.org/preact-render-to-string/-/preact-render-to-string-6.5.11.tgz", - "integrity": "sha512-ubnauqoGczeGISiOh6RjX0/cdaF8v/oDXIjO85XALCQjwQP+SB4RDXXtvZ6yTYSjG+PC1QRP2AhPgCEsM2EvUw==", - "license": "MIT", - "peerDependencies": { - "preact": ">=10" - } - }, "node_modules/@babel/runtime": { "version": "7.26.0", "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.26.0.tgz", @@ -3431,11 +3420,6 @@ "integrity": "sha512-NLOpedx9o+rxo/X5ChbdiX6mS1atE4WHmEEIcR9NLenRVa5HoVjAvjafwU3FPTqnZEstpoqCaW7fagqSoTDNeg==", "license": "MIT" }, - "node_modules/@types/cookie": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.6.0.tgz", - "integrity": "sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==" - }, "node_modules/@types/d3-array": { "version": "3.2.1", "resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.1.tgz", @@ -5078,14 +5062,6 @@ "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", "dev": true }, - "node_modules/cookie": { - "version": "0.7.1", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz", - "integrity": "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==", - "engines": { - "node": ">= 0.6" - } - }, "node_modules/core-util-is": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", @@ -8933,11 +8909,12 @@ } }, "node_modules/next-auth": { - "version": "5.0.0-beta.25", - "resolved": "https://registry.npmjs.org/next-auth/-/next-auth-5.0.0-beta.25.tgz", - "integrity": "sha512-2dJJw1sHQl2qxCrRk+KTQbeH+izFbGFPuJj5eGgBZFYyiYYtvlrBeUw1E/OJJxTRjuxbSYGnCTkUIRsIIW0bog==", + "version": "5.0.0-beta.28", + "resolved": "https://registry.npmjs.org/next-auth/-/next-auth-5.0.0-beta.28.tgz", + "integrity": "sha512-2RDR1h3DJb4nizcd5UBBwC2gtyP7j/jTvVLvEtDaFSKUWNfou3Gek2uTNHSga/Q4I/GF+OJobA4mFbRaWJgIDQ==", + "license": "ISC", "dependencies": { - "@auth/core": "0.37.2" + "@auth/core": "0.39.1" }, "peerDependencies": { "@simplewebauthn/browser": "^9.0.1", @@ -9061,9 +9038,10 @@ } }, "node_modules/oauth4webapi": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/oauth4webapi/-/oauth4webapi-3.1.2.tgz", - "integrity": "sha512-KQZkNU+xn02lWrFu5Vjqg9E81yPtDSxUZorRHlLWVoojD+H/0GFbH59kcnz5Thdjj7c4/mYMBPj/mhvGe/kKXA==", + "version": "3.5.1", + "resolved": "https://registry.npmjs.org/oauth4webapi/-/oauth4webapi-3.5.1.tgz", + "integrity": "sha512-txg/jZQwcbaF7PMJgY7aoxc9QuCxHVFMiEkDIJ60DwDz3PbtXPQnrzo+3X4IRYGChIwWLabRBRpf1k9hO9+xrQ==", + "license": "MIT", "funding": { "url": "https://github.com/sponsors/panva" } @@ -9613,21 +9591,20 @@ "dev": true }, "node_modules/preact": { - "version": "10.11.3", - "resolved": "https://registry.npmjs.org/preact/-/preact-10.11.3.tgz", - "integrity": "sha512-eY93IVpod/zG3uMF22Unl8h9KkrcKIRs2EGar8hwLZZDU1lkjph303V9HZBwufh2s736U6VXuhD109LYqPoffg==", + "version": "10.24.3", + "resolved": "https://registry.npmjs.org/preact/-/preact-10.24.3.tgz", + "integrity": "sha512-Z2dPnBnMUfyQfSQ+GBdsGa16hz35YmLmtTLhM169uW944hYL6xzTYkJjC07j+Wosz733pMWx0fgON3JNw1jJQA==", + "license": "MIT", "funding": { "type": "opencollective", "url": "https://opencollective.com/preact" } }, "node_modules/preact-render-to-string": { - "version": "5.2.3", - "resolved": "https://registry.npmjs.org/preact-render-to-string/-/preact-render-to-string-5.2.3.tgz", - "integrity": "sha512-aPDxUn5o3GhWdtJtW0svRC2SS/l8D9MAgo2+AWml+BhDImb27ALf04Q2d+AHqUUOc6RdSXFIBVa2gxzgMKgtZA==", - "dependencies": { - "pretty-format": "^3.8.0" - }, + "version": "6.5.11", + "resolved": "https://registry.npmjs.org/preact-render-to-string/-/preact-render-to-string-6.5.11.tgz", + "integrity": "sha512-ubnauqoGczeGISiOh6RjX0/cdaF8v/oDXIjO85XALCQjwQP+SB4RDXXtvZ6yTYSjG+PC1QRP2AhPgCEsM2EvUw==", + "license": "MIT", "peerDependencies": { "preact": ">=10" } @@ -9734,11 +9711,6 @@ } } }, - "node_modules/pretty-format": { - "version": "3.8.0", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-3.8.0.tgz", - "integrity": "sha512-WuxUnVtlWL1OfZFQFuqvnvs6MiAGk9UNsBostyBOB0Is9wb5uRESevA6rnl/rkksXaGX3GzZhPup5d6Vp1nFew==" - }, "node_modules/prisma": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/prisma/-/prisma-6.1.0.tgz",