mirror of
https://github.com/woodchen-ink/Q58Connect.git
synced 2025-07-18 14:01:55 +08:00
feat: Add admin route and navigation for admin users
This commit is contained in:
parent
a9a5d2d59a
commit
5c2085d238
113
src/app/(admin)/admin/page.tsx
Normal file
113
src/app/(admin)/admin/page.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
26
src/app/(admin)/layout.tsx
Normal file
26
src/app/(admin)/layout.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
@ -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");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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">
|
||||||
|
@ -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>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
@ -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 "";
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -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"
|
||||||
|
Loading…
x
Reference in New Issue
Block a user