feat: Add admin route and navigation for admin users

This commit is contained in:
wood chen 2025-02-09 19:05:17 +08:00
parent a9a5d2d59a
commit 5c2085d238
7 changed files with 155 additions and 12 deletions

View File

@ -0,0 +1,113 @@
import { redirect } from "next/navigation";
import { prisma } from "@/lib/prisma";
import { getCurrentUser } from "@/lib/session";
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table";
async function getStats() {
const [userCount, clientCount] = await Promise.all([
prisma.user.count(),
prisma.client.count(),
]);
const clients = await prisma.client.findMany({
include: {
user: {
select: {
username: true,
email: true,
},
},
},
orderBy: {
createdAt: "desc",
},
take: 10,
});
return {
userCount,
clientCount,
recentClients: clients,
};
}
export default async function AdminPage() {
const user = await getCurrentUser();
if (!user || user.role !== "ADMIN") {
redirect("/dashboard");
}
const stats = await getStats();
return (
<div className="mx-auto max-w-7xl px-4 py-8 sm:px-6 lg:px-8">
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
<Card>
<CardHeader>
<CardTitle></CardTitle>
<CardDescription></CardDescription>
</CardHeader>
<CardContent>
<p className="text-3xl font-bold">{stats.userCount}</p>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle></CardTitle>
<CardDescription></CardDescription>
</CardHeader>
<CardContent>
<p className="text-3xl font-bold">{stats.clientCount}</p>
</CardContent>
</Card>
</div>
<Card className="mt-8">
<CardHeader>
<CardTitle></CardTitle>
<CardDescription> 10 </CardDescription>
</CardHeader>
<CardContent>
<Table>
<TableHeader>
<TableRow>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead>Client ID</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{stats.recentClients.map((client) => (
<TableRow key={client.id}>
<TableCell className="font-medium">{client.name}</TableCell>
<TableCell>{client.user.username}</TableCell>
<TableCell>
{new Date(client.createdAt).toLocaleString()}
</TableCell>
<TableCell className="font-mono">{client.clientId}</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</CardContent>
</Card>
</div>
);
}

View File

@ -0,0 +1,26 @@
import { redirect } from "next/navigation";
import { getCurrentUser } from "@/lib/session";
import { DashboardHeader } from "@/components/layout/dashboard-header";
export default async function AdminLayout({
children,
}: {
children: React.ReactNode;
}) {
const user = await getCurrentUser();
if (!user) {
redirect("/sign-in");
}
if (user.role !== "ADMIN") {
redirect("/dashboard");
}
return (
<div className="min-h-screen">
<DashboardHeader />
{children}
</div>
);
}

View File

@ -10,7 +10,6 @@ export default async function AuthLayout({
const user = await getCurrentUser(); const user = await getCurrentUser();
if (user) { if (user) {
if (user.role === "ADMIN") redirect("/admin");
redirect("/dashboard"); redirect("/dashboard");
} }

View File

@ -9,7 +9,9 @@ export default async function DashboardLayout({
children: React.ReactNode; children: React.ReactNode;
}) { }) {
const user = await getCurrentUser(); const user = await getCurrentUser();
if (!user) redirect("/sign-in"); if (!user) {
redirect("/sign-in");
}
return ( return (
<div className="min-h-screen"> <div className="min-h-screen">

View File

@ -1,7 +1,6 @@
"use client"; "use client";
import { useCallback, useEffect, useState } from "react"; import { useCallback, useEffect, useState } from "react";
import { useRouter } from "next/navigation";
import { signIn } from "@/actions/user-authorize"; import { signIn } from "@/actions/user-authorize";
import { useSession } from "next-auth/react"; import { useSession } from "next-auth/react";
@ -17,7 +16,6 @@ export function UserAuthorize({
const [isLoading, setIsLoading] = useState<boolean>(false); const [isLoading, setIsLoading] = useState<boolean>(false);
const [error, setError] = useState<Error | unknown>(null); const [error, setError] = useState<Error | unknown>(null);
const { update } = useSession(); const { update } = useSession();
const router = useRouter();
const signInCallback = useCallback(async () => { const signInCallback = useCallback(async () => {
if (isLoading) { if (isLoading) {
@ -25,22 +23,18 @@ export function UserAuthorize({
} }
setIsLoading(true); setIsLoading(true);
try { try {
await signIn({ ...data, redirectTo: "/dashboard" }); const result = await signIn({ ...data, redirectTo: "/dashboard" });
// 更新 session // 更新 session
await update(); await update();
router.push("/dashboard");
setIsLoading(false); setIsLoading(false);
} catch (error) { } catch (error) {
setError(error); setError(error);
setIsLoading(false); setIsLoading(false);
} }
}, [data, isLoading, update, router]); }, [data, isLoading, update]);
useEffect(() => { useEffect(() => {
const timer = setTimeout(signInCallback, 5); signInCallback();
return () => {
clearTimeout(timer);
};
}, [signInCallback]); }, [signInCallback]);
return ( return (
@ -48,7 +42,7 @@ export function UserAuthorize({
{error ? ( {error ? (
<p className="text-center"></p> <p className="text-center"></p>
) : ( ) : (
<p className="text-center">...</p> <p className="text-center">...</p>
)} )}
</> </>
); );

View File

@ -11,6 +11,7 @@ export function DashboardHeader() {
if (pathname === "/dashboard") return "控制台"; if (pathname === "/dashboard") return "控制台";
if (pathname === "/dashboard/clients") return "应用管理"; if (pathname === "/dashboard/clients") return "应用管理";
if (pathname.includes("/dashboard/clients/")) return "应用编辑"; if (pathname.includes("/dashboard/clients/")) return "应用编辑";
if (pathname === "/admin") return "管理后台";
return ""; return "";
}; };

View File

@ -89,6 +89,14 @@ export function NavBar() {
<DropdownMenuItem asChild> <DropdownMenuItem asChild>
<Link href="/dashboard/clients"></Link> <Link href="/dashboard/clients"></Link>
</DropdownMenuItem> </DropdownMenuItem>
{user.role === "ADMIN" && (
<>
<DropdownMenuSeparator />
<DropdownMenuItem asChild>
<Link href="/admin"></Link>
</DropdownMenuItem>
</>
)}
<DropdownMenuSeparator /> <DropdownMenuSeparator />
<DropdownMenuItem <DropdownMenuItem
className="text-red-600 dark:text-red-400" className="text-red-600 dark:text-red-400"