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:
2026-07-04 01:00:22 +08:00
parent 51eafb23f3
commit 876789fac1
93 changed files with 8803 additions and 669 deletions

View File

@@ -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": "工作区已删除"}