From c16f3968d54e37cbd53aec2af258488c53a761e8 Mon Sep 17 00:00:00 2001 From: chihlasm Date: Sat, 4 Apr 2026 07:43:23 +0000 Subject: [PATCH] feat: add device types CRUD router Adds GET/POST/PUT/DELETE endpoints at /device-types with team-scoped access. System types are read-only; custom types are scoped to the creating team. Co-Authored-By: Claude Opus 4.6 (1M context) --- backend/app/api/endpoints/device_types.py | 119 ++++++++++++++++++++++ backend/app/api/router.py | 42 +++++++- 2 files changed, 160 insertions(+), 1 deletion(-) create mode 100644 backend/app/api/endpoints/device_types.py diff --git a/backend/app/api/endpoints/device_types.py b/backend/app/api/endpoints/device_types.py new file mode 100644 index 00000000..7daa409c --- /dev/null +++ b/backend/app/api/endpoints/device_types.py @@ -0,0 +1,119 @@ +"""Device types API endpoints.""" +from typing import Annotated +from uuid import UUID + +from fastapi import APIRouter, Depends, HTTPException +from sqlalchemy import select, or_ +from sqlalchemy.ext.asyncio import AsyncSession + +from app.core.database import get_db +from app.api.deps import get_current_active_user +from app.models.user import User +from app.models.device_type import DeviceType +from app.schemas.device_type import ( + DeviceTypeCreate, + DeviceTypeUpdate, + DeviceTypeResponse, +) + +router = APIRouter(prefix="/device-types", tags=["device-types"]) + + +@router.get("/", response_model=list[DeviceTypeResponse]) +async def list_device_types( + db: Annotated[AsyncSession, Depends(get_db)], + current_user: Annotated[User, Depends(get_current_active_user)], +) -> list[DeviceTypeResponse]: + stmt = ( + select(DeviceType) + .where( + or_( + DeviceType.is_system.is_(True), + DeviceType.team_id == current_user.team_id, + ) + ) + .order_by(DeviceType.category, DeviceType.sort_order, DeviceType.label) + ) + result = await db.execute(stmt) + rows = result.scalars().all() + return [DeviceTypeResponse.model_validate(r) for r in rows] + + +@router.post("/", response_model=DeviceTypeResponse, status_code=201) +async def create_device_type( + data: DeviceTypeCreate, + db: Annotated[AsyncSession, Depends(get_db)], + current_user: Annotated[User, Depends(get_current_active_user)], +) -> DeviceTypeResponse: + existing = await db.execute( + select(DeviceType).where( + DeviceType.slug == data.slug, + DeviceType.team_id == current_user.team_id, + ) + ) + if existing.scalar_one_or_none(): + raise HTTPException(status_code=409, detail=f"Device type '{data.slug}' already exists for your team") + + system_existing = await db.execute( + select(DeviceType).where( + DeviceType.slug == data.slug, + DeviceType.is_system.is_(True), + ) + ) + if system_existing.scalar_one_or_none(): + raise HTTPException(status_code=409, detail=f"Device type '{data.slug}' conflicts with a system type") + + device_type = DeviceType( + slug=data.slug, + label=data.label, + category=data.category, + is_system=False, + team_id=current_user.team_id, + sort_order=data.sort_order, + ) + db.add(device_type) + await db.commit() + await db.refresh(device_type) + return DeviceTypeResponse.model_validate(device_type) + + +@router.put("/{device_type_id}", response_model=DeviceTypeResponse) +async def update_device_type( + device_type_id: UUID, + data: DeviceTypeUpdate, + db: Annotated[AsyncSession, Depends(get_db)], + current_user: Annotated[User, Depends(get_current_active_user)], +) -> DeviceTypeResponse: + device_type = await db.get(DeviceType, device_type_id) + if not 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: + raise HTTPException(status_code=404, detail="Device type not found") + + update_data = data.model_dump(exclude_unset=True) + for field, value in update_data.items(): + setattr(device_type, field, value) + + await db.commit() + await db.refresh(device_type) + return DeviceTypeResponse.model_validate(device_type) + + +@router.delete("/{device_type_id}", status_code=204) +async def delete_device_type( + device_type_id: UUID, + db: Annotated[AsyncSession, Depends(get_db)], + current_user: Annotated[User, Depends(get_current_active_user)], +) -> None: + device_type = await db.get(DeviceType, device_type_id) + if not 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: + raise HTTPException(status_code=404, detail="Device type not found") + + await db.delete(device_type) + await db.commit() diff --git a/backend/app/api/router.py b/backend/app/api/router.py index ed32ba58..8cf7d7bb 100644 --- a/backend/app/api/router.py +++ b/backend/app/api/router.py @@ -1,3 +1,4 @@ +<<<<<<< HEAD from fastapi import APIRouter, Depends from app.api.deps import require_tenant_context @@ -24,6 +25,7 @@ from app.api.endpoints import ( branding, categories, copilot, + device_types, feedback, flow_proposals, flowpilot_analytics, @@ -58,6 +60,44 @@ from app.api.endpoints import ( webhooks, accounts, ) +======= +from fastapi import APIRouter +from app.api.endpoints import auth, trees, sessions, sidebar, invite, categories, tags, folders, step_categories, steps, admin, accounts, webhooks, shares, shared, tree_markdown +from app.api.endpoints import admin_dashboard, admin_audit, admin_plan_limits, admin_feature_flags, admin_settings, admin_categories +from app.api.endpoints import ratings, analytics +from app.api.endpoints import target_lists +from app.api.endpoints import maintenance_schedules +from app.api.endpoints import feedback +from app.api.endpoints import ai_builder +from app.api.endpoints import ai_fix +from app.api.endpoints import ai_chat +from app.api.endpoints import copilot +from app.api.endpoints import assistant_chat +from app.api.endpoints import survey +from app.api.endpoints import admin_survey +from app.api.endpoints import tree_transfer +from app.api.endpoints import ai_suggestions +from app.api.endpoints import kb_accelerator +from app.api.endpoints import beta_signup +from app.api.endpoints import scripts +from app.api.endpoints import integrations +from app.api.endpoints import onboarding +from app.api.endpoints import branding +from app.api.endpoints import supporting_data +from app.api.endpoints import ai_sessions +from app.api.endpoints import flow_proposals +from app.api.endpoints import flowpilot_analytics +from app.api.endpoints import notifications +from app.api.endpoints import public_templates +from app.api.endpoints import admin_gallery +from app.api.endpoints import uploads +from app.api.endpoints import script_builder +from app.api.endpoints import beta_feedback +from app.api.endpoints import session_branches +from app.api.endpoints import session_handoffs +from app.api.endpoints import session_resolutions +from app.api.endpoints import device_types +>>>>>>> a3c4987 (feat: add device types CRUD router) api_router = APIRouter() @@ -93,7 +133,6 @@ api_router.include_router(admin_settings.router) api_router.include_router(admin_categories.router) api_router.include_router(admin_survey.router) api_router.include_router(admin_gallery.router) - # --------------------------------------------------------------------------- # User-facing endpoints — tenant context required # --------------------------------------------------------------------------- @@ -142,3 +181,4 @@ api_router.include_router(script_builder.router, dependencies=_tenant_deps) api_router.include_router(beta_feedback.router, dependencies=_tenant_deps) api_router.include_router(session_branches.router, dependencies=_tenant_deps) api_router.include_router(session_handoffs.router, dependencies=_tenant_deps) +api_router.include_router(device_types.router, dependencies=_tenant_deps)