This commit is contained in:
aaron 2026-04-27 23:09:51 +08:00
parent 105bde0d3c
commit cf695efb38
18 changed files with 120 additions and 101 deletions

View File

@ -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

View File

@ -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

View File

@ -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)

View File

@ -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

View File

@ -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

View File

@ -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}

View File

@ -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

View File

@ -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"}

View File

@ -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)

View File

@ -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": "投票已删除"}

View File

@ -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)

View File

@ -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,

View File

@ -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<HTMLInputElement>(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<UserListItem>(`/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() {
<div className="py-12 text-center text-[#9d806f]">
{isSuperAdmin ? "请在顶部选择一个班级" : "您尚未分配班级"}
</div>
) : !canManageMembers ? (
<Card className="bg-[#fffaf2]">
<CardContent className="p-6">
<p className="text-sm text-[#765a4d]">
</p>
</CardContent>
</Card>
) : (
<>
{/* 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() {
</div>
<div className="space-y-3">
{PERMISSION_GROUPS.map((group) => (
{PERMISSION_GROUPS.filter((group) => group.permissions.length > 0).map((group) => (
<div key={group.title} className="rounded-xl border border-[#eadbc8] bg-[#fffaf2] p-3">
<div className="mb-2">
<p className="text-sm font-medium text-[#4e1d1a]">{group.title}</p>

View File

@ -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 (
<div className="space-y-6">
@ -19,6 +20,7 @@ export default function AdminPage() {
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
{canManageMembers && (
<Link href="/admin/members">
<Card className="cursor-pointer bg-[#fffaf2] transition-all hover:-translate-y-0.5 hover:shadow-[0_24px_45px_-32px_rgba(84,29,23,0.38)]">
<CardHeader>
@ -31,6 +33,7 @@ export default function AdminPage() {
</CardContent>
</Card>
</Link>
)}
<Link href="/admin/classes">
<Card className="cursor-pointer bg-[#fffaf2] transition-all hover:-translate-y-0.5 hover:shadow-[0_24px_45px_-32px_rgba(84,29,23,0.38)]">

View File

@ -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[] },
];

View File

@ -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",

View File

@ -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",

View File

@ -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"