import { Metadata } from 'next'; import { Prisma } from '@prisma/client'; import { db } from '@/lib/db'; import { auth } from '@/lib/auth'; import { redirect } from 'next/navigation'; import { getCachedBunnyStorageStats, getCachedUserBunnyStorage, getCachedUserDownloadEgress, getCachedUserMediaStorage } from '@/lib/admin-stats'; import { Film, HardDrive } from 'lucide-react'; import Link from 'next/link'; import { Button } from '@/components/ui/button'; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow, } from '@/components/ui/table'; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; import { format } from 'date-fns'; export const metadata: Metadata = { title: 'Manage Users | Admin', }; type SortBy = | 'user' | 'joinedDate' | 'workspacesOwned' | 'invitedMembers' | 'projectsOwned' | 'totalComments' | 'bunnyUpload' | 'downloadEgress' | 'mediaStorage'; type SortDirection = 'asc' | 'desc'; const SORTABLE_COLUMNS: SortBy[] = [ 'user', 'joinedDate', 'workspacesOwned', 'invitedMembers', 'projectsOwned', 'totalComments', 'bunnyUpload', 'downloadEgress', 'mediaStorage', ]; function formatBytes(bytes: number, decimals = 2) { if (bytes < 0) return 'Error Fetching'; if (!+bytes) return '0 Bytes'; const k = 1000; const dm = decimals < 0 ? 0 : decimals; const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB']; const i = Math.floor(Math.log(bytes) / Math.log(k)); return `${parseFloat((bytes / Math.pow(k, i)).toFixed(dm))} ${sizes[i]}`; } function isSortBy(value: string | undefined): value is SortBy { return !!value && SORTABLE_COLUMNS.includes(value as SortBy); } function getDefaultSortDirection(sortBy: SortBy): SortDirection { return sortBy === 'user' ? 'asc' : 'desc'; } function getSortIndicator(column: SortBy, activeSortBy: SortBy, activeSortDirection: SortDirection): string { if (column !== activeSortBy) return '↕'; return activeSortDirection === 'asc' ? '↑' : '↓'; } function canSortInDb(sortBy: SortBy): boolean { return sortBy === 'user' || sortBy === 'joinedDate' || sortBy === 'workspacesOwned' || sortBy === 'projectsOwned' || sortBy === 'totalComments'; } function getUsersOrderBy(sortBy: SortBy, sortDirection: SortDirection): Prisma.UserOrderByWithRelationInput[] { const createdAtTieBreaker: Prisma.UserOrderByWithRelationInput = { createdAt: 'desc' }; if (sortBy === 'user') { return [ { name: sortDirection }, { email: sortDirection }, createdAtTieBreaker, ]; } if (sortBy === 'joinedDate') { return [{ createdAt: sortDirection }]; } if (sortBy === 'workspacesOwned') { return [ { ownedWorkspaces: { _count: sortDirection } }, createdAtTieBreaker, ]; } if (sortBy === 'projectsOwned') { return [ { projects: { _count: sortDirection } }, createdAtTieBreaker, ]; } if (sortBy === 'totalComments') { return [ { comments: { _count: sortDirection } }, createdAtTieBreaker, ]; } return [createdAtTieBreaker]; } export default async function AdminUsersPage({ searchParams }: { searchParams: Promise<{ page?: string; sortBy?: string; sortDirection?: string }> }) { const session = await auth(); if (!session?.user?.isAdmin) { redirect('/'); } const resolvedSearchParams = await searchParams; const requestedPage = Number(resolvedSearchParams?.page) || 1; const sortBy: SortBy = isSortBy(resolvedSearchParams?.sortBy) ? resolvedSearchParams.sortBy : 'joinedDate'; const sortDirection: SortDirection = resolvedSearchParams?.sortDirection === 'asc' || resolvedSearchParams?.sortDirection === 'desc' ? resolvedSearchParams.sortDirection : getDefaultSortDirection(sortBy); const pageSize = 20; const [totalUsers, userStorage, userBunnyStorage, userDownloadEgress, bunnyStorageStats] = await Promise.all([ db.user.count(), getCachedUserMediaStorage(), getCachedUserBunnyStorage(), getCachedUserDownloadEgress(), getCachedBunnyStorageStats(), ]); const totalPages = Math.max(1, Math.ceil(totalUsers / pageSize)); const page = Math.min(Math.max(1, requestedPage), totalPages); const skip = (page - 1) * pageSize; const select = { id: true, name: true, email: true, createdAt: true, ownedWorkspaces: { select: { _count: { select: { members: true, } } } }, _count: { select: { ownedWorkspaces: true, projects: true, comments: true, } } } satisfies Prisma.UserSelect; let paginatedUsers: Array<{ id: string; name: string | null; email: string | null; createdAt: Date; ownedWorkspaces: Array<{ _count: { members: number } }>; _count: { ownedWorkspaces: number; projects: number; comments: number }; invitedMembersCount: number; bunnyUploadBytes: number; downloadEgressBytes: number; mediaStorageBytes: number; }> = []; if (canSortInDb(sortBy)) { const users = await db.user.findMany({ skip, take: pageSize, orderBy: getUsersOrderBy(sortBy, sortDirection), select, }); paginatedUsers = users.map((user) => ({ ...user, invitedMembersCount: user.ownedWorkspaces.reduce( (total, workspace) => total + workspace._count.members, 0 ), bunnyUploadBytes: userBunnyStorage[user.id] || 0, downloadEgressBytes: userDownloadEgress[user.id] || 0, mediaStorageBytes: userStorage[user.id]?.total || 0, })); } else { const users = await db.user.findMany({ select }); const usersWithMetrics = users.map((user) => ({ ...user, invitedMembersCount: user.ownedWorkspaces.reduce( (total, workspace) => total + workspace._count.members, 0 ), bunnyUploadBytes: userBunnyStorage[user.id] || 0, downloadEgressBytes: userDownloadEgress[user.id] || 0, mediaStorageBytes: userStorage[user.id]?.total || 0, })); const sortedUsers = usersWithMetrics.sort((a, b) => { let comparison = 0; if (sortBy === 'invitedMembers') { comparison = a.invitedMembersCount - b.invitedMembersCount; } else if (sortBy === 'bunnyUpload') { comparison = a.bunnyUploadBytes - b.bunnyUploadBytes; } else if (sortBy === 'downloadEgress') { comparison = a.downloadEgressBytes - b.downloadEgressBytes; } else if (sortBy === 'mediaStorage') { comparison = a.mediaStorageBytes - b.mediaStorageBytes; } if (comparison === 0) { comparison = new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime(); } return sortDirection === 'asc' ? comparison : -comparison; }); paginatedUsers = sortedUsers.slice(skip, skip + pageSize); } const buildUsersPageHref = ( targetPage: number, targetSortBy: SortBy = sortBy, targetSortDirection: SortDirection = sortDirection ): string => { const params = new URLSearchParams({ page: String(targetPage), sortBy: targetSortBy, sortDirection: targetSortDirection, }); return `/admin/users?${params.toString()}`; }; const buildSortHref = (column: SortBy): string => { const nextDirection: SortDirection = column === sortBy ? sortDirection === 'asc' ? 'desc' : 'asc' : getDefaultSortDirection(column); return buildUsersPageHref(1, column, nextDirection); }; return (

Users

Bunny Stream Storage
{formatBytes(bunnyStorageStats.totalBytes)}
Cloudflare R2 Media Storage
{formatBytes(Object.values(userStorage).reduce((sum, item) => sum + item.total, 0))}
All Users A comprehensive list of all {totalUsers} users registered on the platform.
User {getSortIndicator('user', sortBy, sortDirection)} Joined Date {getSortIndicator('joinedDate', sortBy, sortDirection)} Workspaces Owned {getSortIndicator('workspacesOwned', sortBy, sortDirection)} Invited Members {getSortIndicator('invitedMembers', sortBy, sortDirection)} Projects Owned {getSortIndicator('projectsOwned', sortBy, sortDirection)} Total Comments {getSortIndicator('totalComments', sortBy, sortDirection)} Bunny Upload {getSortIndicator('bunnyUpload', sortBy, sortDirection)} Download Egress (Est.) {getSortIndicator('downloadEgress', sortBy, sortDirection)} Media Storage {getSortIndicator('mediaStorage', sortBy, sortDirection)} {paginatedUsers.length === 0 ? ( No users found. ) : ( paginatedUsers.map((user) => (
{user.name || 'Anonymous'} {user.email}
{format(new Date(user.createdAt), 'MMM dd, yyyy')} {user._count.ownedWorkspaces} {user.invitedMembersCount} {user._count.projects} {user._count.comments} {formatBytes(user.bunnyUploadBytes)} {formatBytes(user.downloadEgressBytes)}
{formatBytes(user.mediaStorageBytes)} {(userStorage[user.id]?.voice > 0 || userStorage[user.id]?.image > 0) && ( {userStorage[user.id]?.voice > 0 && 🎤 {formatBytes(userStorage[user.id]?.voice)}} {userStorage[user.id]?.voice > 0 && userStorage[user.id]?.image > 0 && } {userStorage[user.id]?.image > 0 && 🖼️ {formatBytes(userStorage[user.id]?.image)}} )}
)) )}
{/* Pagination */} {totalPages > 1 && (
Page {page} of {totalPages}
)}
); }