Files
aiagent/backend/app/api/users.py
renjianbo 876789fac1 feat: multi-tenant workspace isolation, RBAC, sidebar nav, billing, and Android enhancements
- 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>
2026-07-04 01:00:22 +08:00

265 lines
8.2 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
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": "密码已重置"}