hku-class/backend/app/api/users.py
2026-04-12 18:15:38 +08:00

124 lines
3.9 KiB
Python

import json
from fastapi import APIRouter, Depends, HTTPException, UploadFile, File, status
from sqlalchemy.ext.asyncio import AsyncSession
from app.core.deps import get_current_user, require_role
from app.db.database import get_db
from app.db.models import User
from app.schemas.user import UserOut, UserUpdate, UserListItem, UserStatusUpdate
from app.schemas.common import PageResponse
from app.services.user_service import (
update_profile,
update_user_status,
list_users,
get_user_by_id,
)
from app.services.cos_service import upload_image
from app.services.email_service import send_approval_notification
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 UserOut.model_validate(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 UserOut.model_validate(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")),
db: AsyncSession = Depends(get_db),
):
users, total = await list_users(db, page, page_size, class_id, status, role)
total_pages = (total + page_size - 1) // page_size
return PageResponse(
items=[UserListItem.model_validate(u) 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", "class_admin")),
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")
# Class admin can only manage users in their own class
if admin.role == "class_admin" and target.class_id != admin.class_id:
raise HTTPException(
status_code=403, detail="Cannot manage users outside your class"
)
updated = await update_user_status(db, user_id, data.status, data.role)
# Send email notification
if data.status in ("approved", "rejected"):
await send_approval_notification(target.email, data.status == "approved")
return {"message": f"User status updated to {data.status}"}
@router.put("/{user_id}/role")
async def change_user_role(
user_id: int,
role: str,
admin: User = Depends(require_role("super_admin")),
db: AsyncSession = Depends(get_db),
):
if role not in ("super_admin", "class_admin", "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")
target.role = role
await db.commit()
return {"message": f"User role updated to {role}"}