From 436bdbbe2130f095a54ae7935280e06c109f43d1 Mon Sep 17 00:00:00 2001 From: sanjana-singhania Date: Tue, 19 Nov 2024 13:37:48 -0500 Subject: [PATCH 01/15] add admin auth --- packages/trpc/src/internal/init.ts | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/packages/trpc/src/internal/init.ts b/packages/trpc/src/internal/init.ts index 2332a1f..aa0b9b9 100644 --- a/packages/trpc/src/internal/init.ts +++ b/packages/trpc/src/internal/init.ts @@ -50,6 +50,7 @@ export const createCallerFactory = t.createCallerFactory; // Procedure builders export const baseProcedureBuilder = t.procedure; + export const authenticatedProcedureBuilder = baseProcedureBuilder.use( async ({ ctx, next }) => { const sessionId = getSessionCookie(); @@ -89,6 +90,15 @@ export const authenticatedProcedureBuilder = baseProcedureBuilder.use( }, ); +export const adminAuthenticatedProcedureBuilder = + authenticatedProcedureBuilder.use(async ({ ctx, next }) => { + if (ctx.session.user.role !== "ADMIN") { + throw new TRPCError({ code: "UNAUTHORIZED" }); + } + + return next(); + }); + // This middleware is used to prevent authenticated users from accessing a resource export const notAuthenticatedProcedureBuilder = baseProcedureBuilder.use( async ({ ctx, next }) => { From 8314b659b22682760e9cdc6328ae8def12ea2630 Mon Sep 17 00:00:00 2001 From: sanjana-singhania Date: Tue, 19 Nov 2024 13:57:13 -0500 Subject: [PATCH 02/15] initial routes to fetch data --- packages/trpc/src/procedures/admin-view.ts | 25 ++++++++++++++++++++++ 1 file changed, 25 insertions(+) create mode 100644 packages/trpc/src/procedures/admin-view.ts diff --git a/packages/trpc/src/procedures/admin-view.ts b/packages/trpc/src/procedures/admin-view.ts new file mode 100644 index 0000000..d617d91 --- /dev/null +++ b/packages/trpc/src/procedures/admin-view.ts @@ -0,0 +1,25 @@ +import { adminAuthenticatedProcedureBuilder } from "../internal/init"; + +export const userView = adminAuthenticatedProcedureBuilder.query( + async ({ ctx }) => { + const users = ctx.prisma.user.findMany(); + + return users; + }, +); + +export const groupView = adminAuthenticatedProcedureBuilder.query( + async ({ ctx }) => { + const groups = ctx.prisma.group.findMany(); + + return groups; + }, +); + +export const groupInvitesView = adminAuthenticatedProcedureBuilder.query( + async ({ ctx }) => { + const groupInvites = ctx.prisma.groupInvite.findMany(); + + return groupInvites; + }, +); From 12c29a7048ac97a25c0ebfe79fd62184c7cc9373 Mon Sep 17 00:00:00 2001 From: sanjana-singhania Date: Tue, 19 Nov 2024 14:31:50 -0500 Subject: [PATCH 03/15] frontend wip --- apps/web/app/(pages)/admin/page.tsx | 135 ++++++++++++++++++++ bun.lockb | Bin 279868 -> 282420 bytes packages/components/src/admin/DataTable.tsx | 70 ++++++++++ packages/ui/package.json | 1 + packages/ui/shad/badge.tsx | 37 ++++++ packages/ui/shad/button.tsx | 6 +- packages/ui/shad/card.tsx | 83 ++++++++++++ packages/ui/shad/pagination.tsx | 122 ++++++++++++++++++ packages/ui/shad/table.tsx | 120 +++++++++++++++++ packages/ui/shad/tabs.tsx | 53 ++++++++ 10 files changed, 624 insertions(+), 3 deletions(-) create mode 100644 apps/web/app/(pages)/admin/page.tsx create mode 100644 packages/components/src/admin/DataTable.tsx create mode 100644 packages/ui/shad/badge.tsx create mode 100644 packages/ui/shad/card.tsx create mode 100644 packages/ui/shad/pagination.tsx create mode 100644 packages/ui/shad/table.tsx create mode 100644 packages/ui/shad/tabs.tsx diff --git a/apps/web/app/(pages)/admin/page.tsx b/apps/web/app/(pages)/admin/page.tsx new file mode 100644 index 0000000..a7c8916 --- /dev/null +++ b/apps/web/app/(pages)/admin/page.tsx @@ -0,0 +1,135 @@ +//dont use use client + +// import { HydrateClient, trpc } from "@good-dog/trpc/server"; + +// export default function AdminPage() { +// void trpc.user.prefetch(); +// {/* your component here */}; +// } +"use client"; + +import { useState } from "react"; + +import { DataTable } from "@good-dog/components/admin/DataTable"; +import { Badge } from "@good-dog/ui/badge"; +import { + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, +} from "@good-dog/ui/card"; +import { Tabs, TabsContent, TabsList, TabsTrigger } from "@good-dog/ui/tabs"; + +// Mock data (replace with actual data fetching logic) +const mockUsers = [ + { id: 1, name: "John Doe", email: "john@example.com", role: "Admin" }, + { id: 2, name: "Jane Smith", email: "jane@example.com", role: "User" }, + // Add more mock users... +]; + +const mockGroups = [ + { id: 1, name: "Administrators", memberCount: 5 }, + { id: 2, name: "Users", memberCount: 100 }, + // Add more mock groups... +]; + +const mockInvites = [ + { + id: 1, + email: "newuser@example.com", + status: "Pending", + expiresAt: "2023-12-31", + }, + { + id: 2, + email: "anotheruser@example.com", + status: "Expired", + expiresAt: "2023-11-30", + }, + // Add more mock invites... +]; + +const columns = { + users: [ + { accessorKey: "id", header: "ID" }, + { accessorKey: "name", header: "Name" }, + { accessorKey: "email", header: "Email" }, + { accessorKey: "role", header: "Role" }, + ], + groups: [ + { accessorKey: "id", header: "ID" }, + { accessorKey: "name", header: "Name" }, + { accessorKey: "memberCount", header: "Members" }, + ], + invites: [ + { accessorKey: "id", header: "ID" }, + { accessorKey: "email", header: "Email" }, + { + accessorKey: "status", + header: "Status", + cell: (value: string) => ( + + {value} + + ), + }, + { accessorKey: "expiresAt", header: "Expires At" }, + ], +}; + +export default function AdminDashboard() { + const [activeTab, setActiveTab] = useState("users"); + + return ( +
+
+

