fix: align network map builder with account isolation
This commit is contained in:
@@ -15,6 +15,7 @@ from app.schemas.device_type import (
|
||||
DeviceTypeUpdate,
|
||||
DeviceTypeResponse,
|
||||
)
|
||||
from app.core.service_account import PLATFORM_ACCOUNT_ID
|
||||
|
||||
router = APIRouter(prefix="/device-types", tags=["device-types"])
|
||||
|
||||
@@ -28,8 +29,8 @@ async def list_device_types(
|
||||
select(DeviceType)
|
||||
.where(
|
||||
or_(
|
||||
DeviceType.is_system.is_(True),
|
||||
DeviceType.team_id == current_user.team_id,
|
||||
DeviceType.account_id == PLATFORM_ACCOUNT_ID,
|
||||
DeviceType.account_id == current_user.account_id,
|
||||
)
|
||||
)
|
||||
.order_by(DeviceType.category, DeviceType.sort_order, DeviceType.label)
|
||||
@@ -48,16 +49,16 @@ async def create_device_type(
|
||||
existing = await db.execute(
|
||||
select(DeviceType).where(
|
||||
DeviceType.slug == data.slug,
|
||||
DeviceType.team_id == current_user.team_id,
|
||||
DeviceType.account_id == current_user.account_id,
|
||||
)
|
||||
)
|
||||
if existing.scalar_one_or_none():
|
||||
raise HTTPException(status_code=409, detail=f"Device type '{data.slug}' already exists for your team")
|
||||
raise HTTPException(status_code=409, detail=f"Device type '{data.slug}' already exists for your account")
|
||||
|
||||
system_existing = await db.execute(
|
||||
select(DeviceType).where(
|
||||
DeviceType.slug == data.slug,
|
||||
DeviceType.is_system.is_(True),
|
||||
DeviceType.account_id == PLATFORM_ACCOUNT_ID,
|
||||
)
|
||||
)
|
||||
if system_existing.scalar_one_or_none():
|
||||
@@ -68,7 +69,7 @@ async def create_device_type(
|
||||
label=data.label,
|
||||
category=data.category,
|
||||
is_system=False,
|
||||
team_id=current_user.team_id,
|
||||
account_id=current_user.account_id,
|
||||
sort_order=data.sort_order,
|
||||
)
|
||||
db.add(device_type)
|
||||
@@ -89,7 +90,7 @@ async def update_device_type(
|
||||
raise HTTPException(status_code=404, detail="Device type not found")
|
||||
if device_type.is_system:
|
||||
raise HTTPException(status_code=403, detail="Cannot modify system device types")
|
||||
if device_type.team_id != current_user.team_id:
|
||||
if device_type.account_id != current_user.account_id:
|
||||
raise HTTPException(status_code=404, detail="Device type not found")
|
||||
|
||||
update_data = data.model_dump(exclude_unset=True)
|
||||
@@ -112,7 +113,7 @@ async def delete_device_type(
|
||||
raise HTTPException(status_code=404, detail="Device type not found")
|
||||
if device_type.is_system:
|
||||
raise HTTPException(status_code=403, detail="Cannot delete system device types")
|
||||
if device_type.team_id != current_user.team_id:
|
||||
if device_type.account_id != current_user.account_id:
|
||||
raise HTTPException(status_code=404, detail="Device type not found")
|
||||
|
||||
await db.delete(device_type)
|
||||
|
||||
@@ -13,6 +13,7 @@ from app.api.deps import get_current_active_user
|
||||
from app.models.user import User
|
||||
from app.models.device_type import DeviceType
|
||||
from app.models.network_diagram import NetworkDiagram
|
||||
from app.core.service_account import PLATFORM_ACCOUNT_ID
|
||||
from app.schemas.network_diagram import (
|
||||
NetworkDiagramCreate,
|
||||
NetworkDiagramUpdate,
|
||||
@@ -49,11 +50,11 @@ router = APIRouter(prefix="/network-diagrams", tags=["network-diagrams"])
|
||||
|
||||
async def _get_diagram_or_404(
|
||||
diagram_id: UUID,
|
||||
team_id: UUID,
|
||||
account_id: UUID,
|
||||
db: AsyncSession,
|
||||
) -> NetworkDiagram:
|
||||
diagram = await db.get(NetworkDiagram, diagram_id)
|
||||
if not diagram or diagram.team_id != team_id or diagram.is_archived:
|
||||
if not diagram or diagram.account_id != account_id or diagram.is_archived:
|
||||
raise HTTPException(status_code=404, detail="Diagram not found")
|
||||
return diagram
|
||||
|
||||
@@ -88,9 +89,12 @@ def _diagram_to_list_item(
|
||||
)
|
||||
|
||||
|
||||
async def _get_available_slugs(team_id: UUID, db: AsyncSession) -> set[str]:
|
||||
async def _get_available_slugs(account_id: UUID, db: AsyncSession) -> set[str]:
|
||||
stmt = select(DeviceType.slug).where(
|
||||
or_(DeviceType.is_system.is_(True), DeviceType.team_id == team_id)
|
||||
or_(
|
||||
DeviceType.account_id == PLATFORM_ACCOUNT_ID,
|
||||
DeviceType.account_id == account_id,
|
||||
)
|
||||
)
|
||||
result = await db.execute(stmt)
|
||||
return {row[0] for row in result.all()}
|
||||
@@ -104,7 +108,7 @@ async def list_client_names(
|
||||
stmt = (
|
||||
select(NetworkDiagram.client_name)
|
||||
.where(
|
||||
NetworkDiagram.team_id == current_user.team_id,
|
||||
NetworkDiagram.account_id == current_user.account_id,
|
||||
NetworkDiagram.is_archived.is_(False),
|
||||
NetworkDiagram.client_name.isnot(None),
|
||||
NetworkDiagram.client_name != "",
|
||||
@@ -126,7 +130,7 @@ async def list_diagrams(
|
||||
stmt = (
|
||||
select(NetworkDiagram)
|
||||
.where(
|
||||
NetworkDiagram.team_id == current_user.team_id,
|
||||
NetworkDiagram.account_id == current_user.account_id,
|
||||
NetworkDiagram.is_archived.is_(False),
|
||||
)
|
||||
.order_by(NetworkDiagram.updated_at.desc())
|
||||
@@ -148,7 +152,7 @@ async def list_diagrams(
|
||||
# Single query for custom device types so category_counts is accurate
|
||||
dt_stmt = select(DeviceType.slug, DeviceType.category).where(
|
||||
DeviceType.is_system.is_(False),
|
||||
DeviceType.team_id == current_user.team_id,
|
||||
DeviceType.account_id == current_user.account_id,
|
||||
)
|
||||
dt_result = await db.execute(dt_stmt)
|
||||
custom_slug_category = {row[0]: row[1] for row in dt_result.all()}
|
||||
@@ -164,13 +168,8 @@ async def create_diagram(
|
||||
db: Annotated[AsyncSession, Depends(get_db)],
|
||||
current_user: Annotated[User, Depends(get_current_active_user)],
|
||||
) -> NetworkDiagramResponse:
|
||||
if current_user.team_id is None:
|
||||
raise HTTPException(
|
||||
status_code=422,
|
||||
detail="Network Diagrams require a team account. Assign your account to a team first.",
|
||||
)
|
||||
diagram = NetworkDiagram(
|
||||
team_id=current_user.team_id,
|
||||
account_id=current_user.account_id,
|
||||
name=data.name,
|
||||
client_name=data.client_name,
|
||||
asset_name=data.asset_name,
|
||||
@@ -191,7 +190,7 @@ async def get_diagram(
|
||||
db: Annotated[AsyncSession, Depends(get_db)],
|
||||
current_user: Annotated[User, Depends(get_current_active_user)],
|
||||
) -> NetworkDiagramResponse:
|
||||
diagram = await _get_diagram_or_404(diagram_id, current_user.team_id, db)
|
||||
diagram = await _get_diagram_or_404(diagram_id, current_user.account_id, db)
|
||||
return _diagram_to_response(diagram)
|
||||
|
||||
|
||||
@@ -202,7 +201,7 @@ async def update_diagram(
|
||||
db: Annotated[AsyncSession, Depends(get_db)],
|
||||
current_user: Annotated[User, Depends(get_current_active_user)],
|
||||
) -> NetworkDiagramResponse:
|
||||
diagram = await _get_diagram_or_404(diagram_id, current_user.team_id, db)
|
||||
diagram = await _get_diagram_or_404(diagram_id, current_user.account_id, db)
|
||||
|
||||
update_data = data.model_dump(exclude_unset=True)
|
||||
if "nodes" in update_data and update_data["nodes"] is not None:
|
||||
@@ -225,7 +224,7 @@ async def archive_diagram(
|
||||
db: Annotated[AsyncSession, Depends(get_db)],
|
||||
current_user: Annotated[User, Depends(get_current_active_user)],
|
||||
) -> None:
|
||||
diagram = await _get_diagram_or_404(diagram_id, current_user.team_id, db)
|
||||
diagram = await _get_diagram_or_404(diagram_id, current_user.account_id, db)
|
||||
diagram.is_archived = True
|
||||
diagram.updated_at = datetime.now(timezone.utc)
|
||||
await db.commit()
|
||||
@@ -237,9 +236,9 @@ async def duplicate_diagram(
|
||||
db: Annotated[AsyncSession, Depends(get_db)],
|
||||
current_user: Annotated[User, Depends(get_current_active_user)],
|
||||
) -> NetworkDiagramResponse:
|
||||
source = await _get_diagram_or_404(diagram_id, current_user.team_id, db)
|
||||
source = await _get_diagram_or_404(diagram_id, current_user.account_id, db)
|
||||
copy = NetworkDiagram(
|
||||
team_id=current_user.team_id,
|
||||
account_id=current_user.account_id,
|
||||
name=f"Copy of {source.name}",
|
||||
client_name=source.client_name,
|
||||
asset_name=source.asset_name,
|
||||
@@ -260,7 +259,7 @@ async def export_diagram(
|
||||
db: Annotated[AsyncSession, Depends(get_db)],
|
||||
current_user: Annotated[User, Depends(get_current_active_user)],
|
||||
) -> DiagramExportResponse:
|
||||
diagram = await _get_diagram_or_404(diagram_id, current_user.team_id, db)
|
||||
diagram = await _get_diagram_or_404(diagram_id, current_user.account_id, db)
|
||||
nodes = [DiagramNode(**n) for n in (diagram.nodes or [])]
|
||||
edges = [DiagramEdge(**e) for e in (diagram.edges or [])]
|
||||
return DiagramExportResponse(
|
||||
@@ -280,7 +279,7 @@ async def import_diagram(
|
||||
db: Annotated[AsyncSession, Depends(get_db)],
|
||||
current_user: Annotated[User, Depends(get_current_active_user)],
|
||||
) -> DiagramImportResponse:
|
||||
available_slugs = await _get_available_slugs(current_user.team_id, db)
|
||||
available_slugs = await _get_available_slugs(current_user.account_id, db)
|
||||
|
||||
warnings: list[str] = []
|
||||
for node in data.nodes:
|
||||
@@ -288,7 +287,7 @@ async def import_diagram(
|
||||
warnings.append(f"Unknown device type '{node.type}' — will render with default icon")
|
||||
|
||||
diagram = NetworkDiagram(
|
||||
team_id=current_user.team_id,
|
||||
account_id=current_user.account_id,
|
||||
name=data.name,
|
||||
client_name=data.client_name,
|
||||
description=data.description,
|
||||
@@ -312,7 +311,7 @@ async def ai_generate_diagram(
|
||||
db: Annotated[AsyncSession, Depends(get_db)],
|
||||
current_user: Annotated[User, Depends(get_current_active_user)],
|
||||
) -> AIGenerateResponse:
|
||||
available_slugs_set = await _get_available_slugs(current_user.team_id, db)
|
||||
available_slugs_set = await _get_available_slugs(current_user.account_id, db)
|
||||
available_slugs = list(available_slugs_set)
|
||||
|
||||
existing_node_ids: list[str] | None = None
|
||||
|
||||
Reference in New Issue
Block a user