233 lines
7.4 KiB
Python
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", "student")),
|
|
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", "student")),
|
|
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", "student")),
|
|
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", "student")),
|
|
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),
|
|
)
|