Admin Dashboard

+ + + Users + Groups + Invites + + + + + Users + + Manage user accounts in the system. + + + + + + + + + + + Groups + + Manage user groups and permissions. + + + + + + + + + + + Invites + Manage pending invitations. + + + + + + + +
+
+ ); +} diff --git a/bun.lockb b/bun.lockb index b043afb01d8ed66a99fc2697ec5ed651c39aa219..c395718d0d5890e506a7aff8be62c521a57537e6 100755 GIT binary patch delta 12256 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 diff --git a/packages/components/src/admin/DataTable.tsx b/packages/components/src/admin/DataTable.tsx new file mode 100644 index 0000000..35073b2 --- /dev/null +++ b/packages/components/src/admin/DataTable.tsx @@ -0,0 +1,70 @@ +import { useState } from "react"; + +import { Pagination } from "@good-dog/ui/pagination"; +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from "@good-dog/ui/table"; + +interface ColumnDef { + accessorKey: string; + header: string; + cell?: (value: any) => React.ReactNode; +} + +interface DataTableProps { + columns: ColumnDef[]; + data: T[]; + itemsPerPage?: number; +} + +export function DataTable({ + columns, + data, + itemsPerPage = 10, +}: DataTableProps) { + const [currentPage, setCurrentPage] = useState(1); + + const totalPages = Math.ceil(data.length / itemsPerPage); + const startIndex = (currentPage - 1) * itemsPerPage; + const endIndex = startIndex + itemsPerPage; + const currentData = data.slice(startIndex, endIndex); + + return ( +
+
+ + + + {columns.map((column) => ( + {column.header} + ))} + + + + {currentData.map((row, rowIndex) => ( + + {columns.map((column) => ( + + {column.cell + ? column.cell(row[column.accessorKey as keyof T]) + : row[column.accessorKey as keyof T]?.toString()} + + ))} + + ))} + +
+
+ +
+ ); +} diff --git a/packages/ui/package.json b/packages/ui/package.json index ca3ac67..94623b0 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -40,6 +40,7 @@ "@radix-ui/react-label": "^2.1.0", "@radix-ui/react-slot": "^1.1.0", "@radix-ui/react-switch": "^1.1.1", + "@radix-ui/react-tabs": "^1.1.1", "class-variance-authority": "^0.7.0", "clsx": "^2.1.1", "date-fns": "^4.1.0", diff --git a/packages/ui/shad/badge.tsx b/packages/ui/shad/badge.tsx new file mode 100644 index 0000000..a99527d --- /dev/null +++ b/packages/ui/shad/badge.tsx @@ -0,0 +1,37 @@ +import type { VariantProps } from "class-variance-authority"; +import React from "react"; +import { cva } from "class-variance-authority"; + +import { cn } from "@good-dog/ui"; + +const badgeVariants = cva( + "inline-flex items-center rounded-md border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2", + { + variants: { + variant: { + default: + "border-transparent bg-primary text-primary-foreground shadow hover:bg-primary/80", + secondary: + "border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80", + destructive: + "border-transparent bg-destructive text-destructive-foreground shadow hover:bg-destructive/80", + outline: "text-foreground", + }, + }, + defaultVariants: { + variant: "default", + }, + }, +); + +export interface BadgeProps + extends React.HTMLAttributes, + VariantProps {} + +function Badge({ className, variant, ...props }: BadgeProps) { + return ( +
+ ); +} + +export { Badge, badgeVariants }; diff --git a/packages/ui/shad/button.tsx b/packages/ui/shad/button.tsx index 4d8fc8d..f354c89 100644 --- a/packages/ui/shad/button.tsx +++ b/packages/ui/shad/button.tsx @@ -1,12 +1,12 @@ import type { VariantProps } from "class-variance-authority"; -import React from "react"; +import * as React from "react"; import { Slot } from "@radix-ui/react-slot"; import { cva } from "class-variance-authority"; -import { cn } from "."; +import { cn } from "@good-dog/ui"; const buttonVariants = cva( - "inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50", + "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0", { variants: { variant: { diff --git a/packages/ui/shad/card.tsx b/packages/ui/shad/card.tsx new file mode 100644 index 0000000..3469abf --- /dev/null +++ b/packages/ui/shad/card.tsx @@ -0,0 +1,83 @@ +import React from "react"; + +import { cn } from "@good-dog/ui"; + +const Card = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)); +Card.displayName = "Card"; + +const CardHeader = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)); +CardHeader.displayName = "CardHeader"; + +const CardTitle = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)); +CardTitle.displayName = "CardTitle"; + +const CardDescription = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)); +CardDescription.displayName = "CardDescription"; + +const CardContent = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)); +CardContent.displayName = "CardContent"; + +const CardFooter = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)); +CardFooter.displayName = "CardFooter"; + +export { + Card, + CardHeader, + CardFooter, + CardTitle, + CardDescription, + CardContent, +}; diff --git a/packages/ui/shad/pagination.tsx b/packages/ui/shad/pagination.tsx new file mode 100644 index 0000000..999255b --- /dev/null +++ b/packages/ui/shad/pagination.tsx @@ -0,0 +1,122 @@ +import React from "react"; +import { + ChevronLeftIcon, + ChevronRightIcon, + DotsHorizontalIcon, +} from "@radix-ui/react-icons"; + +import { cn } from "@good-dog/ui"; + +import { ButtonProps, buttonVariants } from "./button"; + +const Pagination = ({ className, ...props }: React.ComponentProps<"nav">) => ( +