From cf695efb38414eeaee47c6c91af1418afa6f0ef0 Mon Sep 17 00:00:00 2001 From: aaron <> Date: Mon, 27 Apr 2026 23:09:51 +0800 Subject: [PATCH] 1 --- backend/app/api/announcements.py | 4 +- backend/app/api/assignments.py | 7 +- backend/app/api/classes.py | 16 ++-- backend/app/api/directory.py | 4 +- backend/app/api/fund.py | 6 +- backend/app/api/resources.py | 6 +- backend/app/api/schedule.py | 6 +- backend/app/api/timeline.py | 25 ++++--- backend/app/api/users.py | 8 +- backend/app/api/votes.py | 13 ++-- backend/app/core/deps.py | 7 +- backend/app/schemas/user.py | 12 ++- frontend/src/app/(app)/admin/members/page.tsx | 74 ++++++++++--------- frontend/src/app/(app)/admin/page.tsx | 27 ++++--- frontend/src/components/sidebar.tsx | 2 +- frontend/src/lib/constants.ts | 2 - frontend/src/lib/permissions.ts | 1 - frontend/src/lib/types.ts | 1 - 18 files changed, 120 insertions(+), 101 deletions(-) diff --git a/backend/app/api/announcements.py b/backend/app/api/announcements.py index c9343ec..84a045f 100644 --- a/backend/app/api/announcements.py +++ b/backend/app/api/announcements.py @@ -1,7 +1,7 @@ from fastapi import APIRouter, Depends, HTTPException from sqlalchemy.ext.asyncio import AsyncSession -from app.core.deps import ensure_class_permission, require_role, resolve_class_id_for_user +from app.core.deps import ensure_class_access, ensure_class_permission, require_role, resolve_class_id_for_user from app.db.database import get_db from app.db.models import User from app.schemas.announcement import AnnouncementCreate, AnnouncementUpdate, AnnouncementOut @@ -28,7 +28,7 @@ async def get_announcements( effective_class_id = resolve_class_id_for_user(user, class_id) if effective_class_id is None: return PageResponse(items=[], total=0, page=page, page_size=page_size, total_pages=0) - ensure_class_permission(user, "class_view", effective_class_id) + ensure_class_access(user, effective_class_id) announcements, total = await list_announcements(db, effective_class_id, page, page_size) total_pages = (total + page_size - 1) // page_size diff --git a/backend/app/api/assignments.py b/backend/app/api/assignments.py index 8a86c0a..df8db7c 100644 --- a/backend/app/api/assignments.py +++ b/backend/app/api/assignments.py @@ -4,6 +4,7 @@ from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.orm import selectinload from app.core.deps import ( + ensure_class_access, ensure_class_permission, get_effective_class_permissions, require_role, @@ -93,7 +94,7 @@ async def get_assignments( effective_class_id = resolve_class_id_for_user(user, class_id) if effective_class_id is None: return PageResponse(items=[], total=0, page=page, page_size=page_size, total_pages=0) - ensure_class_permission(user, "class_view", effective_class_id) + ensure_class_access(user, effective_class_id) assignments, total = await list_assignments(db, effective_class_id, page, page_size) total_pages = (total + page_size - 1) // page_size @@ -155,7 +156,7 @@ async def get_assignment_detail( assignment = await get_assignment_by_id(db, assignment_id) if assignment is None: raise HTTPException(status_code=404, detail="Assignment not found") - ensure_class_permission(user, "class_view", assignment.class_id) + ensure_class_access(user, assignment.class_id) base = _build_assignment_out(assignment, user.id, await _get_member_count(db, assignment.class_id)) @@ -215,7 +216,7 @@ async def submit_assignment( assignment = await get_assignment_by_id(db, assignment_id) if assignment is None: raise HTTPException(status_code=404, detail="Assignment not found") - ensure_class_permission(user, "class_view", assignment.class_id) + ensure_class_access(user, assignment.class_id) # Upload file file_url = None diff --git a/backend/app/api/classes.py b/backend/app/api/classes.py index 803cfdb..0992bc7 100644 --- a/backend/app/api/classes.py +++ b/backend/app/api/classes.py @@ -119,7 +119,7 @@ async def get_members( status: str | None = None, page: int = 1, page_size: int = 50, - admin: User = Depends(require_role("super_admin", "teacher", "student")), + admin: User = Depends(require_role("super_admin", "teacher")), db: AsyncSession = Depends(get_db), ): ensure_class_permission(admin, "member_view", class_id) @@ -143,7 +143,7 @@ async def get_class_inactive_members( class_id: int, page: int = 1, page_size: int = 50, - admin: User = Depends(require_role("super_admin", "teacher", "student")), + admin: User = Depends(require_role("super_admin", "teacher")), db: AsyncSession = Depends(get_db), ): ensure_class_permission(admin, "member_manage", class_id) @@ -162,7 +162,7 @@ async def get_class_inactive_members( async def import_class_members( class_id: int, data: MemberImportRequest, - admin: User = Depends(require_role("super_admin", "teacher", "student")), + admin: User = Depends(require_role("super_admin", "teacher")), db: AsyncSession = Depends(get_db), ): ensure_class_permission(admin, "member_manage", class_id) @@ -174,7 +174,7 @@ async def import_class_members( async def upload_member_file( class_id: int, file: UploadFile = File(...), - admin: User = Depends(require_role("super_admin", "teacher", "student")), + admin: User = Depends(require_role("super_admin", "teacher")), db: AsyncSession = Depends(get_db), ): ensure_class_permission(admin, "member_manage", class_id) @@ -239,7 +239,7 @@ async def upload_member_file( async def delete_inactive_member_item( class_id: int, user_id: int, - admin: User = Depends(require_role("super_admin", "teacher", "student")), + admin: User = Depends(require_role("super_admin", "teacher")), db: AsyncSession = Depends(get_db), ): ensure_class_permission(admin, "member_manage", class_id) @@ -252,7 +252,7 @@ async def delete_inactive_member_item( @router.post("/{class_id}/inactive-members/clear") async def clear_class_inactive_members( class_id: int, - admin: User = Depends(require_role("super_admin", "teacher", "student")), + admin: User = Depends(require_role("super_admin", "teacher")), db: AsyncSession = Depends(get_db), ): ensure_class_permission(admin, "member_manage", class_id) @@ -266,7 +266,7 @@ async def clear_class_inactive_members( @router.get("/{class_id}/invite-code") async def get_invite_code( class_id: int, - admin: User = Depends(require_role("super_admin", "teacher", "student")), + admin: User = Depends(require_role("super_admin", "teacher")), db: AsyncSession = Depends(get_db), ): ensure_class_permission(admin, "member_manage", class_id) @@ -279,7 +279,7 @@ async def get_invite_code( @router.post("/{class_id}/invite-code/regenerate") async def regenerate_invite( class_id: int, - admin: User = Depends(require_role("super_admin", "teacher", "student")), + admin: User = Depends(require_role("super_admin", "teacher")), db: AsyncSession = Depends(get_db), ): ensure_class_permission(admin, "member_manage", class_id) diff --git a/backend/app/api/directory.py b/backend/app/api/directory.py index f84862f..395c3c3 100644 --- a/backend/app/api/directory.py +++ b/backend/app/api/directory.py @@ -1,7 +1,7 @@ from fastapi import APIRouter, Depends, HTTPException, status from sqlalchemy.ext.asyncio import AsyncSession -from app.core.deps import ensure_class_permission, get_current_user, resolve_class_id_for_user +from app.core.deps import ensure_class_access, get_current_user, resolve_class_id_for_user from app.db.database import get_db from app.db.models import User from app.schemas.user import UserPublic @@ -26,7 +26,7 @@ async def search_members( effective_class_id = resolve_class_id_for_user(user, class_id) if effective_class_id is None: return PageResponse(items=[], total=0, page=page, page_size=page_size, total_pages=0) - ensure_class_permission(user, "class_view", effective_class_id) + ensure_class_access(user, effective_class_id) users, total = await search_directory( db, effective_class_id, search, industry, company, page, page_size diff --git a/backend/app/api/fund.py b/backend/app/api/fund.py index 285371e..2bb444d 100644 --- a/backend/app/api/fund.py +++ b/backend/app/api/fund.py @@ -1,7 +1,7 @@ from fastapi import APIRouter, Depends, HTTPException from sqlalchemy.ext.asyncio import AsyncSession -from app.core.deps import ensure_class_permission, require_role, resolve_class_id_for_user +from app.core.deps import ensure_class_access, ensure_class_permission, require_role, resolve_class_id_for_user from app.db.database import get_db from app.db.models import FundRecord, User from app.schemas.fund import FundRecordCreate, FundRecordUpdate, FundRecordOut, FundStatistics @@ -42,7 +42,7 @@ async def get_statistics( total_income=0, total_expense=0, balance=0, income_by_category=[], expense_by_category=[] ) - ensure_class_permission(user, "class_view", effective_class_id) + ensure_class_access(user, effective_class_id) return await get_fund_statistics(db, effective_class_id) @@ -59,7 +59,7 @@ async def get_fund_records( effective_class_id = resolve_class_id_for_user(user, class_id) if effective_class_id is None: return PageResponse(items=[], total=0, page=page, page_size=page_size, total_pages=0) - ensure_class_permission(user, "class_view", effective_class_id) + ensure_class_access(user, effective_class_id) records, total = await list_fund_records(db, effective_class_id, page, page_size, type, category) total_pages = (total + page_size - 1) // page_size diff --git a/backend/app/api/resources.py b/backend/app/api/resources.py index f358fa0..516ce2c 100644 --- a/backend/app/api/resources.py +++ b/backend/app/api/resources.py @@ -1,7 +1,7 @@ from fastapi import APIRouter, Depends, HTTPException, UploadFile, File, Form from sqlalchemy.ext.asyncio import AsyncSession -from app.core.deps import ensure_class_permission, require_role, resolve_class_id_for_user +from app.core.deps import ensure_class_access, ensure_class_permission, require_role, resolve_class_id_for_user from app.db.database import get_db from app.db.models import User from app.schemas.resource import ResourceCreate, ResourceOut @@ -46,7 +46,7 @@ async def get_resources( effective_class_id = resolve_class_id_for_user(user, class_id) if effective_class_id is None: return PageResponse(items=[], total=0, page=page, page_size=page_size, total_pages=0) - ensure_class_permission(user, "class_view", effective_class_id) + ensure_class_access(user, effective_class_id) resources, total = await list_resources(db, effective_class_id, category, page, page_size) total_pages = (total + page_size - 1) // page_size @@ -134,7 +134,7 @@ async def download_resource( resource = await get_resource_by_id(db, resource_id) if resource is None: raise HTTPException(status_code=404, detail="Resource not found") - ensure_class_permission(user, "class_view", resource.class_id) + ensure_class_access(user, resource.class_id) await increment_download_count(db, resource) return {"file_url": resource.file_url} diff --git a/backend/app/api/schedule.py b/backend/app/api/schedule.py index cfd3057..72d789c 100644 --- a/backend/app/api/schedule.py +++ b/backend/app/api/schedule.py @@ -1,7 +1,7 @@ from fastapi import APIRouter, Depends, HTTPException, status from sqlalchemy.ext.asyncio import AsyncSession -from app.core.deps import ensure_class_permission, require_role, resolve_class_id_for_user +from app.core.deps import ensure_class_access, ensure_class_permission, require_role, resolve_class_id_for_user from app.db.database import get_db from app.db.models import User from app.schemas.schedule import ScheduleCreate, ScheduleUpdate, ScheduleOut @@ -28,7 +28,7 @@ async def get_upcoming( effective_class_id = resolve_class_id_for_user(user, class_id) if effective_class_id is None: return [] - ensure_class_permission(user, "class_view", effective_class_id) + ensure_class_access(user, effective_class_id) items = await get_upcoming_schedules(db, effective_class_id, limit) return [ScheduleOut.model_validate(i) for i in items] @@ -45,7 +45,7 @@ async def get_schedules( effective_class_id = resolve_class_id_for_user(user, class_id) if effective_class_id is None: return PageResponse(items=[], total=0, page=page, page_size=page_size, total_pages=0) - ensure_class_permission(user, "class_view", effective_class_id) + ensure_class_access(user, effective_class_id) items, total = await list_schedules(db, effective_class_id, type, page, page_size) total_pages = (total + page_size - 1) // page_size diff --git a/backend/app/api/timeline.py b/backend/app/api/timeline.py index 58a71c7..8555ac7 100644 --- a/backend/app/api/timeline.py +++ b/backend/app/api/timeline.py @@ -3,6 +3,7 @@ from sqlalchemy.ext.asyncio import AsyncSession import asyncio from app.core.deps import ( + ensure_class_access, ensure_class_permission, get_effective_class_permissions, require_role, @@ -79,7 +80,7 @@ async def get_timelines( effective_class_id = resolve_class_id_for_user(user, class_id) if effective_class_id is None: return PageResponse(items=[], total=0, page=page, page_size=page_size, total_pages=0) - ensure_class_permission(user, "class_view", effective_class_id) + ensure_class_access(user, effective_class_id) posts, total = await list_timelines(db, effective_class_id, page, page_size) total_pages = (total + page_size - 1) // page_size @@ -103,7 +104,7 @@ async def create_new_timeline( effective_class_id = resolve_class_id_for_user(user, class_id) if effective_class_id is None: raise HTTPException(status_code=400, detail="You are not assigned to a class") - ensure_class_permission(user, "class_view", effective_class_id) + ensure_class_access(user, effective_class_id) data = TimelineCreate(title=title, content=content) post = await create_timeline(db, effective_class_id, user.id, data) @@ -153,7 +154,7 @@ async def upload_timeline_images( can_manage = "timeline_manage" in get_effective_class_permissions(user, post.class_id) if not can_manage and post.author_id != user.id: raise HTTPException(status_code=403, detail="Access denied") - ensure_class_permission(user, "class_view", post.class_id) + ensure_class_access(user, post.class_id) urls = [] for f in files: @@ -182,7 +183,7 @@ async def update_existing_timeline( can_manage = "timeline_manage" in get_effective_class_permissions(user, post.class_id) if not can_manage and post.author_id != user.id: raise HTTPException(status_code=403, detail="Access denied") - ensure_class_permission(user, "class_view", post.class_id) + ensure_class_access(user, post.class_id) updated = await update_timeline(db, post, data) return _build_timeline_out(updated, user.id) @@ -200,7 +201,7 @@ async def delete_existing_timeline( can_manage = "timeline_manage" in get_effective_class_permissions(user, post.class_id) if not can_manage and post.author_id != user.id: raise HTTPException(status_code=403, detail="Access denied") - ensure_class_permission(user, "class_view", post.class_id) + ensure_class_access(user, post.class_id) await delete_timeline(db, post) return {"message": "Timeline post deleted"} @@ -217,7 +218,7 @@ async def like_timeline_post( post = await get_timeline_by_id(db, post_id) if post is None: raise HTTPException(status_code=404, detail="Timeline post not found") - ensure_class_permission(user, "class_view", post.class_id) + ensure_class_access(user, post.class_id) return await toggle_like(db, post_id, user.id) @@ -232,7 +233,7 @@ async def get_post_comments( post = await get_timeline_by_id(db, post_id) if post is None: raise HTTPException(status_code=404, detail="Timeline post not found") - ensure_class_permission(user, "class_view", post.class_id) + ensure_class_access(user, post.class_id) comments, total = await list_comments(db, post_id, page, page_size) total_pages = (total + page_size - 1) // page_size items = [ @@ -260,7 +261,7 @@ async def add_post_comment( post = await get_timeline_by_id(db, post_id) if post is None: raise HTTPException(status_code=404, detail="Timeline post not found") - ensure_class_permission(user, "class_view", post.class_id) + ensure_class_access(user, post.class_id) comment = await create_comment(db, post_id, user.id, data) return TimelineCommentOut( @@ -283,13 +284,13 @@ async def delete_timeline_comment( comment = await get_comment_by_id(db, comment_id) if comment is None: raise HTTPException(status_code=404, detail="Comment not found") - can_manage = "timeline_manage" in get_effective_class_permissions(user, post.class_id) - if not can_manage and comment.author_id != user.id: - raise HTTPException(status_code=403, detail="Access denied") post = await get_timeline_by_id(db, comment.post_id) if post is None: raise HTTPException(status_code=404, detail="Timeline post not found") - ensure_class_permission(user, "class_view", post.class_id) + can_manage = "timeline_manage" in get_effective_class_permissions(user, post.class_id) + if not can_manage and comment.author_id != user.id: + raise HTTPException(status_code=403, detail="Access denied") + ensure_class_access(user, post.class_id) await delete_comment(db, comment) return {"message": "Comment deleted"} diff --git a/backend/app/api/users.py b/backend/app/api/users.py index 91dfbdf..5ae9726 100644 --- a/backend/app/api/users.py +++ b/backend/app/api/users.py @@ -86,7 +86,7 @@ async def list_all_users( class_id: int | None = None, status: str | None = None, role: str | None = None, - admin: User = Depends(require_role("super_admin", "teacher", "student")), + admin: User = Depends(require_role("super_admin", "teacher")), db: AsyncSession = Depends(get_db), ): if class_id is not None: @@ -109,7 +109,7 @@ async def list_all_users( async def change_user_status( user_id: int, data: UserStatusUpdate, - admin: User = Depends(require_role("super_admin", "teacher", "student")), + admin: User = Depends(require_role("super_admin", "teacher")), db: AsyncSession = Depends(get_db), ): target = await get_user_by_id(db, user_id) @@ -151,7 +151,7 @@ async def change_user_role( async def change_committee_role( user_id: int, data: CommitteeRoleUpdate, - admin: User = Depends(require_role("super_admin", "teacher", "student")), + admin: User = Depends(require_role("super_admin", "teacher")), db: AsyncSession = Depends(get_db), ): target = await get_user_by_id(db, user_id) @@ -171,7 +171,7 @@ async def change_committee_role( async def change_class_permissions( user_id: int, data: ClassPermissionsUpdate, - admin: User = Depends(require_role("super_admin", "teacher", "student")), + admin: User = Depends(require_role("super_admin", "teacher")), db: AsyncSession = Depends(get_db), ): target = await get_user_by_id(db, user_id) diff --git a/backend/app/api/votes.py b/backend/app/api/votes.py index d4ea8b8..f6e4f78 100644 --- a/backend/app/api/votes.py +++ b/backend/app/api/votes.py @@ -2,6 +2,7 @@ from fastapi import APIRouter, Depends, HTTPException from sqlalchemy.ext.asyncio import AsyncSession from app.core.deps import ( + ensure_class_access, ensure_class_permission, get_effective_class_permissions, require_role, @@ -81,7 +82,7 @@ async def get_votes( effective_class_id = resolve_class_id_for_user(user, class_id) if effective_class_id is None: return PageResponse(items=[], total=0, page=page, page_size=page_size, total_pages=0) - ensure_class_permission(user, "class_view", effective_class_id) + ensure_class_access(user, effective_class_id) votes, total = await list_votes(db, effective_class_id, page, page_size) total_pages = (total + page_size - 1) // page_size @@ -104,7 +105,7 @@ async def create_new_vote( effective_class_id = resolve_class_id_for_user(user, class_id) if effective_class_id is None: raise HTTPException(status_code=400, detail="You are not assigned to a class") - ensure_class_permission(user, "class_view", effective_class_id) + ensure_class_access(user, effective_class_id) vote = await create_vote(db, effective_class_id, user.id, data) # Reload with relationships @@ -121,7 +122,7 @@ async def get_vote_detail( vote = await get_vote_by_id(db, vote_id) if vote is None: raise HTTPException(status_code=404, detail="Vote not found") - ensure_class_permission(user, "class_view", vote.class_id) + ensure_class_access(user, vote.class_id) return _build_vote_out(vote, user.id) @@ -135,7 +136,7 @@ async def submit_vote_response( vote = await get_vote_by_id(db, vote_id) if vote is None: raise HTTPException(status_code=404, detail="Vote not found") - ensure_class_permission(user, "class_view", vote.class_id) + ensure_class_access(user, vote.class_id) try: await submit_vote(db, vote_id, user.id, data.option_ids) @@ -158,7 +159,7 @@ async def close_vote_endpoint( can_manage = "vote_manage" in get_effective_class_permissions(user, vote.class_id) if not can_manage and user.role == "student" and vote.creator_id != user.id: raise HTTPException(status_code=403, detail="只有创建者或管理员可以关闭投票") - ensure_class_permission(user, "class_view", vote.class_id) + ensure_class_access(user, vote.class_id) await close_vote(db, vote) return {"message": "投票已关闭"} @@ -176,7 +177,7 @@ async def delete_vote_endpoint( can_manage = "vote_manage" in get_effective_class_permissions(user, vote.class_id) if not can_manage and user.role == "student" and vote.creator_id != user.id: raise HTTPException(status_code=403, detail="只有创建者或管理员可以删除投票") - ensure_class_permission(user, "class_view", vote.class_id) + ensure_class_access(user, vote.class_id) await delete_vote(db, vote) return {"message": "投票已删除"} diff --git a/backend/app/core/deps.py b/backend/app/core/deps.py index ca2b25b..764b356 100644 --- a/backend/app/core/deps.py +++ b/backend/app/core/deps.py @@ -11,7 +11,6 @@ from app.db.models import ClassMembership, User security = HTTPBearer() CLASS_PERMISSIONS = { - "class_view", "member_view", "member_manage", "committee_manage", @@ -26,7 +25,6 @@ CLASS_PERMISSIONS = { } TEACHER_DEFAULT_PERMISSIONS = { - "class_view", "member_view", "member_manage", "committee_manage", @@ -118,6 +116,11 @@ def get_effective_class_permissions(user: User, class_id: int | None = None) -> scoped_permissions = membership.get_class_permissions() if membership else [] if user.role == "teacher": return set(TEACHER_DEFAULT_PERMISSIONS) | set(scoped_permissions) + scoped_permissions = [ + permission + for permission in scoped_permissions + if permission not in {"member_view", "member_manage", "committee_manage"} + ] return set(scoped_permissions) diff --git a/backend/app/schemas/user.py b/backend/app/schemas/user.py index 74b0cf9..b06f0fc 100644 --- a/backend/app/schemas/user.py +++ b/backend/app/schemas/user.py @@ -5,6 +5,14 @@ from pydantic import BaseModel, EmailStr, Field from app.db.models import ClassMembership, User +def normalize_class_permissions(permissions: list[str]) -> list[str]: + return [ + permission + for permission in permissions + if permission not in {"class_view", "member_view", "member_manage", "committee_manage"} + ] + + class MembershipOut(BaseModel): id: int class_id: int @@ -129,7 +137,7 @@ def build_membership_out(membership: ClassMembership) -> MembershipOut: class_name=membership.class_.name if membership.class_ else None, membership_role=membership.membership_role, committee_role=membership.committee_role, - class_permissions=membership.get_class_permissions(), + class_permissions=normalize_class_permissions(membership.get_class_permissions()), ) @@ -170,7 +178,7 @@ def build_user_list_item(user: User, class_id: int | None = None) -> UserListIte industry=user.industry, company=user.company, committee_role=active_membership.committee_role if active_membership else None, - class_permissions=active_membership.get_class_permissions() if active_membership else [], + class_permissions=normalize_class_permissions(active_membership.get_class_permissions()) if active_membership else [], created_at=user.created_at, memberships=memberships, active_membership=build_membership_out(active_membership) if active_membership else None, diff --git a/frontend/src/app/(app)/admin/members/page.tsx b/frontend/src/app/(app)/admin/members/page.tsx index 7e7f1a3..b56fec7 100644 --- a/frontend/src/app/(app)/admin/members/page.tsx +++ b/frontend/src/app/(app)/admin/members/page.tsx @@ -43,7 +43,6 @@ import type { UserListItem, } from "@/lib/types"; import { CLASS_PERMISSIONS, ROLES } from "@/lib/constants"; -import { hasClassPermission } from "@/lib/permissions"; type InviteCodeResponse = { invite_code: string; @@ -82,8 +81,8 @@ const PERMISSION_GROUPS: Array<{ }> = [ { title: "成员与组织", - description: "管理班级成员、班委和基础可见范围", - permissions: ["class_view", "member_view", "member_manage", "committee_manage"], + description: "老师与超级管理员固定拥有成员管理与班委设置能力", + permissions: [], }, { title: "内容与活动", @@ -112,10 +111,6 @@ const COMMITTEE_PRESETS: Array<{ role: "班长", description: "统筹班级日常管理与主要内容发布", permissions: [ - "class_view", - "member_view", - "member_manage", - "committee_manage", "announcement_manage", "timeline_manage", "vote_manage", @@ -127,9 +122,6 @@ const COMMITTEE_PRESETS: Array<{ role: "副班长", description: "协助班长处理班级组织和内容管理", permissions: [ - "class_view", - "member_view", - "member_manage", "announcement_manage", "timeline_manage", "vote_manage", @@ -141,8 +133,6 @@ const COMMITTEE_PRESETS: Array<{ role: "学习委员", description: "负责学习通知、作业协同与课程安排", permissions: [ - "class_view", - "member_view", "announcement_manage", "assignment_manage", "schedule_manage", @@ -153,8 +143,6 @@ const COMMITTEE_PRESETS: Array<{ role: "组织委员", description: "负责活动组织、投票协同和排期推进", permissions: [ - "class_view", - "member_view", "timeline_manage", "vote_manage", "schedule_manage", @@ -165,8 +153,6 @@ const COMMITTEE_PRESETS: Array<{ role: "宣传委员", description: "负责公告发布、班级宣传与内容协同传播", permissions: [ - "class_view", - "member_view", "announcement_manage", "timeline_manage", "vote_manage", @@ -177,8 +163,6 @@ const COMMITTEE_PRESETS: Array<{ role: "生活委员", description: "负责生活事务通知与班费相关管理", permissions: [ - "class_view", - "member_view", "announcement_manage", "fund_manage", "resource_manage", @@ -188,8 +172,6 @@ const COMMITTEE_PRESETS: Array<{ role: "文体委员", description: "负责文体活动、动态发布与投票互动", permissions: [ - "class_view", - "member_view", "timeline_manage", "vote_manage", "schedule_manage", @@ -238,12 +220,13 @@ export default function MembersPage() { const fileInputRef = useRef(null); const isSuperAdmin = user?.role === "super_admin"; - const canManageCommittee = hasClassPermission(user, "committee_manage", activeClassId); - const canOpenMemberManager = isSuperAdmin || canManageCommittee; + const canManageMembers = user?.role === "super_admin" || user?.role === "teacher"; + const canManageCommittee = canManageMembers; + const canOpenMemberManager = canManageMembers; // Load members const loadMembers = useCallback(async () => { - if (!activeClassId) { + if (!activeClassId || !canManageMembers) { setMembers([]); setMembersLoading(false); return; @@ -266,11 +249,11 @@ export default function MembersPage() { } finally { setMembersLoading(false); } - }, [activeClassId, membersPage]); + }, [activeClassId, canManageMembers, membersPage]); // Load inactive members const loadInactiveMembers = useCallback(async () => { - if (!activeClassId) { + if (!activeClassId || !canManageMembers) { setInactiveMembers([]); setInviteCode(""); setInactiveLoading(false); @@ -294,16 +277,16 @@ export default function MembersPage() { } finally { setInactiveLoading(false); } - }, [activeClassId, inactivePage]); + }, [activeClassId, canManageMembers, inactivePage]); useEffect(() => { - if (!activeClassId) return; + if (!activeClassId || !canManageMembers) return; if (activeTab === "members") { void loadMembers(); return; } void loadInactiveMembers(); - }, [activeClassId, activeTab, loadInactiveMembers, loadMembers]); + }, [activeClassId, activeTab, canManageMembers, loadInactiveMembers, loadMembers]); // Member actions const handleRoleChange = async (userId: number, newRole: string) => { @@ -448,7 +431,15 @@ export default function MembersPage() { }; const applyPermissionPreset = (permissions: ClassPermission[]) => { - setMemberPermissionsValue(Array.from(new Set(permissions))); + setMemberPermissionsValue( + Array.from( + new Set( + permissions.filter( + (permission) => !["member_view", "member_manage", "committee_manage"].includes(permission) + ) + ) + ) + ); }; const applyCommitteePreset = (presetRole: string) => { @@ -464,7 +455,9 @@ export default function MembersPage() { try { await putAPI(`/api/users/${userId}/class-permissions`, { class_id: activeClassId, - class_permissions: permissions, + class_permissions: permissions.filter( + (permission) => !["member_view", "member_manage", "committee_manage"].includes(permission) + ), }); } catch (err: unknown) { toast.error(getErrorMessage(err, "操作失败")); @@ -550,7 +543,11 @@ export default function MembersPage() { setManagingMember(member); setMemberRoleValue(member.role); setMemberCommitteeValue(member.committee_role ?? ""); - setMemberPermissionsValue(member.class_permissions ?? []); + setMemberPermissionsValue( + (member.class_permissions ?? []).filter( + (permission) => !["member_view", "member_manage", "committee_manage"].includes(permission) + ) + ); setMemberManagerOpen(true); }; @@ -567,7 +564,9 @@ export default function MembersPage() { const originalRole = managingMember.role; const originalCommittee = managingMember.committee_role ?? ""; - const originalPermissions = managingMember.class_permissions ?? []; + const originalPermissions = (managingMember.class_permissions ?? []).filter( + (permission) => !["member_view", "member_manage", "committee_manage"].includes(permission) + ); const normalizedCommittee = memberCommitteeValue.trim(); const permissionsChanged = originalPermissions.length !== memberPermissionsValue.length || @@ -619,6 +618,14 @@ export default function MembersPage() {
{isSuperAdmin ? "请在顶部选择一个班级" : "您尚未分配班级"}
+ ) : !canManageMembers ? ( + + +

+ 只有老师和超级管理员可以进入成员管理。 +

+
+
) : ( <> {/* Tab switcher */} @@ -1145,7 +1152,6 @@ export default function MembersPage() { className="h-7 text-xs" onClick={() => applyPermissionPreset([ - "member_view", "announcement_manage", "timeline_manage", "vote_manage", @@ -1187,7 +1193,7 @@ export default function MembersPage() {
- {PERMISSION_GROUPS.map((group) => ( + {PERMISSION_GROUPS.filter((group) => group.permissions.length > 0).map((group) => (

{group.title}

diff --git a/frontend/src/app/(app)/admin/page.tsx b/frontend/src/app/(app)/admin/page.tsx index c772b14..5b65cba 100644 --- a/frontend/src/app/(app)/admin/page.tsx +++ b/frontend/src/app/(app)/admin/page.tsx @@ -7,6 +7,7 @@ import Link from "next/link"; export default function AdminPage() { const { user } = useAuth(); + const canManageMembers = user?.role === "super_admin" || user?.role === "teacher"; return (
@@ -19,18 +20,20 @@ export default function AdminPage() {
- - - - 成员管理 - - -

- 导入成员、管理未激活成员和班级权限 -

-
-
- + {canManageMembers && ( + + + + 成员管理 + + +

+ 导入成员、管理未激活成员和班级权限 +

+
+
+ + )} diff --git a/frontend/src/components/sidebar.tsx b/frontend/src/components/sidebar.tsx index 479106b..b598b4f 100644 --- a/frontend/src/components/sidebar.tsx +++ b/frontend/src/components/sidebar.tsx @@ -23,7 +23,7 @@ const navItems = [ ]; 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", roles: ["super_admin", "teacher", "student"] as UserRole[], permissions: ["member_view"] as ClassPermission[] }, + { 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", "teacher"] as UserRole[], permissions: ["member_view"] as ClassPermission[] }, { 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[] }, { href: "/admin/modules", label: "模块管理", icon: "M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.066 2.573c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.573 1.066c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.066-2.573c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z M15 12a3 3 0 11-6 0 3 3 0 016 0z", roles: ["super_admin", "teacher", "student"] as UserRole[], permissions: ["module_manage"] as ClassPermission[] }, ]; diff --git a/frontend/src/lib/constants.ts b/frontend/src/lib/constants.ts index aaaed08..f235043 100644 --- a/frontend/src/lib/constants.ts +++ b/frontend/src/lib/constants.ts @@ -5,7 +5,6 @@ export const ROLES = { } as const; export const CLASS_PERMISSIONS = { - class_view: "班级查看", member_view: "成员查看", member_manage: "成员管理", committee_manage: "班委设置", @@ -20,7 +19,6 @@ export const CLASS_PERMISSIONS = { } as const; export const TEACHER_DEFAULT_PERMISSIONS = [ - "class_view", "member_view", "member_manage", "committee_manage", diff --git a/frontend/src/lib/permissions.ts b/frontend/src/lib/permissions.ts index d155a4f..ebcd894 100644 --- a/frontend/src/lib/permissions.ts +++ b/frontend/src/lib/permissions.ts @@ -20,7 +20,6 @@ export function getEffectiveClassPermissions( if (!user) return new Set(); if (user.role === "super_admin") { return new Set([ - "class_view", "member_view", "member_manage", "committee_manage", diff --git a/frontend/src/lib/types.ts b/frontend/src/lib/types.ts index 8034d57..59043a1 100644 --- a/frontend/src/lib/types.ts +++ b/frontend/src/lib/types.ts @@ -2,7 +2,6 @@ export type UserRole = "super_admin" | "teacher" | "student"; export type UserStatus = "inactive" | "approved" | "disabled"; export type ScheduleType = "course" | "deadline" | "activity"; export type ClassPermission = - | "class_view" | "member_view" | "member_manage" | "committee_manage"