feat: protect production Snipe-IT operations

This commit is contained in:
2026-08-05 12:37:12 +01:00
parent 26330b75f7
commit 8c69331118
13 changed files with 289 additions and 26 deletions

View File

@@ -2,6 +2,7 @@
namespace App\Integrations\SnipeIt;
use App\Integrations\SnipeIt\Data\AssetData;
use App\Integrations\SnipeIt\Data\HardwarePage;
use App\Integrations\SnipeIt\Exceptions\SnipeItApiException;
use App\Integrations\SnipeIt\Exceptions\SnipeItConfigurationException;
@@ -16,6 +17,8 @@ use Throwable;
final class SnipeItClient
{
public function __construct(private readonly SnipeItAssetMapper $mapper) {}
public function isConfigured(): bool
{
return filled(config('services.snipe_it.url')) && filled(config('services.snipe_it.read_token'));
@@ -98,6 +101,27 @@ final class SnipeItClient
} while ($received > 0 && $offset < $page->total);
}
public function getHardware(int $hardwareId): AssetData
{
$this->assertPositiveIdentifiers($hardwareId);
$response = $this->readRequest()->get("/hardware/{$hardwareId}");
if (! $response->successful()) {
throw new SnipeItApiException(
"O Snipe-IT respondeu com HTTP {$response->status()} ao consultar o equipamento.",
$response->status(),
);
}
$payload = $response->json();
if (! is_array($payload) || ($payload['status'] ?? null) === 'error') {
throw new SnipeItApiException('O Snipe-IT não devolveu um estado válido para o equipamento.');
}
/** @var array<string, mixed> $payload */
return $this->mapper->map($payload);
}
/** @return array<string, mixed> */
public function checkoutHardware(
int $hardwareId,
@@ -108,6 +132,13 @@ final class SnipeItClient
): array {
$this->assertPositiveIdentifiers($hardwareId, $statusId, $assignedUserId);
$current = $this->getHardware($hardwareId);
if ($current->isAssigned()) {
throw new SnipeItApiException(
"O equipamento {$current->assetTag} já está atribuído no Snipe-IT; o checkout não foi executado.",
);
}
$payload = [
'status_id' => $statusId,
'checkout_to_type' => 'user',
@@ -128,15 +159,23 @@ final class SnipeItClient
public function checkinHardware(
int $hardwareId,
int $statusId,
int $expectedAssignedUserId,
?int $locationId = null,
?string $note = null,
): array {
$this->assertPositiveIdentifiers($hardwareId, $statusId);
$this->assertPositiveIdentifiers($hardwareId, $statusId, $expectedAssignedUserId);
if ($locationId !== null && $locationId < 1) {
throw new SnipeItConfigurationException('O identificador da localização do Snipe-IT tem de ser positivo.');
}
$current = $this->getHardware($hardwareId);
if (! $current->isAssignedToUser($expectedAssignedUserId)) {
throw new SnipeItApiException(
"O equipamento {$current->assetTag} não está atribuído ao destinatário esperado; o check-in não foi executado.",
);
}
$payload = ['status_id' => $statusId];
if ($locationId !== null) {