feat(node): pair onboard computers with the Core fleet through UI
This commit is contained in:
@@ -0,0 +1,85 @@
|
||||
"""Operator-only fleet admission; the separate private mTLS listener is in fleet."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import ipaddress
|
||||
from typing import Annotated
|
||||
from urllib.parse import urlsplit
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, Request, Response
|
||||
from pydantic import BaseModel, ConfigDict, Field
|
||||
|
||||
from k1link.fleet.registry import FleetRegistry
|
||||
from k1link.fleet.trust import PairingError
|
||||
|
||||
|
||||
def local_operator(request: Request) -> FleetRegistry:
|
||||
try:
|
||||
peer = ipaddress.ip_address(request.client.host)
|
||||
host = urlsplit(f"http://{request.headers.get('host', '')}")
|
||||
if not peer.is_loopback or host.hostname not in ("127.0.0.1", "localhost", "::1"):
|
||||
raise ValueError
|
||||
origin = request.headers.get("origin")
|
||||
if origin and origin != f"http://{request.headers['host']}":
|
||||
raise ValueError
|
||||
if request.headers.get("sec-fetch-site") == "cross-site":
|
||||
raise ValueError
|
||||
except (ValueError, AttributeError, KeyError):
|
||||
raise HTTPException(403, "Откройте Mission Core на компьютере оператора.") from None
|
||||
registry = getattr(request.app.state, "fleet", None)
|
||||
if registry is None:
|
||||
raise HTTPException(503, "Реестр аппаратов недоступен. Повторите подключение.")
|
||||
return registry
|
||||
|
||||
|
||||
class PreviewRequest(BaseModel):
|
||||
model_config = ConfigDict(extra="forbid")
|
||||
code: str = Field(min_length=1, max_length=4096)
|
||||
|
||||
|
||||
class AddRequest(BaseModel):
|
||||
model_config = ConfigDict(extra="forbid")
|
||||
preview_id: str = Field(min_length=43, max_length=43)
|
||||
name: str = Field(min_length=1, max_length=80)
|
||||
platform: str = Field(pattern="^(ugv|uav|stationary|other)$")
|
||||
|
||||
|
||||
router = APIRouter(prefix="/api/v1/fleet", tags=["fleet"])
|
||||
|
||||
|
||||
@router.get("")
|
||||
def fleet_list(response: Response, fleet: Annotated[FleetRegistry, Depends(local_operator)]):
|
||||
response.headers["Cache-Control"] = "no-store"
|
||||
return fleet.listing()
|
||||
|
||||
|
||||
@router.post("/preview")
|
||||
def fleet_preview(
|
||||
body: PreviewRequest,
|
||||
response: Response,
|
||||
fleet: Annotated[FleetRegistry, Depends(local_operator)],
|
||||
):
|
||||
response.headers["Cache-Control"] = "no-store"
|
||||
try:
|
||||
return fleet.preview(body.code)
|
||||
except PairingError as error:
|
||||
raise HTTPException(409, str(error)) from None
|
||||
|
||||
|
||||
@router.post("")
|
||||
def fleet_add(
|
||||
body: AddRequest, response: Response, fleet: Annotated[FleetRegistry, Depends(local_operator)]
|
||||
):
|
||||
response.headers["Cache-Control"] = "no-store"
|
||||
try:
|
||||
return fleet.add(body.preview_id, body.name, body.platform)
|
||||
except PairingError as error:
|
||||
raise HTTPException(409, str(error)) from None
|
||||
|
||||
|
||||
@router.delete("/{vehicle_id}")
|
||||
def fleet_revoke(vehicle_id: str, fleet: Annotated[FleetRegistry, Depends(local_operator)]):
|
||||
try:
|
||||
return fleet.revoke(vehicle_id)
|
||||
except PairingError as error:
|
||||
raise HTTPException(404, str(error)) from None
|
||||
Reference in New Issue
Block a user