hku-class/backend/app/api/users.py
2026-04-27 23:09:51 +08:00

233 lines
7.4 KiB
Python

from fastapi import APIRouter, Depends, HTTPException, UploadFile, File
from sqlalchemy.ext.asyncio import AsyncSession
from app.core.deps import (
CLASS_PERMISSIONS,
ensure_class_access,
ensure_class_permission,
get_current_user,
require_role,
)
from app.db.database import get_db
from app.db.models import User
from app.schemas.user import (
TeacherAssignRequest,
TeacherAssignResponse,
TeacherCreateRequest,
TeacherCreateResponse,
UserOut,
UserUpdate,
UserListItem,
UserStatusUpdate,
UserRoleUpdate,
CommitteeRoleUpdate,
ClassPermissionsUpdate,
build_user_list_item,
build_user_out,
)
from app.schemas.common import PageResponse
from app.services.user_service import (
update_profile,
update_user_status,
update_user_role,
update_user_committee_role,
update_user_class_permissions,
assign_existing_teacher_to_class,
create_or_assign_teacher,
list_users,
get_user_by_id,
)
from app.services.cos_service import upload_image
router = APIRouter(prefix="/api/users", tags=["users"])
@router.get("/me", response_model=UserOut)
async def get_my_profile(user: User = Depends(get_current_user)):
return build_user_out(user)
@router.put("/me", response_model=UserOut)
async def update_my_profile(
data: UserUpdate,
user: User = Depends(get_current_user),
db: AsyncSession = Depends(get_db),
):
try:
updated = await update_profile(db, user, data)
except ValueError as e:
raise HTTPException(status_code=400, detail=str(e))
return build_user_out(updated)
@router.post("/me/avatar")
async def upload_avatar(
file: UploadFile = File(...),
user: User = Depends(get_current_user),
db: AsyncSession = Depends(get_db),
):
contents = await file.read()
if len(contents) > 5 * 1024 * 1024: # 5MB limit
raise HTTPException(status_code=400, detail="File too large (max 5MB)")
if file.content_type not in {"image/jpeg", "image/png", "image/gif", "image/webp"}:
raise HTTPException(status_code=400, detail="Invalid file type")
url = upload_image(f"avatars/{user.id}", file.filename or "avatar.jpg", contents, file.content_type)
user.avatar_url = url
await db.commit()
return {"avatar_url": url}
@router.get("/", response_model=PageResponse[UserListItem])
async def list_all_users(
page: int = 1,
page_size: int = 20,
class_id: int | None = None,
status: str | None = None,
role: str | None = None,
admin: User = Depends(require_role("super_admin", "teacher")),
db: AsyncSession = Depends(get_db),
):
if class_id is not None:
ensure_class_permission(admin, "member_view", class_id)
elif admin.role != "super_admin":
raise HTTPException(status_code=400, detail="class_id is required")
users, total = await list_users(db, page, page_size, class_id, status, role)
total_pages = (total + page_size - 1) // page_size
return PageResponse(
items=[build_user_list_item(u, class_id) for u in users],
total=total,
page=page,
page_size=page_size,
total_pages=total_pages,
)
@router.put("/{user_id}/status")
async def change_user_status(
user_id: int,
data: UserStatusUpdate,
admin: User = Depends(require_role("super_admin", "teacher")),
db: AsyncSession = Depends(get_db),
):
target = await get_user_by_id(db, user_id)
if target is None:
raise HTTPException(status_code=404, detail="User not found")
target_class_id = target.get_default_membership().class_id if target.get_default_membership() else None
ensure_class_permission(admin, "member_manage", target_class_id)
# Only super_admin can change roles
if data.role and admin.role != "super_admin":
raise HTTPException(
status_code=403, detail="Only super admin can change user roles"
)
updated = await update_user_status(db, user_id, data.status, data.role)
return {"message": f"用户状态已更新为 {data.status}"}
@router.put("/{user_id}/role")
async def change_user_role(
user_id: int,
data: UserRoleUpdate,
admin: User = Depends(require_role("super_admin")),
db: AsyncSession = Depends(get_db),
):
if data.role not in ("super_admin", "teacher", "student"):
raise HTTPException(status_code=400, detail="Invalid role")
target = await get_user_by_id(db, user_id)
if target is None:
raise HTTPException(status_code=404, detail="User not found")
await update_user_role(db, target, data.role)
return {"message": f"User role updated to {data.role}"}
@router.put("/{user_id}/committee-role")
async def change_committee_role(
user_id: int,
data: CommitteeRoleUpdate,
admin: User = Depends(require_role("super_admin", "teacher")),
db: AsyncSession = Depends(get_db),
):
target = await get_user_by_id(db, user_id)
if target is None:
raise HTTPException(status_code=404, detail="User not found")
ensure_class_permission(admin, "committee_manage", data.class_id)
try:
await update_user_committee_role(db, target, data.class_id, data.committee_role)
except ValueError as e:
raise HTTPException(status_code=400, detail=str(e))
return {"message": "Committee role updated"}
@router.put("/{user_id}/class-permissions")
async def change_class_permissions(
user_id: int,
data: ClassPermissionsUpdate,
admin: User = Depends(require_role("super_admin", "teacher")),
db: AsyncSession = Depends(get_db),
):
target = await get_user_by_id(db, user_id)
if target is None:
raise HTTPException(status_code=404, detail="User not found")
ensure_class_permission(admin, "committee_manage", data.class_id)
invalid = [permission for permission in data.class_permissions if permission not in CLASS_PERMISSIONS]
if invalid:
raise HTTPException(status_code=400, detail=f"Invalid permissions: {', '.join(invalid)}")
try:
await update_user_class_permissions(db, target, data.class_id, data.class_permissions)
except ValueError as e:
raise HTTPException(status_code=400, detail=str(e))
return {"message": "Class permissions updated"}
@router.post("/teachers", response_model=TeacherCreateResponse)
async def create_teacher_user(
data: TeacherCreateRequest,
admin: User = Depends(require_role("super_admin")),
db: AsyncSession = Depends(get_db),
):
try:
user, created, assigned = await create_or_assign_teacher(db, data)
except ValueError as e:
raise HTTPException(status_code=400, detail=str(e))
if created:
message = "老师账号已创建并加入当前班级"
elif assigned:
message = "老师账号已加入当前班级"
else:
message = "该老师已存在于当前班级"
return TeacherCreateResponse(
message=message,
user=build_user_out(user, data.class_id),
)
@router.post("/teachers/assign", response_model=TeacherAssignResponse)
async def assign_teacher_to_class(
data: TeacherAssignRequest,
admin: User = Depends(require_role("super_admin")),
db: AsyncSession = Depends(get_db),
):
try:
user, assigned = await assign_existing_teacher_to_class(db, data)
except ValueError as e:
raise HTTPException(status_code=400, detail=str(e))
return TeacherAssignResponse(
message="老师已加入当前班级" if assigned else "该老师已在当前班级",
user=build_user_out(user, data.class_id),
)