Files
api/services/nginx/app/routes/edgeGatewaysRoute.php
T

470 lines
19 KiB
PHP

<?php
namespace routes;
use classes\authentication;
use classes\edge_gateway_install_service;
use classes\edge_gateway_manager;
use classes\edge_gateway_operation_exception;
use classes\edge_gateway_operation_service;
use classes\edge_gateway_registry_service;
use classes\edge_gateway_view_service;
use classes\response;
use Exception;
use traits\route_t;
class edgeGatewaysRoute
{
use route_t;
public function run(): void
{
$this->get('/edge-gateways', fn() => $this->handleListGateways(), [
'modules_shelly_config' => 'Manage department edge gateways for local Shelly control',
]);
$this->get('/edge-gateways/{id}', fn() => $this->handleGatewayDetail(), [
'modules_shelly_config' => 'View department edge gateway detail',
]);
$this->put('/edge-gateways/{id}', fn() => $this->handleGatewayUpdate(), [
'modules_shelly_config' => 'Update edge gateway metadata and primary assignment',
]);
$this->get('/edge-gateways/{id}/operations', fn() => $this->handleGatewayOperationsList(), [
'modules_shelly_config' => 'List edge gateway operations',
]);
$this->post('/edge-gateways/{id}/operations', fn() => $this->handleGatewayOperationCreate(), [
'modules_shelly_config' => 'Queue an edge gateway operation',
]);
$this->get('/edge-gateways/{id}/operations/{operationId}/events', fn() => $this->handleGatewayOperationEvents(), [
'modules_shelly_config' => 'List edge gateway operation events',
]);
$this->post('/edge-gateways/{id}/rotate-credentials', fn() => $this->handleGatewayCredentialRotate(), [
'modules_shelly_config' => 'Rotate edge gateway credentials',
]);
$this->post('/edge-gateways/install-token', fn() => $this->handleInstallTokenCreate(), [
'modules_shelly_config' => 'Create a one-time Raspberry Pi edge gateway installer token',
]);
$this->post('/edge-gateways/{id}/discovery', fn() => $this->handleGatewayDiscovery(), [
'modules_shelly_config' => 'Queue Shelly discovery through the local edge gateway',
]);
$this->put('/edge-gateways/{id}/bindings', fn() => $this->handleBindingsUpdate(), [
'modules_shelly_config' => 'Approve or override relay bindings for an edge gateway',
]);
$this->delete('/edge-gateways/{id}', fn() => $this->handleGatewayDelete(), [
'modules_shelly_config' => 'Delete an edge gateway registration',
]);
$this->post('/departments/{id}/gateway-cutover', fn() => $this->handleDepartmentCutover(), [
'modules_shelly_config' => 'Cut a department over from Shelly cloud to local edge gateways',
]);
$this->get('/edge-agent/install-token/verify', fn() => $this->handleInstallTokenVerify());
$this->get('/edge-agent/install.sh', fn() => $this->renderInstallScript());
$this->get('/edge-agent/artifacts/agent.php', fn() => $this->renderArtifact('agent.php'));
$this->get('/edge-agent/artifacts/lan-worker.php', fn() => $this->renderArtifact('lan-worker.php'));
$this->get('/edge-agent/artifacts/docker-compose.gateway.yml', fn() => $this->renderArtifact('docker-compose.gateway.yml'));
$this->get('/edge-agent/artifacts/Dockerfile.edge-agent', fn() => $this->renderArtifact('Dockerfile.edge-agent'));
$this->get('/edge-agent/artifacts/Dockerfile.lan-worker', fn() => $this->renderArtifact('Dockerfile.lan-worker'));
$this->get('/edge-agent/artifacts/gateway-launcher.sh', fn() => $this->renderArtifact('gateway-launcher.sh'));
$this->get('/edge-agent/artifacts/truckwash-edge-gateway-stack.service', fn() => $this->renderArtifact('truckwash-edge-gateway-stack.service'));
$this->get('/edge-agent/artifacts/truckwash-edge-agent.service', fn() => $this->renderArtifact('truckwash-edge-agent.service'));
$this->post('/edge-agent/claim', fn() => $this->handleAgentClaim());
$this->post('/edge-agent/gateways/{id}/heartbeat', fn() => $this->handleAgentHeartbeat());
$this->post('/edge-agent/gateways/{id}/operations/next', fn() => $this->handleAgentOperationNext());
$this->post('/edge-agent/gateways/{id}/operations/{operationId}/events', fn() => $this->handleAgentOperationEvent());
$this->post('/edge-agent/gateways/{id}/operations/{operationId}/complete', fn() => $this->handleAgentOperationComplete());
$this->post('/edge-agent/gateways/{id}/commands/poll', fn() => $this->handleAgentCommandPoll());
$this->post('/edge-agent/gateways/{id}/commands/{jobId}/result', fn() => $this->handleAgentCommandResult());
$this->post('/edge-agent/gateways/{id}/presence', fn() => $this->handleAgentPresence());
}
private function handleListGateways(): void
{
global /** @var response $response */ $response;
$this->requirePermission('modules_shelly_config');
$departmentId = self::isParametersSet(['department_id']) ? (int)self::getParameter('department_id') : null;
$view = trim((string)$this->fromQuery('view'));
if ($departmentId !== null && $departmentId > 0) {
$this->requireDepartmentAccess($departmentId);
}
$gateways = $this->views()->listGateways($departmentId, $view !== 'summary');
$response->add_meta('fleet_usage', $this->views()->buildFleetUsageStatistics($departmentId, $gateways));
$response->success($gateways);
}
private function handleGatewayDetail(): void
{
global /** @var response $response */ $response;
$this->requirePermission('modules_shelly_config');
$response->success($this->requireGatewayAccess((int)$this->fromRoute('id')));
}
private function handleGatewayUpdate(): void
{
global /** @var response $response */ $response;
$this->requirePermission('modules_shelly_config');
self::requireParameters(['label', 'is_primary']);
self::requireType(self::getParameter('label'), self::TYPE_STRING());
self::requireType(self::getParameter('is_primary'), self::TYPE_BOOL());
$gatewayId = (int)$this->fromRoute('id');
$this->requireGatewayAccess($gatewayId);
$response->success($this->registry()->updateGatewayMetadata($gatewayId, [
'label' => (string)self::getParameter('label'),
'is_primary' => (bool)self::getParameter('is_primary'),
], $this->actorUserId()));
}
private function handleGatewayOperationsList(): void
{
global /** @var response $response */ $response;
$this->requirePermission('modules_shelly_config');
$gatewayId = (int)$this->fromRoute('id');
$this->requireGatewayAccess($gatewayId);
$response->success($this->operations()->listOperations($gatewayId));
}
private function handleGatewayOperationCreate(): void
{
global /** @var response $response */ $response;
$this->requirePermission('modules_shelly_config');
self::requireParameters(['type', 'request']);
self::requireType(self::getParameter('type'), self::TYPE_STRING());
self::requireType(self::getParameter('request'), self::TYPE_ARRAY());
$gatewayId = (int)$this->fromRoute('id');
$this->requireGatewayAccess($gatewayId);
try {
$operation = $this->operations()->queueOperation(
$gatewayId,
(string)self::getParameter('type'),
(array)self::getParameter('request'),
$this->actorUserId()
);
$response->success([
'operation' => $operation,
'gateway' => $this->views()->getGateway($gatewayId),
], 201);
} catch (edge_gateway_operation_exception $exception) {
$response->error([
'message' => $exception->getMessage(),
'error_code' => $exception->errorCode,
], $exception->status);
}
}
private function handleGatewayOperationEvents(): void
{
global /** @var response $response */ $response;
$this->requirePermission('modules_shelly_config');
$gatewayId = (int)$this->fromRoute('id');
$operationId = (int)$this->fromRoute('operationId');
self::requireParameterIntPositive($operationId, 'operationId');
$this->requireGatewayAccess($gatewayId);
$response->success($this->operations()->listOperationEvents($gatewayId, $operationId));
}
private function handleGatewayCredentialRotate(): void
{
global /** @var response $response */ $response;
$this->requirePermission('modules_shelly_config');
$gatewayId = (int)$this->fromRoute('id');
$this->requireGatewayAccess($gatewayId);
$response->success($this->operations()->rotateCredentials($gatewayId, $this->actorUserId()));
}
private function handleInstallTokenCreate(): void
{
global /** @var response $response */ $response;
$this->requirePermission('modules_shelly_config');
self::requireParameters(['department_id']);
$departmentId = (int)self::getParameter('department_id');
self::requireParameterIntPositive($departmentId, 'department_id');
$this->requireDepartmentAccess($departmentId);
$response->success(
$this->registry()->createInstallToken(
$departmentId,
self::isParametersSet(['label']) ? (string)self::getParameter('label') : null,
$this->actorUserId()
),
201
);
}
private function handleGatewayDiscovery(): void
{
global /** @var response $response */ $response;
$this->requirePermission('modules_shelly_config');
$gatewayId = (int)$this->fromRoute('id');
$this->requireGatewayAccess($gatewayId);
$this->operations()->queueDiscoveryOperation($gatewayId, $this->actorUserId());
$response->success($this->views()->getGateway($gatewayId));
}
private function handleBindingsUpdate(): void
{
global /** @var response $response */ $response;
$this->requirePermission('modules_shelly_config');
self::requireParameters(['bindings']);
self::requireType(self::getParameter('bindings'), self::TYPE_ARRAY());
$gatewayId = (int)$this->fromRoute('id');
$this->requireGatewayAccess($gatewayId);
$this->registry()->setRelayBindings($gatewayId, (array)self::getParameter('bindings'), $this->actorUserId());
$response->success($this->views()->getGateway($gatewayId));
}
private function handleGatewayDelete(): void
{
global /** @var response $response */ $response;
$this->requirePermission('modules_shelly_config');
$gatewayId = (int)$this->fromRoute('id');
$this->requireGatewayAccess($gatewayId);
$response->success($this->registry()->deleteGateway($gatewayId, $this->actorUserId()));
}
private function handleDepartmentCutover(): void
{
global /** @var response $response */ $response;
$this->requirePermission('modules_shelly_config');
self::requireParameters(['transport_mode']);
$departmentId = (int)$this->fromRoute('id');
self::requireParameterIntPositive($departmentId, 'id');
$this->requireDepartmentAccess($departmentId);
$response->success($this->registry()->setDepartmentTransportMode($departmentId, (string)self::getParameter('transport_mode'), $this->actorUserId()));
}
private function renderInstallScript(): void
{
$token = trim((string)$this->fromQuery('token'));
if ($token === '') {
http_response_code(400);
echo 'Missing token';
exit;
}
header('Content-Type: text/x-shellscript; charset=utf-8');
echo $this->install()->buildInstallScript($token);
exit;
}
private function handleInstallTokenVerify(): void
{
global /** @var response $response */ $response;
$token = trim((string)$this->fromQuery('token'));
if ($token === '') {
$response->error('Missing token', 400);
}
$response->success($this->install()->verifyInstallToken($token));
}
private function renderArtifact(string $fileName): void
{
try {
header('Content-Type: ' . $this->install()->contentType($fileName));
echo $this->install()->readArtifact($fileName);
exit;
} catch (Exception $exception) {
http_response_code(404);
echo $exception->getMessage();
exit;
}
}
private function handleAgentClaim(): void
{
global /** @var response $response */ $response;
self::requireParameters(['token']);
$payload = self::getParametersAsArray();
$response->success(
$this->registry()->claimGateway(
(string)$payload['token'],
trim((string)($payload['hostname'] ?? gethostname() ?: 'unknown-gateway')),
isset($payload['installed_version']) ? (string)$payload['installed_version'] : null,
isset($payload['metadata']) && is_array($payload['metadata']) ? (array)$payload['metadata'] : []
),
201
);
}
private function handleAgentHeartbeat(): void
{
global /** @var response $response */ $response;
$gatewayId = (int)$this->fromRoute('id');
$payload = self::getParametersAsArray();
$response->success($this->registry()->recordHeartbeat($gatewayId, $this->requireAgentToken($payload), $payload));
}
private function handleAgentOperationNext(): void
{
global /** @var response $response */ $response;
$gatewayId = (int)$this->fromRoute('id');
$payload = self::getParametersAsArray();
try {
$response->success($this->operations()->claimNextOperation(
$gatewayId,
$this->requireAgentToken($payload),
isset($payload['wait_seconds']) ? (int)$payload['wait_seconds'] : edge_gateway_manager::COMMAND_POLL_TIMEOUT_SECONDS,
isset($payload['agent_instance_id']) ? (string)$payload['agent_instance_id'] : null
));
} catch (edge_gateway_operation_exception $exception) {
$response->error([
'message' => $exception->getMessage(),
'error_code' => $exception->errorCode,
], $exception->status);
}
}
private function handleAgentOperationEvent(): void
{
global /** @var response $response */ $response;
$gatewayId = (int)$this->fromRoute('id');
$operationId = (int)$this->fromRoute('operationId');
self::requireParameterIntPositive($operationId, 'operationId');
$payload = self::getParametersAsArray();
try {
$response->success($this->operations()->appendAgentOperationEvent(
$gatewayId,
$operationId,
$this->requireAgentToken($payload),
$payload
));
} catch (edge_gateway_operation_exception $exception) {
$response->error([
'message' => $exception->getMessage(),
'error_code' => $exception->errorCode,
], $exception->status);
}
}
private function handleAgentOperationComplete(): void
{
global /** @var response $response */ $response;
$gatewayId = (int)$this->fromRoute('id');
$operationId = (int)$this->fromRoute('operationId');
self::requireParameterIntPositive($operationId, 'operationId');
$payload = self::getParametersAsArray();
try {
$response->success($this->operations()->completeAgentOperation(
$gatewayId,
$operationId,
$this->requireAgentToken($payload),
$payload
));
} catch (edge_gateway_operation_exception $exception) {
$response->error([
'message' => $exception->getMessage(),
'error_code' => $exception->errorCode,
], $exception->status);
}
}
private function handleAgentCommandPoll(): void
{
global /** @var response $response */ $response;
$gatewayId = (int)$this->fromRoute('id');
$payload = self::getParametersAsArray();
$response->success($this->manager()->pollCommand(
$gatewayId,
$this->requireAgentToken($payload),
isset($payload['wait_seconds']) ? (int)$payload['wait_seconds'] : edge_gateway_manager::COMMAND_POLL_TIMEOUT_SECONDS
));
}
private function handleAgentCommandResult(): void
{
global /** @var response $response */ $response;
$gatewayId = (int)$this->fromRoute('id');
$jobId = (int)$this->fromRoute('jobId');
self::requireParameterIntPositive($jobId, 'jobId');
$payload = self::getParametersAsArray();
$response->success($this->manager()->submitCommandResult(
$gatewayId,
$jobId,
$this->requireAgentToken($payload),
(bool)($payload['ok'] ?? false),
isset($payload['result']) && is_array($payload['result']) ? (array)$payload['result'] : [],
isset($payload['error']) ? (string)$payload['error'] : null
));
}
private function handleAgentPresence(): void
{
global /** @var response $response */ $response;
$gatewayId = (int)$this->fromRoute('id');
$payload = self::getParametersAsArray();
$this->requireAgentToken($payload);
$response->success($this->manager()->recordBrokerPresence(
$gatewayId,
isset($payload['status']) ? (string)$payload['status'] : 'disconnected',
isset($payload['connection_id']) ? (string)$payload['connection_id'] : null,
isset($payload['reason']) ? (string)$payload['reason'] : null,
isset($payload['metadata']) && is_array($payload['metadata']) ? (array)$payload['metadata'] : []
));
}
private function requireGatewayAccess(int $gatewayId): array
{
self::requireParameterIntPositive($gatewayId, 'id');
$gateway = $this->views()->getGateway($gatewayId);
$this->requireDepartmentAccess((int)$gateway['department_id']);
return $gateway;
}
private function requireAgentToken(array $payload): string
{
global /** @var response $response */ $response;
$token = trim((string)($payload['agent_token'] ?? $this->fromRequest('agent_token')));
if ($token === '') {
$response->error('Missing edge gateway agent token', 401);
}
return $token;
}
private function actorUserId(): ?int
{
$user = (new authentication())->get_user();
return $user ? (int)$user->id : null;
}
private function views(): edge_gateway_view_service
{
return new edge_gateway_view_service();
}
private function registry(): edge_gateway_registry_service
{
return new edge_gateway_registry_service();
}
private function manager(): edge_gateway_manager
{
return new edge_gateway_manager();
}
private function operations(): edge_gateway_operation_service
{
return new edge_gateway_operation_service($this->manager());
}
private function install(): edge_gateway_install_service
{
return new edge_gateway_install_service();
}
}