- Backend: workspace_id isolation for 14 model tables + safe migration/backfill - Backend: RBAC system with 4 roles and 23 permissions, seeded on startup - Backend: workspace admin endpoints (list/manage all workspaces) - Backend: admin user management API (CRUD, reset password) - Backend: billing API with subscription plans, usage tracking, rate limiting - Backend: fix system_logs.py UNION query and wrong column references - Backend: WebSocket JWT auth and workspace enforcement - Frontend: sidebar navigation replacing top dropdown menu - Frontend: user management page (Users.vue) for admins - Frontend: enhanced Workspaces.vue with admin table view - Frontend: workspace RBAC computed properties in user store - Android: agent marketplace, billing/subscription UI, onboarding wizard - Android: phone login, analytics tracker, crash handler, network diagnostics - Android: splash screen, encrypted token storage, app update enhancements - Docs: multi-tenant RBAC guide with 8 sections and role-permission matrix Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
265 lines
8.2 KiB
Python
265 lines
8.2 KiB
Python
"""
|
||
Admin 用户管理 API(仅平台管理员可访问)
|
||
"""
|
||
from __future__ import annotations
|
||
|
||
import logging
|
||
import uuid
|
||
from datetime import datetime
|
||
from typing import List, Optional
|
||
|
||
from fastapi import APIRouter, Depends, HTTPException, Query
|
||
from pydantic import BaseModel, Field, EmailStr
|
||
from sqlalchemy.orm import Session
|
||
from sqlalchemy import func
|
||
|
||
from app.api.auth import get_current_user
|
||
from app.core.database import get_db
|
||
from app.core.security import get_password_hash
|
||
from app.models.user import User
|
||
from app.models.workspace import WorkspaceMembership
|
||
|
||
logger = logging.getLogger(__name__)
|
||
|
||
router = APIRouter(prefix="/api/v1/admin/users", tags=["admin-users"])
|
||
|
||
|
||
def _require_admin(current_user: User = Depends(get_current_user)) -> User:
|
||
if current_user.role != "admin":
|
||
raise HTTPException(status_code=403, detail="仅平台管理员可访问")
|
||
return current_user
|
||
|
||
|
||
# ── Request / Response schemas ──
|
||
|
||
class UserCreateRequest(BaseModel):
|
||
username: str = Field(..., min_length=2, max_length=50)
|
||
email: EmailStr
|
||
password: str = Field(..., min_length=6, max_length=128)
|
||
role: str = Field(default="user")
|
||
phone: Optional[str] = None
|
||
status: str = Field(default="active")
|
||
|
||
|
||
class UserUpdateRequest(BaseModel):
|
||
username: Optional[str] = Field(None, min_length=2, max_length=50)
|
||
email: Optional[EmailStr] = None
|
||
role: Optional[str] = None
|
||
phone: Optional[str] = None
|
||
status: Optional[str] = None
|
||
subscription_tier: Optional[str] = None
|
||
|
||
|
||
class ResetPasswordRequest(BaseModel):
|
||
new_password: str = Field(..., min_length=6, max_length=128)
|
||
|
||
|
||
class UserItem(BaseModel):
|
||
id: str
|
||
username: str
|
||
email: str
|
||
role: str
|
||
phone: Optional[str] = None
|
||
phone_verified: bool = False
|
||
status: str
|
||
is_email_verified: bool = False
|
||
subscription_tier: str = "free"
|
||
daily_usage_count: int = 0
|
||
workspace_count: int = 0
|
||
created_at: Optional[datetime] = None
|
||
updated_at: Optional[datetime] = None
|
||
|
||
class Config:
|
||
from_attributes = True
|
||
|
||
|
||
class UserListResponse(BaseModel):
|
||
items: List[UserItem]
|
||
total: int
|
||
page: int
|
||
page_size: int
|
||
|
||
|
||
# ── Endpoints ──
|
||
|
||
@router.get("", response_model=UserListResponse)
|
||
def list_users(
|
||
page: int = Query(1, ge=1),
|
||
page_size: int = Query(20, ge=1, le=100),
|
||
search: Optional[str] = Query(None, description="搜索用户名或邮箱"),
|
||
status: Optional[str] = Query(None, description="过滤状态: active/disabled/deleted"),
|
||
role: Optional[str] = Query(None, description="过滤角色"),
|
||
db: Session = Depends(get_db),
|
||
_admin: User = Depends(_require_admin),
|
||
):
|
||
"""获取用户列表(分页、搜索、过滤)"""
|
||
q = db.query(User)
|
||
|
||
if search:
|
||
like = f"%{search}%"
|
||
q = q.filter((User.username.ilike(like)) | (User.email.ilike(like)))
|
||
if status:
|
||
q = q.filter(User.status == status)
|
||
if role:
|
||
q = q.filter(User.role == role)
|
||
|
||
total = q.count()
|
||
items = q.order_by(User.created_at.desc()).offset((page - 1) * page_size).limit(page_size).all()
|
||
|
||
# 获取每个用户的 workspace 数量
|
||
user_ids = [u.id for u in items]
|
||
ws_counts = {}
|
||
if user_ids:
|
||
rows = (
|
||
db.query(
|
||
WorkspaceMembership.user_id,
|
||
func.count(WorkspaceMembership.workspace_id).label("cnt"),
|
||
)
|
||
.filter(WorkspaceMembership.user_id.in_(user_ids))
|
||
.group_by(WorkspaceMembership.user_id)
|
||
.all()
|
||
)
|
||
ws_counts = {r.user_id: r.cnt for r in rows}
|
||
|
||
return {
|
||
"items": [
|
||
{**{c.name: getattr(u, c.name) for c in User.__table__.columns},
|
||
"workspace_count": ws_counts.get(u.id, 0)}
|
||
for u in items
|
||
],
|
||
"total": total,
|
||
"page": page,
|
||
"page_size": page_size,
|
||
}
|
||
|
||
|
||
@router.post("", response_model=UserItem)
|
||
def create_user(
|
||
data: UserCreateRequest,
|
||
db: Session = Depends(get_db),
|
||
_admin: User = Depends(_require_admin),
|
||
):
|
||
"""管理员创建新用户"""
|
||
if db.query(User).filter(User.username == data.username).first():
|
||
raise HTTPException(status_code=409, detail="用户名已存在")
|
||
if db.query(User).filter(User.email == data.email).first():
|
||
raise HTTPException(status_code=409, detail="邮箱已存在")
|
||
|
||
user = User(
|
||
id=str(uuid.uuid4()),
|
||
username=data.username,
|
||
email=data.email,
|
||
password_hash=get_password_hash(data.password),
|
||
role=data.role,
|
||
phone=data.phone,
|
||
status=data.status,
|
||
)
|
||
db.add(user)
|
||
db.commit()
|
||
db.refresh(user)
|
||
|
||
logger.info("管理员创建了用户: %s (%s)", user.username, user.id)
|
||
return {**{c.name: getattr(user, c.name) for c in User.__table__.columns}, "workspace_count": 0}
|
||
|
||
|
||
@router.get("/{user_id}", response_model=UserItem)
|
||
def get_user(
|
||
user_id: str,
|
||
db: Session = Depends(get_db),
|
||
_admin: User = Depends(_require_admin),
|
||
):
|
||
"""获取用户详情"""
|
||
user = db.query(User).filter(User.id == user_id).first()
|
||
if not user:
|
||
raise HTTPException(status_code=404, detail="用户不存在")
|
||
|
||
ws_count = (
|
||
db.query(func.count(WorkspaceMembership.workspace_id))
|
||
.filter(WorkspaceMembership.user_id == user_id)
|
||
.scalar()
|
||
) or 0
|
||
|
||
return {**{c.name: getattr(user, c.name) for c in User.__table__.columns}, "workspace_count": ws_count}
|
||
|
||
|
||
@router.put("/{user_id}", response_model=UserItem)
|
||
def update_user(
|
||
user_id: str,
|
||
data: UserUpdateRequest,
|
||
db: Session = Depends(get_db),
|
||
_admin: User = Depends(_require_admin),
|
||
):
|
||
"""编辑用户"""
|
||
user = db.query(User).filter(User.id == user_id).first()
|
||
if not user:
|
||
raise HTTPException(status_code=404, detail="用户不存在")
|
||
|
||
if data.username is not None and data.username != user.username:
|
||
if db.query(User).filter(User.username == data.username, User.id != user_id).first():
|
||
raise HTTPException(status_code=409, detail="用户名已存在")
|
||
user.username = data.username
|
||
if data.email is not None and data.email != user.email:
|
||
if db.query(User).filter(User.email == data.email, User.id != user_id).first():
|
||
raise HTTPException(status_code=409, detail="邮箱已存在")
|
||
user.email = data.email
|
||
if data.role is not None:
|
||
user.role = data.role
|
||
if data.phone is not None:
|
||
user.phone = data.phone
|
||
if data.status is not None:
|
||
if data.status not in ("active", "disabled", "deleted"):
|
||
raise HTTPException(status_code=400, detail="无效的状态值")
|
||
user.status = data.status
|
||
if data.subscription_tier is not None:
|
||
user.subscription_tier = data.subscription_tier
|
||
|
||
db.commit()
|
||
db.refresh(user)
|
||
|
||
logger.info("管理员更新了用户: %s (%s)", user.username, user.id)
|
||
ws_count = (
|
||
db.query(func.count(WorkspaceMembership.workspace_id))
|
||
.filter(WorkspaceMembership.user_id == user_id)
|
||
.scalar()
|
||
) or 0
|
||
return {**{c.name: getattr(user, c.name) for c in User.__table__.columns}, "workspace_count": ws_count}
|
||
|
||
|
||
@router.delete("/{user_id}")
|
||
def delete_user(
|
||
user_id: str,
|
||
db: Session = Depends(get_db),
|
||
_admin: User = Depends(_require_admin),
|
||
):
|
||
"""软删除用户(标记为 deleted)"""
|
||
user = db.query(User).filter(User.id == user_id).first()
|
||
if not user:
|
||
raise HTTPException(status_code=404, detail="用户不存在")
|
||
if user.role == "admin":
|
||
raise HTTPException(status_code=400, detail="不能删除平台管理员")
|
||
|
||
user.status = "deleted"
|
||
db.commit()
|
||
|
||
logger.info("管理员软删除了用户: %s (%s)", user.username, user.id)
|
||
return {"message": "用户已删除"}
|
||
|
||
|
||
@router.post("/{user_id}/reset-password")
|
||
def reset_password(
|
||
user_id: str,
|
||
data: ResetPasswordRequest,
|
||
db: Session = Depends(get_db),
|
||
_admin: User = Depends(_require_admin),
|
||
):
|
||
"""重置用户密码"""
|
||
user = db.query(User).filter(User.id == user_id).first()
|
||
if not user:
|
||
raise HTTPException(status_code=404, detail="用户不存在")
|
||
|
||
user.password_hash = get_password_hash(data.new_password)
|
||
db.commit()
|
||
|
||
logger.info("管理员重置了用户 %s 的密码", user.username)
|
||
return {"message": "密码已重置"}
|