Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions config/packages/api_platform.yaml
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
api_platform:
title: 'ITKsites Detection API'
description: 'REST API for ingesting server detection results from the ITK sites server harvester. Detection results are processed asynchronously to track servers, sites, domains, packages, modules, Docker images, and git repositories.'
version: '1.0.0'

mapping:
paths: ['%kernel.project_dir%/src/Entity']
formats:
Expand Down
40 changes: 28 additions & 12 deletions public/api-spec-v1.json
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
{
"openapi": "3.1.0",
"info": {
"title": "",
"description": "",
"version": "0.0.0"
"title": "ITKsites Detection API",
"description": "REST API for ingesting server detection results from the ITK sites server harvester. Detection results are processed asynchronously to track servers, sites, domains, packages, modules, Docker images, and git repositories.",
"version": "1.0.0"
},
"servers": [
{
"url": "/",
"url": "https://itksites.local.itkdev.dk",
"description": ""
}
],
Expand All @@ -20,11 +20,11 @@
],
"responses": {
"202": {
"description": "DetectionResult resource created",
"description": "Detection result accepted for processing",
"links": {}
},
"400": {
"description": "Invalid input",
"description": "Invalid input \u2014 malformed request body",
"content": {
"application/ld+json": {
"schema": {
Expand All @@ -44,8 +44,14 @@
},
"links": {}
},
"401": {
"description": "Unauthorized \u2014 missing or invalid API key. The Authorization header must use the format: Apikey {key}"
},
"403": {
"description": "Forbidden \u2014 the authenticated server does not have the required ROLE_SERVER role"
},
"422": {
"description": "An error occurred",
"description": "Validation error \u2014 one or more fields failed constraint validation",
"content": {
"application/ld+json": {
"schema": {
Expand All @@ -66,8 +72,8 @@
"links": {}
}
},
"summary": "Creates a DetectionResult resource.",
"description": "Creates a DetectionResult resource.",
"summary": "Submit a detection result for async processing",
"description": "Accepts a detection result from the server harvester and queues it for asynchronous processing. The result is deduplicated by content hash \u2014 identical submissions update the last contact timestamp without triggering reprocessing. Returns 202 Accepted with an empty body.",
"parameters": [],
"requestBody": {
"description": "The new DetectionResult resource",
Expand Down Expand Up @@ -228,15 +234,25 @@
"type": "object",
"properties": {
"type": {
"default": "",
"type": "string"
"enum": [
"dir",
"docker",
"drupal",
"git",
"nginx",
"symfony"
]
},
"rootDir": {
"description": "Absolute path to the root directory of the detected installation on the server",
"default": "",
"example": "/data/www/example-site/htdocs",
"type": "string"
},
"data": {
"description": "JSON-encoded payload from the server harvester containing the detection details. Structure varies by type.",
"default": "",
"example": "{\"packages\":{\"symfony/framework-bundle\":{\"version\":\"7.2.1\"}}}",
"type": "string"
}
}
Expand Down Expand Up @@ -347,7 +363,7 @@
"securitySchemes": {
"apiKey": {
"type": "apiKey",
"description": "Value for the Authorization header parameter.",
"description": "Server API key. Use the format: Apikey {your-api-key}",
"name": "Authorization",
"in": "header"
}
Expand Down
37 changes: 25 additions & 12 deletions public/api-spec-v1.yaml
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
openapi: 3.1.0
info:
title: ''
description: ''
version: 0.0.0
title: 'ITKsites Detection API'
description: 'REST API for ingesting server detection results from the ITK sites server harvester. Detection results are processed asynchronously to track servers, sites, domains, packages, modules, Docker images, and git repositories.'
version: 1.0.0
servers:
-
url: /
url: 'https://itksites.local.itkdev.dk'
description: ''
paths:
/api/detection_results:
Expand All @@ -15,10 +15,10 @@
- DetectionResult
responses:
'202':
description: 'DetectionResult resource created'
description: 'Detection result accepted for processing'
links: { }
'400':
description: 'Invalid input'
description: 'Invalid input — malformed request body'
content:
application/ld+json:
schema:
Expand All @@ -30,8 +30,12 @@
schema:
$ref: '#/components/schemas/Error'
links: { }
'401':
description: 'Unauthorized — missing or invalid API key. The Authorization header must use the format: Apikey {key}'
'403':
description: 'Forbidden — the authenticated server does not have the required ROLE_SERVER role'
'422':
description: 'An error occurred'
description: 'Validation error — one or more fields failed constraint validation'
content:
application/ld+json:
schema:
Expand All @@ -43,8 +47,8 @@
schema:
$ref: '#/components/schemas/ConstraintViolation'
links: { }
summary: 'Creates a DetectionResult resource.'
description: 'Creates a DetectionResult resource.'
summary: 'Submit a detection result for async processing'
description: 'Accepts a detection result from the server harvester and queues it for asynchronous processing. The result is deduplicated by content hash — identical submissions update the last contact timestamp without triggering reprocessing. Returns 202 Accepted with an empty body.'
parameters: []
requestBody:
description: 'The new DetectionResult resource'
Expand Down Expand Up @@ -159,13 +163,22 @@
type: object
properties:
type:
default: ''
type: string
enum:

Check failure on line 166 in public/api-spec-v1.yaml

View workflow job for this annotation

GitHub Actions / Detect breaking changes in API specification

request-property-became-enum

in API POST /api/detection_results request property 'type' was restricted to a list of enum values (media type: application/ld+json)

Check failure on line 166 in public/api-spec-v1.yaml

View workflow job for this annotation

GitHub Actions / Detect breaking changes in API specification

request-property-became-enum

in API POST /api/detection_results request property 'type' was restricted to a list of enum values (media type: application/json)
- dir
- docker
- drupal
- git
- nginx
- symfony
rootDir:
description: 'Absolute path to the root directory of the detected installation on the server'
default: ''
example: /data/www/example-site/htdocs
type: string
data:
description: 'JSON-encoded payload from the server harvester containing the detection details. Structure varies by type.'
default: ''
example: '{"packages":{"symfony/framework-bundle":{"version":"7.2.1"}}}'
type: string
Error:
type: object
Expand Down Expand Up @@ -246,7 +259,7 @@
securitySchemes:
apiKey:
type: apiKey
description: 'Value for the Authorization header parameter.'
description: 'Server API key. Use the format: Apikey {your-api-key}'
name: Authorization
in: header
security:
Expand Down
43 changes: 42 additions & 1 deletion src/Entity/DetectionResult.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,44 @@

namespace App\Entity;

use ApiPlatform\Metadata\ApiProperty;
use ApiPlatform\Metadata\ApiResource;
use ApiPlatform\Metadata\Post;
use ApiPlatform\OpenApi\Model;
use App\Repository\DetectionResultRepository;
use App\Types\DetectionType;
use App\Utils\RootDirNormalizer;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Serializer\Attribute\Groups;

#[ApiResource(
operations: [
new Post(status: 202, output: false, messenger: true),
new Post(
status: 202,
output: false,
messenger: true,
openapi: new Model\Operation(
summary: 'Submit a detection result for async processing',
description: 'Accepts a detection result from the server harvester and queues it for asynchronous processing. The result is deduplicated by content hash — identical submissions update the last contact timestamp without triggering reprocessing. Returns 202 Accepted with an empty body.',
responses: [
'202' => new Model\Response(
description: 'Detection result accepted for processing',
),
'400' => new Model\Response(
description: 'Invalid input — malformed request body',
),
'401' => new Model\Response(
description: 'Unauthorized — missing or invalid API key. The Authorization header must use the format: Apikey {key}',
),
'403' => new Model\Response(
description: 'Forbidden — the authenticated server does not have the required ROLE_SERVER role',
),
'422' => new Model\Response(
description: 'Validation error — one or more fields failed constraint validation',
),
],
),
),
],
denormalizationContext: ['groups' => ['write']],
)]
Expand All @@ -24,10 +52,19 @@ class DetectionResult extends AbstractBaseEntity implements \Stringable
{
#[ORM\Column(type: 'string', length: 255)]
#[Groups(['write'])]
#[ApiProperty(
description: 'The type of detection result, determines which handler processes the data',
example: DetectionType::NGINX,
schema: ['enum' => [DetectionType::DIRECTORY, DetectionType::DOCKER, DetectionType::DRUPAL, DetectionType::GIT, DetectionType::NGINX, DetectionType::SYMFONY]],
)]
private string $type = '';

#[ORM\Column(type: 'string', length: 255)]
#[Groups(['write'])]
#[ApiProperty(
description: 'Absolute path to the root directory of the detected installation on the server',
example: '/data/www/example-site/htdocs',
)]
private string $rootDir = '';

#[ORM\ManyToOne(targetEntity: Server::class, inversedBy: 'detectionResults')]
Expand All @@ -36,6 +73,10 @@ class DetectionResult extends AbstractBaseEntity implements \Stringable

#[ORM\Column(type: 'text')]
#[Groups(['write'])]
#[ApiProperty(
description: 'JSON-encoded payload from the server harvester containing the detection details. Structure varies by type.',
example: '{"packages":{"symfony/framework-bundle":{"version":"7.2.1"}}}',
)]
private string $data = '';

