1
This commit is contained in:
parent
105bde0d3c
commit
cf695efb38
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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}
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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"}
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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": "投票已删除"}
|
||||
|
||||
@ -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)
|
||||
|
||||
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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)]">
|
||||
|
||||
@ -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[] },
|
||||
];
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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"
|
||||
|
||||
Loading…
Reference in New Issue
Block a user