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