import Link from "next/link";
import { AlertTriangle, Database, LockKeyhole, Plus, Search, Shield, UsersRound } from "lucide-react";
import { redirect } from "next/navigation";
import { AppShell } from "@/components/app-shell";
import { Avatar } from "@/components/avatar";
import { StatusBadge } from "@/components/status-badge";
import { SYSTEM_PERMISSIONS } from "@/lib/access";
import { prisma } from "@/lib/prisma";
import { getShellContext } from "@/lib/shell";
import { requireCurrentUser, userIsAdmin } from "@/lib/session";
import { requireInitialSetup } from "@/lib/setup";
import { DataTable, EmptyState, MetricTile, PageHeader, Panel, StatusDot, Tabs } from "@/components/ui";
export const dynamic = "force-dynamic";
const adminTabs = ["Users", "Rooms", "Roles", "Invites", "Instance", "Security"];
export default async function AdminPage({ searchParams }: { searchParams: Promise<{ tab?: string; q?: string }> }) {
await requireInitialSetup();
const user = await requireCurrentUser();
if (!userIsAdmin(user)) {
redirect("/dashboard");
}
const { tab = "Users", q = "" } = await searchParams;
const activeTab = adminTabs.includes(tab) ? tab : "Users";
const query = q.trim().toLowerCase();
const shell = await getShellContext(user);
const [users, rooms, roles, pendingRequests] = await Promise.all([
prisma.user.findMany({
include: { roles: { include: { role: true } }, _count: { select: { ownedRooms: true, roomMembers: true } } },
orderBy: { createdAt: "asc" }
}),
prisma.room.findMany({
include: { owner: true, _count: { select: { members: true, mediaSources: true } } },
orderBy: { updatedAt: "desc" }
}),
prisma.role.findMany({
include: { _count: { select: { users: true, permissions: true } } },
orderBy: { name: "asc" }
}),
prisma.friendship.count({ where: { status: "PENDING" } })
]);
const filteredUsers = query
? users.filter((account) =>
[account.username, account.displayName || "", ...account.roles.map((role) => role.role.name)].join(" ").toLowerCase().includes(query)
)
: users;
const filteredRooms = query
? rooms.filter((room) =>
[room.name, room.slug, room.visibility, room.owner?.username || "", room.owner?.displayName || ""].join(" ").toLowerCase().includes(query)
)
: rooms;
const filteredRoles = query
? roles.filter((role) => [role.name, role.description || "", role.scope].join(" ").toLowerCase().includes(query))
: roles;
return (
0 ? "warn" : "neutral"} label={`${pendingRequests} pending requests`} />
>
}
actions={
<>
>
}
/>
}
>
({ label: item, href: `/admin?tab=${encodeURIComponent(item)}` }))} />
{activeTab === "Users" ? : null}
{activeTab === "Rooms" ? : null}
{activeTab === "Roles" ? : null}
{activeTab === "Invites" ? : null}
{activeTab === "Instance" ? : null}
{activeTab === "Security" ? : null}
);
}
function UsersTable({
users
}: {
users: Array<{
id: string;
username: string;
displayName: string | null;
createdAt: Date;
roles: Array<{ roleId: string; role: { name: string } }>;
_count: { ownedRooms: number; roomMembers: number };
}>;
}) {
return (
| User |
Roles |
Rooms |
Created |
Actions |
{users.map((account) => (
{account.displayName || account.username}
@{account.username}
|
{account.roles.length === 0 ? user : null}
{account.roles.map((userRole) => (
{userRole.role.name}
))}
|
{account._count.ownedRooms + account._count.roomMembers} |
{formatDate(account.createdAt)} |
|
))}
);
}
function RoomsTable({
rooms
}: {
rooms: Array<{
id: string;
slug: string;
name: string;
visibility: string;
updatedAt: Date;
owner: { username: string; displayName: string | null } | null;
_count: { members: number; mediaSources: number };
}>;
}) {
return (
| Room |
Owner |
Access |
State |
Updated |
|
{rooms.map((room) => (
|
{room.name}
/{room.slug}
|
{room.owner?.displayName || room.owner?.username || "Unassigned"} |
{room.visibility.toLowerCase().replace("_", " ")} |
{room._count.members + 1} users / {room._count.mediaSources} media |
{formatDate(room.updatedAt)} |
Open |
))}
);
}
function RolesTable({
roles
}: {
roles: Array<{
id: string;
name: string;
description: string | null;
scope: string;
_count: { users: number; permissions: number };
}>;
}) {
return roles.length === 0 ? (
) : (
{roles.map((role) => (
{role.name}
{role.description || `${role.scope.toLowerCase()} role`}
{role._count.users} users / {role._count.permissions} permissions
))}
{SYSTEM_PERMISSIONS.map((permission) => (
{permission}
Assignable to system roles
Enabled
))}
);
}
function InvitesPanel({ pendingRequests }: { pendingRequests: number }) {
return (
{pendingRequests} pending friend requests
Handled by each receiving user in People.
);
}
function InstancePanel({ userCount, roomCount }: { userCount: number; roomCount: number }) {
return (
} label="Database" value="Postgres via DATABASE_URL" />
} label="Sessions" value="Signed HTTP-only cookie" />
} label="Accounts" value={`${userCount} users`} />
} label="Rooms" value={`${roomCount} rooms`} />
);
}
function SecurityPanel() {
return (
} label="Password storage" value="bcrypt hash" />
} label="Admin check" value="Server-side role lookup" />
} label="Registration mode" value="Open registration" />
);
}
function Setting({ icon, label, value }: { icon: React.ReactNode; label: string; value: string }) {
return (
{icon}
{label}
{value}
);
}
function formatDate(date: Date) {
return new Intl.DateTimeFormat("en", { month: "short", day: "numeric", year: "numeric" }).format(date);
}