#[ORM\Column(type: 'string', length: 255, unique: true)]
Expand Down
43 changes: 43 additions & 0 deletions src/OpenApi/OpenApiFactory.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
<?php

declare(strict_types=1);

namespace App\OpenApi;

use ApiPlatform\OpenApi\Factory\OpenApiFactoryInterface;
use ApiPlatform\OpenApi\Model\SecurityScheme;
use ApiPlatform\OpenApi\Model\Server;
use ApiPlatform\OpenApi\OpenApi;
use Symfony\Component\DependencyInjection\Attribute\AsDecorator;
use Symfony\Component\DependencyInjection\Attribute\Autowire;

#[AsDecorator(decorates: 'api_platform.openapi.factory')]
class OpenApiFactory implements OpenApiFactoryInterface
{
public function __construct(
private OpenApiFactoryInterface $decorated,
#[Autowire('%env(default::COMPOSE_SERVER_DOMAIN)%')]
private ?string $serverDomain,
#[Autowire('%env(default::COMPOSE_DOMAIN)%')]
private ?string $fallbackDomain,
) {
}

public function __invoke(array $context = []): OpenApi
{
$openApi = $this->decorated->__invoke($context);

$domain = ($this->serverDomain ?? '') !== '' ? $this->serverDomain : $this->fallbackDomain;

if (null !== $domain && '' !== $domain) {
$openApi = $openApi->withServers([new Server('https://'.$domain)]);
}

$securitySchemes = $openApi->getComponents()->getSecuritySchemes();
if ($securitySchemes instanceof \ArrayObject && isset($securitySchemes['apiKey']) && $securitySchemes['apiKey'] instanceof SecurityScheme) {
$securitySchemes['apiKey'] = $securitySchemes['apiKey']->withDescription('Server API key. Use the format: Apikey {your-api-key}');
}

return $openApi;
}
}
Loading