This commit is contained in:
aaron 2026-04-12 20:15:04 +08:00
parent 55fd44ed87
commit b97e1de2a7
3 changed files with 70 additions and 53 deletions

View File

@ -95,6 +95,12 @@ async def change_user_status(
status_code=403, detail="Cannot manage users outside your class" status_code=403, detail="Cannot manage users outside your class"
) )
# Only super_admin can change roles
if data.role and admin.role != "super_admin":
raise HTTPException(
status_code=403, detail="Only super admin can change user roles"
)
updated = await update_user_status(db, user_id, data.status, data.role) updated = await update_user_status(db, user_id, data.status, data.role)
# Send email notification # Send email notification

View File

@ -481,23 +481,27 @@ export default function MembersPage() {
</p> </p>
</div> </div>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<Select {isSuperAdmin ? (
value={m.role} <Select
onValueChange={(v) => v && handleRoleChange(m.id, v)} value={m.role}
> onValueChange={(v) => v && handleRoleChange(m.id, v)}
<SelectTrigger className="w-28 h-7 text-xs"> >
<SelectValue> <SelectTrigger className="w-28 h-7 text-xs">
{ROLES[m.role as keyof typeof ROLES] || m.role} <SelectValue>
</SelectValue> {ROLES[m.role as keyof typeof ROLES] || m.role}
</SelectTrigger> </SelectValue>
<SelectContent> </SelectTrigger>
<SelectItem value="student"></SelectItem> <SelectContent>
<SelectItem value="class_admin"></SelectItem> <SelectItem value="student"></SelectItem>
{isSuperAdmin && ( <SelectItem value="class_admin"></SelectItem>
<SelectItem value="super_admin"></SelectItem> <SelectItem value="super_admin"></SelectItem>
)} </SelectContent>
</SelectContent> </Select>
</Select> ) : (
<Badge variant="outline">
{ROLES[m.role as keyof typeof ROLES] || m.role}
</Badge>
)}
{getStatusBadge(m.status)} {getStatusBadge(m.status)}
{m.status === "approved" && ( {m.status === "approved" && (
<Button <Button

View File

@ -3,8 +3,9 @@
import Link from "next/link"; import Link from "next/link";
import { usePathname } from "next/navigation"; import { usePathname } from "next/navigation";
import { useSidebar } from "@/hooks/use-sidebar"; import { useSidebar } from "@/hooks/use-sidebar";
import { RoleGuard } from "@/components/role-guard";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import type { UserRole } from "@/lib/types";
import { useAuth } from "@/hooks/use-auth";
const navItems = [ const navItems = [
{ href: "/dashboard", label: "首页", icon: "M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-6 0a1 1 0 001-1v-4a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 001 1m-6 0h6" }, { href: "/dashboard", label: "首页", icon: "M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-6 0a1 1 0 001-1v-4a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 001 1m-6 0h6" },
@ -19,13 +20,17 @@ const navItems = [
]; ];
const adminItems = [ const adminItems = [
{ href: "/admin/members", label: "成员管理", icon: "M12 4.354a4 4 0 110 5.292M15 21H3v-1a6 6 0 0112 0v1zm0 0h6v-1a6 6 0 00-9-5.197M13 7a4 4 0 11-8 0 4 4 0 018 0z" }, { href: "/admin/members", label: "成员管理", icon: "M12 4.354a4 4 0 110 5.292M15 21H3v-1a6 6 0 0112 0v1zm0 0h6v-1a6 6 0 00-9-5.197M13 7a4 4 0 11-8 0 4 4 0 018 0z", roles: ["super_admin", "class_admin"] as UserRole[] },
{ href: "/admin/classes", label: "班级管理", icon: "M19 21V5a2 2 0 00-2-2H7a2 2 0 00-2 2v16m14 0h2m-2 0h-5m-9 0H3m2 0h5M9 7h1m-1 4h1m4-4h1m-1 4h1m-5 10v-5a1 1 0 011-1h2a1 1 0 011 1v5m-4 0h4" }, { href: "/admin/classes", label: "班级管理", icon: "M19 21V5a2 2 0 00-2-2H7a2 2 0 00-2 2v16m14 0h2m-2 0h-5m-9 0H3m2 0h5M9 7h1m-1 4h1m4-4h1m-1 4h1m-5 10v-5a1 1 0 011-1h2a1 1 0 011 1v5m-4 0h4", roles: ["super_admin"] as UserRole[] },
]; ];
export function Sidebar() { export function Sidebar() {
const pathname = usePathname(); const pathname = usePathname();
const { isOpen, close } = useSidebar(); const { isOpen, close } = useSidebar();
const { user } = useAuth();
const visibleAdminItems = user
? adminItems.filter((item) => item.roles.includes(user.role))
: [];
return ( return (
<> <>
@ -80,41 +85,43 @@ export function Sidebar() {
</Link> </Link>
))} ))}
<RoleGuard roles={["super_admin", "class_admin"]}> {visibleAdminItems.length > 0 && (
<div className="pt-4 pb-2"> <>
<p className="px-3 text-xs font-semibold text-gray-400 uppercase tracking-wider"> <div className="pt-4 pb-2">
<p className="px-3 text-xs font-semibold text-gray-400 uppercase tracking-wider">
</p>
</div> </p>
{adminItems.map((item) => ( </div>
<Link {visibleAdminItems.map((item) => (
key={item.href} <Link
href={item.href} key={item.href}
onClick={close} href={item.href}
className={cn( onClick={close}
"flex items-center gap-3 px-3 py-2 rounded-lg text-sm transition-colors", className={cn(
pathname.startsWith(item.href) "flex items-center gap-3 px-3 py-2 rounded-lg text-sm transition-colors",
? "bg-gray-900 text-white" pathname.startsWith(item.href)
: "text-gray-600 hover:bg-gray-100" ? "bg-gray-900 text-white"
)} : "text-gray-600 hover:bg-gray-100"
> )}
<svg
className="w-5 h-5 shrink-0"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
> >
<path <svg
strokeLinecap="round" className="w-5 h-5 shrink-0"
strokeLinejoin="round" fill="none"
strokeWidth={1.5} stroke="currentColor"
d={item.icon} viewBox="0 0 24 24"
/> >
</svg> <path
{item.label} strokeLinecap="round"
</Link> strokeLinejoin="round"
))} strokeWidth={1.5}
</RoleGuard> d={item.icon}
/>
</svg>
{item.label}
</Link>
))}
</>
)}
</nav> </nav>
<div className="p-4 border-t border-gray-200"> <div className="p-4 border-t border-gray-200">