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,6 +481,7 @@ export default function MembersPage() {
</p> </p>
</div> </div>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
{isSuperAdmin ? (
<Select <Select
value={m.role} value={m.role}
onValueChange={(v) => v && handleRoleChange(m.id, v)} onValueChange={(v) => v && handleRoleChange(m.id, v)}
@ -493,11 +494,14 @@ export default function MembersPage() {
<SelectContent> <SelectContent>
<SelectItem value="student"></SelectItem> <SelectItem value="student"></SelectItem>
<SelectItem value="class_admin"></SelectItem> <SelectItem value="class_admin"></SelectItem>
{isSuperAdmin && (
<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,13 +85,14 @@ export function Sidebar() {
</Link> </Link>
))} ))}
<RoleGuard roles={["super_admin", "class_admin"]}> {visibleAdminItems.length > 0 && (
<>
<div className="pt-4 pb-2"> <div className="pt-4 pb-2">
<p className="px-3 text-xs font-semibold text-gray-400 uppercase tracking-wider"> <p className="px-3 text-xs font-semibold text-gray-400 uppercase tracking-wider">
</p> </p>
</div> </div>
{adminItems.map((item) => ( {visibleAdminItems.map((item) => (
<Link <Link
key={item.href} key={item.href}
href={item.href} href={item.href}
@ -114,7 +120,8 @@ export function Sidebar() {
{item.label} {item.label}
</Link> </Link>
))} ))}
</RoleGuard> </>
)}
</nav> </nav>
<div className="p-4 border-t border-gray-200"> <div className="p-4 border-t border-gray-200">