feat(13-03): add link service, schemas, router, and wire subscribers into lifespan
- LinkResponse/UnknownClientResponse Pydantic schemas with from_attributes - Link service with get_links, get_device_links, get_site_links, get_unknown_clients - Unknown clients query uses DISTINCT ON for latest registration per MAC - 4 REST endpoints: tenant links, device links, site links, unknown clients - Interface and link discovery subscribers wired into FastAPI lifespan start/stop - Links router registered at /api prefix Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
89
backend/app/routers/links.py
Normal file
89
backend/app/routers/links.py
Normal file
@@ -0,0 +1,89 @@
|
||||
"""
|
||||
Wireless link API endpoints.
|
||||
|
||||
Routes: /api/tenants/{tenant_id}/links, /api/tenants/{tenant_id}/devices/{device_id}/links,
|
||||
/api/tenants/{tenant_id}/sites/{site_id}/links,
|
||||
/api/tenants/{tenant_id}/devices/{device_id}/unknown-clients
|
||||
|
||||
RBAC:
|
||||
- viewer: GET (read-only) -- all endpoints are GET-only
|
||||
"""
|
||||
|
||||
import uuid
|
||||
from typing import Optional
|
||||
|
||||
from fastapi import APIRouter, Depends, Query
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.database import get_db
|
||||
from app.middleware.tenant_context import CurrentUser, get_current_user
|
||||
from app.routers.devices import _check_tenant_access
|
||||
from app.schemas.link import LinkListResponse, UnknownClientListResponse
|
||||
from app.services import link_service
|
||||
|
||||
router = APIRouter(tags=["links"])
|
||||
|
||||
|
||||
@router.get(
|
||||
"/tenants/{tenant_id}/links",
|
||||
response_model=LinkListResponse,
|
||||
summary="List wireless links",
|
||||
)
|
||||
async def list_links(
|
||||
tenant_id: uuid.UUID,
|
||||
state: Optional[str] = Query(None, description="Filter by link state (active, degraded, down, stale)"),
|
||||
device_id: Optional[uuid.UUID] = Query(None, description="Filter by device (AP or CPE side)"),
|
||||
current_user: CurrentUser = Depends(get_current_user),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
) -> LinkListResponse:
|
||||
"""List all wireless links for a tenant with optional state and device filters."""
|
||||
await _check_tenant_access(current_user, tenant_id, db)
|
||||
return await link_service.get_links(db=db, tenant_id=tenant_id, state=state, device_id=device_id)
|
||||
|
||||
|
||||
@router.get(
|
||||
"/tenants/{tenant_id}/devices/{device_id}/links",
|
||||
response_model=LinkListResponse,
|
||||
summary="List device links",
|
||||
)
|
||||
async def list_device_links(
|
||||
tenant_id: uuid.UUID,
|
||||
device_id: uuid.UUID,
|
||||
current_user: CurrentUser = Depends(get_current_user),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
) -> LinkListResponse:
|
||||
"""List wireless links where the given device is either the AP or CPE."""
|
||||
await _check_tenant_access(current_user, tenant_id, db)
|
||||
return await link_service.get_device_links(db=db, tenant_id=tenant_id, device_id=device_id)
|
||||
|
||||
|
||||
@router.get(
|
||||
"/tenants/{tenant_id}/sites/{site_id}/links",
|
||||
response_model=LinkListResponse,
|
||||
summary="List site links",
|
||||
)
|
||||
async def list_site_links(
|
||||
tenant_id: uuid.UUID,
|
||||
site_id: uuid.UUID,
|
||||
current_user: CurrentUser = Depends(get_current_user),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
) -> LinkListResponse:
|
||||
"""List wireless links where either the AP or CPE device belongs to the given site."""
|
||||
await _check_tenant_access(current_user, tenant_id, db)
|
||||
return await link_service.get_site_links(db=db, tenant_id=tenant_id, site_id=site_id)
|
||||
|
||||
|
||||
@router.get(
|
||||
"/tenants/{tenant_id}/devices/{device_id}/unknown-clients",
|
||||
response_model=UnknownClientListResponse,
|
||||
summary="List unknown wireless clients",
|
||||
)
|
||||
async def list_unknown_clients(
|
||||
tenant_id: uuid.UUID,
|
||||
device_id: uuid.UUID,
|
||||
current_user: CurrentUser = Depends(get_current_user),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
) -> UnknownClientListResponse:
|
||||
"""List wireless clients whose MAC does not resolve to any known device interface."""
|
||||
await _check_tenant_access(current_user, tenant_id, db)
|
||||
return await link_service.get_unknown_clients(db=db, tenant_id=tenant_id, device_id=device_id)
|
||||
Reference in New Issue
Block a user