feat(auth): add protected layout with role-based access control

This commit is contained in:
cfngc4594 2025-06-21 23:27:55 +08:00
parent 0695dd2f61
commit 19fac9b3d6
8 changed files with 54 additions and 58 deletions

View File

@ -1,5 +1,4 @@
import { notFound } from "next/navigation"; import { notFound } from "next/navigation";
import { ProblemEditLayout } from "@/features/admin/ui/layouts/problem-edit-layout";
interface LayoutProps { interface LayoutProps {
children: React.ReactNode; children: React.ReactNode;
@ -13,7 +12,7 @@ const Layout = async ({ children, params }: LayoutProps) => {
return notFound(); return notFound();
} }
return <ProblemEditLayout>{children}</ProblemEditLayout>; return <>{children}</>;
}; };
export default Layout; export default Layout;

View File

@ -8,6 +8,7 @@ import {
BarChart3, BarChart3,
Target, Target,
Activity, Activity,
GraduationCapIcon,
} from "lucide-react"; } from "lucide-react";
import Link from "next/link"; import Link from "next/link";
import { import {
@ -197,21 +198,26 @@ export default async function DashboardPage() {
}, },
], ],
actions: [ actions: [
{
label: "管理员管理",
href: "/dashboard/management",
icon: Target,
},
{ {
label: "用户管理", label: "用户管理",
href: "/dashboard/usermanagement/guest", href: "/dashboard/usermanagement/guest",
icon: Users, icon: Users,
}, },
{
label: "教师管理",
href: "/dashboard/usermanagement/teacher",
icon: GraduationCapIcon,
},
{ {
label: "题目管理", label: "题目管理",
href: "/dashboard/usermanagement/problem", href: "/dashboard/usermanagement/problem",
icon: BookOpen, icon: BookOpen,
}, },
{
label: "管理员设置",
href: "/dashboard/management",
icon: Target,
},
], ],
}; };
case "TEACHER": case "TEACHER":
@ -240,7 +246,7 @@ export default async function DashboardPage() {
], ],
actions: [ actions: [
{ {
label: "学生管理", label: "用户管理",
href: "/dashboard/usermanagement/guest", href: "/dashboard/usermanagement/guest",
icon: Users, icon: Users,
}, },
@ -250,7 +256,7 @@ export default async function DashboardPage() {
icon: BookOpen, icon: BookOpen,
}, },
{ {
label: "统计分析", label: "完成情况",
href: "/dashboard/teacher/dashboard", href: "/dashboard/teacher/dashboard",
icon: BarChart3, icon: BarChart3,
}, },
@ -281,12 +287,12 @@ export default async function DashboardPage() {
}, },
], ],
actions: [ actions: [
{ label: "开始做题", href: "/problemset", icon: BookOpen },
{ {
label: "我的进度", label: "我的进度",
href: "/dashboard/student/dashboard", href: "/dashboard/student/dashboard",
icon: TrendingUp, icon: TrendingUp,
}, },
{ label: "开始做题", href: "/problemset", icon: BookOpen },
{ label: "个人设置", href: "/dashboard/management", icon: Target }, { label: "个人设置", href: "/dashboard/management", icon: Target },
], ],
}; };
@ -361,7 +367,7 @@ export default async function DashboardPage() {
<CardDescription>访</CardDescription> <CardDescription>访</CardDescription>
</CardHeader> </CardHeader>
<CardContent> <CardContent>
<div className="grid gap-4 md:grid-cols-3"> <div className="grid gap-4 md:grid-cols-4">
{config.actions.map((action, index) => ( {config.actions.map((action, index) => (
<Link key={index} href={action.href}> <Link key={index} href={action.href}>
<Button variant="outline" className="w-full justify-start"> <Button variant="outline" className="w-full justify-start">

View File

@ -0,0 +1,15 @@
import { ProtectedLayout } from "@/features/dashboard/layouts/protected-layout";
interface LayoutProps {
children: React.ReactNode;
}
const Layout = ({ children }: LayoutProps) => {
return (
<ProtectedLayout roles={["ADMIN", "TEACHER", "GUEST"]}>
{children}
</ProtectedLayout>
);
};
export default Layout;

View File

@ -33,8 +33,8 @@ const adminData = {
}, },
], ],
navSecondary: [ navSecondary: [
{ title: "帮助", url: "/", icon: LifeBuoy }, { title: "帮助", url: `${siteConfig.url.repo.github}/issues`, icon: LifeBuoy },
{ title: "反馈", url: siteConfig.url.repo.github, icon: Send }, { title: "反馈", url: `${siteConfig.url.repo.github}/pulls`, icon: Send },
], ],
}; };

View File

@ -16,24 +16,28 @@ import { NavMain } from "@/components/nav-main";
import { NavUser } from "@/components/nav-user"; import { NavUser } from "@/components/nav-user";
import { NavProjects } from "@/components/nav-projects"; import { NavProjects } from "@/components/nav-projects";
import { NavSecondary } from "@/components/nav-secondary"; import { NavSecondary } from "@/components/nav-secondary";
import { Command, LifeBuoy, Send, SquareTerminal } from "lucide-react"; import { Command, LifeBuoy, Send, Shield } from "lucide-react";
const data = { const data = {
navMain: [ navMain: [
{ {
title: "页面", title: "管理面板",
url: "#", url: "/dashboard",
icon: SquareTerminal, icon: Shield,
isActive: true, isActive: true,
items: [ items: [
{ {
title: "主页", title: "我的进度",
url: "/dashboard/student/dashboard", url: "/dashboard/student/dashboard",
}, },
{ {
title: "目集", title: "开始做题",
url: "/problemset", url: "/problemset",
}, },
{
title: "个人设置",
url: "/dashboard/management",
},
], ],
}, },
@ -71,12 +75,12 @@ const data = {
navSecondary: [ navSecondary: [
{ {
title: "帮助", title: "帮助",
url: "/", url: `${siteConfig.url.repo.github}/issues`,
icon: LifeBuoy, icon: LifeBuoy,
}, },
{ {
title: "反馈", title: "反馈",
url: siteConfig.url.repo.github, url: `${siteConfig.url.repo.github}/pulls`,
icon: Send, icon: Send,
}, },
], ],

View File

@ -1,12 +1,6 @@
"use client"; "use client";
import { import { Command, LifeBuoy, Send, Shield } from "lucide-react";
Command,
LifeBuoy,
PieChart,
Send,
SquareTerminal,
} from "lucide-react";
import * as React from "react"; import * as React from "react";
import { import {
Sidebar, Sidebar,
@ -26,9 +20,9 @@ import { NavSecondary } from "@/components/nav-secondary";
const data = { const data = {
navMain: [ navMain: [
{ {
title: "教师管理", title: "管理面板",
url: "#", url: "/dashboard",
icon: SquareTerminal, icon: Shield,
isActive: true, isActive: true,
items: [ items: [
{ {
@ -36,47 +30,25 @@ const data = {
url: "/dashboard/usermanagement/guest", url: "/dashboard/usermanagement/guest",
}, },
{ {
title: "题管理", title: "题管理",
url: "/dashboard/usermanagement/problem", url: "/dashboard/usermanagement/problem",
}, },
],
},
{
title: "统计分析",
url: "#",
icon: PieChart,
items: [
{ {
title: "完成情况", title: "完成情况",
url: "/dashboard/teacher/dashboard", url: "/dashboard/teacher/dashboard",
}, },
// {
// title: "错题统计",
// url: "/dashboard/teacher/dashboard",
// },
], ],
}, },
// {
// title: "设置",
// url: "#",
// icon: Settings2,
// items: [
// {
// title: "语言",
// url: "#",
// },
// ],
// },
], ],
navSecondary: [ navSecondary: [
{ {
title: "帮助", title: "帮助",
url: "/", url: `${siteConfig.url.repo.github}/issues`,
icon: LifeBuoy, icon: LifeBuoy,
}, },
{ {
title: "反馈", title: "反馈",
url: siteConfig.url.repo.github, url: `${siteConfig.url.repo.github}/pulls`,
icon: Send, icon: Send,
}, },
], ],

View File

@ -25,7 +25,7 @@ export const ProtectedLayout = async ({
}); });
if (!user || !roles.includes(user.role)) { if (!user || !roles.includes(user.role)) {
redirect("unauthorized"); redirect("/unauthorized");
} }
return <>{children}</>; return <>{children}</>;

View File

@ -251,7 +251,7 @@ export function UserTable(props: UserTableProps) {
if (isProblem) { if (isProblem) {
// 如果是problem类型跳转到编辑路由使用displayId // 如果是problem类型跳转到编辑路由使用displayId
const problem = item as Problem; const problem = item as Problem;
router.push(`/admin/problems/${problem.displayId}/edit`); router.push(`/dashboard/admin/problems/${problem.id}/edit`);
} else { } else {
// 如果是用户类型,打开编辑弹窗 // 如果是用户类型,打开编辑弹窗
setEditingUser(item); setEditingUser(item);