后端: - InstanceGroup 模型 (name/db_type/permissions JSON/is_active) - user_groups 关联表 (用户-权限组 M2M + 权限覆盖) - Instance 模型增加 group_id FK - User 模型增加 groups 关系 - API: CRUD + 实例关联 + 用户分配 + 权限组过滤 - 15 个新测试 (CRUD/关联/权限检查/参数校验) — 217/217 passing 前端: - InstanceGroupPage: 左右布局(列表+详情) - 权限配置: 5类17项权限勾选 (查询/DDL/工单/数据操作/管理) - 实例关联/用户关联弹框 - 侧边栏增加权限组菜单 (数据源分组下) - 路由 + 权限注册
331 lines
10 KiB
Python
331 lines
10 KiB
Python
import json
|
|
from typing import Optional, List
|
|
|
|
from fastapi import APIRouter, Depends, HTTPException
|
|
from sqlalchemy import select, func as sa_func
|
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|
|
|
from app.core.database import get_db
|
|
from app.core.security import get_current_user, require_role
|
|
from app.models.user import User
|
|
from app.models.instance import Instance
|
|
from app.models.instance_group import InstanceGroup, user_groups
|
|
from app.schemas.instance_group import (
|
|
InstanceGroupCreate, InstanceGroupUpdate,
|
|
InstanceGroupResponse, InstanceGroupDetailResponse,
|
|
InstanceAssignRequest, UserAssignRequest, UserGroupUpdateRequest,
|
|
)
|
|
|
|
router = APIRouter(prefix="/instance-groups", tags=["权限组管理"])
|
|
|
|
|
|
def _serialize_group(group: InstanceGroup, instance_count: int = 0, user_count: int = 0) -> dict:
|
|
return {
|
|
"id": group.id,
|
|
"name": group.name,
|
|
"description": group.description,
|
|
"db_type": group.db_type,
|
|
"permissions": group.get_permissions(),
|
|
"is_active": group.is_active,
|
|
"created_at": group.created_at,
|
|
"updated_at": group.updated_at,
|
|
"instance_count": instance_count,
|
|
"user_count": user_count,
|
|
}
|
|
|
|
|
|
def _serialize_group_detail(group: InstanceGroup) -> dict:
|
|
d = _serialize_group(
|
|
group,
|
|
instance_count=len(group.instances) if group.instances else 0,
|
|
user_count=len(group.users) if group.users else 0,
|
|
)
|
|
d["instances"] = [
|
|
{
|
|
"id": inst.id,
|
|
"name": inst.name,
|
|
"db_type": inst.db_type,
|
|
"host": inst.host,
|
|
"port": inst.port,
|
|
"env": inst.env,
|
|
"status": inst.status,
|
|
}
|
|
for inst in (group.instances or [])
|
|
]
|
|
d["users"] = [
|
|
{
|
|
"id": u.id,
|
|
"username": u.username,
|
|
"display_name": u.display_name,
|
|
"role": u.role,
|
|
}
|
|
for u in (group.users or [])
|
|
]
|
|
return d
|
|
|
|
|
|
@router.get("", response_model=List[InstanceGroupResponse])
|
|
async def list_instance_groups(
|
|
db_type: Optional[str] = None,
|
|
db: AsyncSession = Depends(get_db),
|
|
current_user: User = Depends(get_current_user),
|
|
):
|
|
stmt = select(InstanceGroup).order_by(InstanceGroup.id)
|
|
if db_type:
|
|
stmt = stmt.where(InstanceGroup.db_type == db_type)
|
|
result = await db.execute(stmt)
|
|
groups = result.scalars().all()
|
|
return [_serialize_group_detail(g) for g in groups]
|
|
|
|
|
|
@router.post("", response_model=InstanceGroupResponse, status_code=201)
|
|
async def create_instance_group(
|
|
data: InstanceGroupCreate,
|
|
db: AsyncSession = Depends(get_db),
|
|
current_user: User = Depends(require_role("admin")),
|
|
):
|
|
# Check name uniqueness
|
|
existing = await db.execute(
|
|
select(InstanceGroup).where(InstanceGroup.name == data.name)
|
|
)
|
|
if existing.scalar_one_or_none():
|
|
raise HTTPException(status_code=400, detail="权限组名称已存在")
|
|
|
|
group = InstanceGroup(
|
|
name=data.name,
|
|
description=data.description,
|
|
db_type=data.db_type,
|
|
permissions=json.dumps(data.permissions, ensure_ascii=False) if data.permissions else None,
|
|
is_active=data.is_active,
|
|
)
|
|
db.add(group)
|
|
await db.commit()
|
|
await db.refresh(group)
|
|
return _serialize_group(group)
|
|
|
|
|
|
@router.get("/{group_id}", response_model=InstanceGroupDetailResponse)
|
|
async def get_instance_group(
|
|
group_id: int,
|
|
db: AsyncSession = Depends(get_db),
|
|
current_user: User = Depends(get_current_user),
|
|
):
|
|
result = await db.execute(
|
|
select(InstanceGroup).where(InstanceGroup.id == group_id)
|
|
)
|
|
group = result.scalar_one_or_none()
|
|
if not group:
|
|
raise HTTPException(status_code=404, detail="权限组不存在")
|
|
return _serialize_group_detail(group)
|
|
|
|
|
|
@router.put("/{group_id}", response_model=InstanceGroupResponse)
|
|
async def update_instance_group(
|
|
group_id: int,
|
|
data: InstanceGroupUpdate,
|
|
db: AsyncSession = Depends(get_db),
|
|
current_user: User = Depends(require_role("admin")),
|
|
):
|
|
result = await db.execute(
|
|
select(InstanceGroup).where(InstanceGroup.id == group_id)
|
|
)
|
|
group = result.scalar_one_or_none()
|
|
if not group:
|
|
raise HTTPException(status_code=404, detail="权限组不存在")
|
|
|
|
update_data = data.model_dump(exclude_unset=True)
|
|
if "name" in update_data:
|
|
# Check uniqueness
|
|
existing = await db.execute(
|
|
select(InstanceGroup).where(
|
|
InstanceGroup.name == update_data["name"],
|
|
InstanceGroup.id != group_id,
|
|
)
|
|
)
|
|
if existing.scalar_one_or_none():
|
|
raise HTTPException(status_code=400, detail="权限组名称已存在")
|
|
|
|
if "permissions" in update_data:
|
|
update_data["permissions"] = (
|
|
json.dumps(update_data["permissions"], ensure_ascii=False)
|
|
if update_data["permissions"]
|
|
else None
|
|
)
|
|
|
|
for key, value in update_data.items():
|
|
setattr(group, key, value)
|
|
await db.commit()
|
|
await db.refresh(group)
|
|
return _serialize_group(group)
|
|
|
|
|
|
@router.delete("/{group_id}")
|
|
async def delete_instance_group(
|
|
group_id: int,
|
|
db: AsyncSession = Depends(get_db),
|
|
current_user: User = Depends(require_role("admin")),
|
|
):
|
|
result = await db.execute(
|
|
select(InstanceGroup).where(InstanceGroup.id == group_id)
|
|
)
|
|
group = result.scalar_one_or_none()
|
|
if not group:
|
|
raise HTTPException(status_code=404, detail="权限组不存在")
|
|
|
|
# Detach instances
|
|
inst_result = await db.execute(
|
|
select(Instance).where(Instance.group_id == group_id)
|
|
)
|
|
for inst in inst_result.scalars().all():
|
|
inst.group_id = None
|
|
|
|
await db.delete(group)
|
|
await db.commit()
|
|
return {"message": "权限组已删除"}
|
|
|
|
|
|
@router.post("/{group_id}/instances")
|
|
async def assign_instances(
|
|
group_id: int,
|
|
data: InstanceAssignRequest,
|
|
db: AsyncSession = Depends(get_db),
|
|
current_user: User = Depends(require_role("admin")),
|
|
):
|
|
result = await db.execute(
|
|
select(InstanceGroup).where(InstanceGroup.id == group_id)
|
|
)
|
|
group = result.scalar_one_or_none()
|
|
if not group:
|
|
raise HTTPException(status_code=404, detail="权限组不存在")
|
|
|
|
for inst_id in data.instance_ids:
|
|
inst_result = await db.execute(
|
|
select(Instance).where(Instance.id == inst_id)
|
|
)
|
|
inst = inst_result.scalar_one_or_none()
|
|
if inst:
|
|
inst.group_id = group_id
|
|
await db.commit()
|
|
return {"message": "实例已关联"}
|
|
|
|
|
|
@router.delete("/{group_id}/instances/{instance_id}")
|
|
async def remove_instance(
|
|
group_id: int,
|
|
instance_id: int,
|
|
db: AsyncSession = Depends(get_db),
|
|
current_user: User = Depends(require_role("admin")),
|
|
):
|
|
inst_result = await db.execute(
|
|
select(Instance).where(Instance.id == instance_id, Instance.group_id == group_id)
|
|
)
|
|
inst = inst_result.scalar_one_or_none()
|
|
if not inst:
|
|
raise HTTPException(status_code=404, detail="实例未关联该权限组")
|
|
inst.group_id = None
|
|
await db.commit()
|
|
return {"message": "实例已取消关联"}
|
|
|
|
|
|
@router.get("/{group_id}/users")
|
|
async def list_group_users(
|
|
group_id: int,
|
|
db: AsyncSession = Depends(get_db),
|
|
current_user: User = Depends(get_current_user),
|
|
):
|
|
result = await db.execute(
|
|
select(InstanceGroup).where(InstanceGroup.id == group_id)
|
|
)
|
|
group = result.scalar_one_or_none()
|
|
if not group:
|
|
raise HTTPException(status_code=404, detail="权限组不存在")
|
|
|
|
# Query user_groups for permissions override
|
|
ug_result = await db.execute(
|
|
select(user_groups).where(user_groups.c.group_id == group_id)
|
|
)
|
|
ug_rows = ug_result.all()
|
|
perm_map = {}
|
|
for row in ug_rows:
|
|
perms = json.loads(row.permissions) if row.permissions else None
|
|
perm_map[row.user_id] = perms
|
|
|
|
users_data = []
|
|
for u in (group.users or []):
|
|
users_data.append({
|
|
"id": u.id,
|
|
"username": u.username,
|
|
"display_name": u.display_name,
|
|
"role": u.role,
|
|
"permissions": perm_map.get(u.id),
|
|
})
|
|
return users_data
|
|
|
|
|
|
@router.post("/{group_id}/users")
|
|
async def assign_users(
|
|
group_id: int,
|
|
data: UserAssignRequest,
|
|
db: AsyncSession = Depends(get_db),
|
|
current_user: User = Depends(require_role("admin")),
|
|
):
|
|
result = await db.execute(
|
|
select(InstanceGroup).where(InstanceGroup.id == group_id)
|
|
)
|
|
group = result.scalar_one_or_none()
|
|
if not group:
|
|
raise HTTPException(status_code=404, detail="权限组不存在")
|
|
|
|
perm_json = json.dumps(data.permissions, ensure_ascii=False) if data.permissions else None
|
|
|
|
for user_id in data.user_ids:
|
|
# Check if already assigned
|
|
check = await db.execute(
|
|
select(user_groups).where(
|
|
user_groups.c.user_id == user_id,
|
|
user_groups.c.group_id == group_id,
|
|
)
|
|
)
|
|
if not check.first():
|
|
await db.execute(
|
|
user_groups.insert().values(
|
|
user_id=user_id,
|
|
group_id=group_id,
|
|
permissions=perm_json,
|
|
)
|
|
)
|
|
await db.commit()
|
|
return {"message": "用户已分配"}
|
|
|
|
|
|
@router.put("/users/{user_id}/groups")
|
|
async def update_user_groups(
|
|
user_id: int,
|
|
data: UserGroupUpdateRequest,
|
|
db: AsyncSession = Depends(get_db),
|
|
current_user: User = Depends(require_role("admin")),
|
|
):
|
|
user_result = await db.execute(
|
|
select(User).where(User.id == user_id)
|
|
)
|
|
user = user_result.scalar_one_or_none()
|
|
if not user:
|
|
raise HTTPException(status_code=404, detail="用户不存在")
|
|
|
|
# Remove existing group associations
|
|
await db.execute(
|
|
user_groups.delete().where(user_groups.c.user_id == user_id)
|
|
)
|
|
|
|
# Add new ones
|
|
for gid in data.group_ids:
|
|
group_result = await db.execute(
|
|
select(InstanceGroup).where(InstanceGroup.id == gid)
|
|
)
|
|
if group_result.scalar_one_or_none():
|
|
await db.execute(
|
|
user_groups.insert().values(user_id=user_id, group_id=gid)
|
|
)
|
|
await db.commit()
|
|
return {"message": "用户权限组已更新"}
|