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>
This commit is contained in:
@@ -11,6 +11,7 @@ import logging
|
||||
|
||||
from app.core.database import get_db
|
||||
from app.api.auth import get_current_user
|
||||
from app.api.users import _require_admin
|
||||
from app.models.user import User
|
||||
from app.models.workspace import Workspace, WorkspaceMembership
|
||||
from app.services.workspace_service import check_workspace_access, get_user_workspaces
|
||||
@@ -391,3 +392,165 @@ def remove_member(
|
||||
db.commit()
|
||||
|
||||
return {"message": "成员已移除"}
|
||||
|
||||
|
||||
# ── 平台管理员端点 ──
|
||||
|
||||
admin_router = APIRouter(
|
||||
prefix="/api/v1/admin/workspaces",
|
||||
tags=["admin-workspaces"],
|
||||
responses={
|
||||
401: {"description": "未授权"},
|
||||
403: {"description": "仅平台管理员可访问"},
|
||||
404: {"description": "资源不存在"},
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
class AdminWorkspaceResponse(BaseModel):
|
||||
id: str
|
||||
name: str
|
||||
description: Optional[str]
|
||||
is_default: bool
|
||||
owner_id: str
|
||||
owner_name: str = ""
|
||||
max_members: int
|
||||
member_count: int = 0
|
||||
status: str
|
||||
created_at: Optional[str]
|
||||
updated_at: Optional[str]
|
||||
|
||||
|
||||
class AdminWorkspaceListResponse(BaseModel):
|
||||
items: List[AdminWorkspaceResponse]
|
||||
total: int
|
||||
page: int
|
||||
page_size: int
|
||||
|
||||
|
||||
@admin_router.get("", response_model=AdminWorkspaceListResponse)
|
||||
def admin_list_workspaces(
|
||||
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"),
|
||||
db: Session = Depends(get_db),
|
||||
_admin: User = Depends(_require_admin),
|
||||
):
|
||||
"""平台管理员查看所有工作区(分页)。"""
|
||||
q = db.query(Workspace)
|
||||
if search:
|
||||
q = q.filter(Workspace.name.ilike(f"%{search}%"))
|
||||
if status:
|
||||
q = q.filter(Workspace.status == status)
|
||||
|
||||
total = q.count()
|
||||
workspaces = q.order_by(Workspace.created_at.desc()).offset((page - 1) * page_size).limit(page_size).all()
|
||||
|
||||
# 批量查 owner 和 member_count
|
||||
owner_ids = [w.owner_id for w in workspaces]
|
||||
owners = {}
|
||||
if owner_ids:
|
||||
for u in db.query(User).filter(User.id.in_(owner_ids)).all():
|
||||
owners[u.id] = u.username
|
||||
|
||||
items = []
|
||||
for w in workspaces:
|
||||
mc = (
|
||||
db.query(WorkspaceMembership)
|
||||
.filter(WorkspaceMembership.workspace_id == w.id)
|
||||
.count()
|
||||
)
|
||||
items.append(AdminWorkspaceResponse(
|
||||
id=w.id,
|
||||
name=w.name,
|
||||
description=w.description,
|
||||
is_default=bool(w.is_default),
|
||||
owner_id=w.owner_id,
|
||||
owner_name=owners.get(w.owner_id, ""),
|
||||
max_members=w.max_members,
|
||||
member_count=mc,
|
||||
status=w.status,
|
||||
created_at=w.created_at.isoformat() if w.created_at else None,
|
||||
updated_at=w.updated_at.isoformat() if w.updated_at else None,
|
||||
))
|
||||
|
||||
return AdminWorkspaceListResponse(items=items, total=total, page=page, page_size=page_size)
|
||||
|
||||
|
||||
@admin_router.get("/{workspace_id}", response_model=AdminWorkspaceResponse)
|
||||
def admin_get_workspace(
|
||||
workspace_id: str,
|
||||
db: Session = Depends(get_db),
|
||||
_admin: User = Depends(_require_admin),
|
||||
):
|
||||
"""平台管理员查看任意工作区详情。"""
|
||||
ws = db.query(Workspace).filter(Workspace.id == workspace_id).first()
|
||||
if not ws:
|
||||
raise NotFoundError("工作区", workspace_id)
|
||||
|
||||
owner = db.query(User).filter(User.id == ws.owner_id).first()
|
||||
mc = (
|
||||
db.query(WorkspaceMembership)
|
||||
.filter(WorkspaceMembership.workspace_id == workspace_id)
|
||||
.count()
|
||||
)
|
||||
|
||||
return AdminWorkspaceResponse(
|
||||
id=ws.id,
|
||||
name=ws.name,
|
||||
description=ws.description,
|
||||
is_default=bool(ws.is_default),
|
||||
owner_id=ws.owner_id,
|
||||
owner_name=owner.username if owner else "",
|
||||
max_members=ws.max_members,
|
||||
member_count=mc,
|
||||
status=ws.status,
|
||||
created_at=ws.created_at.isoformat() if ws.created_at else None,
|
||||
updated_at=ws.updated_at.isoformat() if ws.updated_at else None,
|
||||
)
|
||||
|
||||
|
||||
@admin_router.put("/{workspace_id}")
|
||||
def admin_update_workspace(
|
||||
workspace_id: str,
|
||||
data: WorkspaceUpdate,
|
||||
db: Session = Depends(get_db),
|
||||
_admin: User = Depends(_require_admin),
|
||||
):
|
||||
"""平台管理员编辑任意工作区。"""
|
||||
ws = db.query(Workspace).filter(Workspace.id == workspace_id).first()
|
||||
if not ws:
|
||||
raise NotFoundError("工作区", workspace_id)
|
||||
|
||||
if data.name is not None:
|
||||
ws.name = data.name
|
||||
if data.description is not None:
|
||||
ws.description = data.description
|
||||
if data.max_members is not None:
|
||||
ws.max_members = data.max_members
|
||||
if data.status is not None:
|
||||
ws.status = data.status
|
||||
ws.updated_at = datetime.utcnow()
|
||||
|
||||
db.commit()
|
||||
return {"message": "工作区已更新"}
|
||||
|
||||
|
||||
@admin_router.delete("/{workspace_id}")
|
||||
def admin_delete_workspace(
|
||||
workspace_id: str,
|
||||
db: Session = Depends(get_db),
|
||||
_admin: User = Depends(_require_admin),
|
||||
):
|
||||
"""平台管理员强制删除工作区(软删除)。"""
|
||||
ws = db.query(Workspace).filter(Workspace.id == workspace_id).first()
|
||||
if not ws:
|
||||
raise NotFoundError("工作区", workspace_id)
|
||||
|
||||
ws.status = "deleted"
|
||||
ws.updated_at = datetime.utcnow()
|
||||
db.commit()
|
||||
|
||||
logger.info("平台管理员删除了工作区: %s (%s)", ws.name, ws.id)
|
||||
return {"message": "工作区已删除"}
|
||||
|
||||
Reference in New Issue
Block a user