mirror of
https://github.com/alexjustesen/speedtest-tracker.git
synced 2026-06-23 04:20:08 +00:00
[Chore] Consolidate Results, Speedtest & Stats API Endpoints into Dedicated Controllers (#2225)
Co-authored-by: Alex Justesen <alexjustesen@users.noreply.github.com>
This commit is contained in:
@@ -2,6 +2,7 @@
|
||||
|
||||
namespace App\Actions;
|
||||
|
||||
use Illuminate\Support\Arr;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Lorisleiva\Actions\Concerns\AsAction;
|
||||
@@ -11,7 +12,22 @@ class GetOoklaSpeedtestServers
|
||||
{
|
||||
use AsAction;
|
||||
|
||||
/**
|
||||
* For UI: return the ID, Sponsor, and Name to start a manual test
|
||||
*/
|
||||
public function handle(): array
|
||||
{
|
||||
return collect(self::fetch())->mapWithKeys(function (array $item) {
|
||||
return [
|
||||
$item['id'] => ($item['sponsor'] ?? 'Unknown').' ('.($item['name'] ?? 'Unknown').', '.$item['id'].')',
|
||||
];
|
||||
})->toArray();
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch the raw Ookla server array from the Ookla API.
|
||||
*/
|
||||
public static function fetch(): array
|
||||
{
|
||||
$query = [
|
||||
'engine' => 'js',
|
||||
@@ -23,6 +39,8 @@ class GetOoklaSpeedtestServers
|
||||
$response = Http::retry(3, 250)
|
||||
->timeout(5)
|
||||
->get(url: 'https://www.speedtest.net/api/js/servers', query: $query);
|
||||
|
||||
return $response->json();
|
||||
} catch (Throwable $e) {
|
||||
Log::error('Unable to retrieve Ookla servers.', [$e->getMessage()]);
|
||||
|
||||
@@ -30,10 +48,28 @@ class GetOoklaSpeedtestServers
|
||||
'⚠️ Unable to retrieve Ookla servers, check internet connection and see logs.',
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
return $response->collect()->mapWithKeys(function (array $item, int $key) {
|
||||
/**
|
||||
* For API: return array of structured server objects
|
||||
*/
|
||||
public static function forApi(): array
|
||||
{
|
||||
$servers = self::fetch();
|
||||
|
||||
// If the first item is not an array, treat as error or empty
|
||||
if (empty($servers) || ! is_array($servers) || (isset($servers[0]) && ! is_array($servers[0]))) {
|
||||
// Optionally, you could return an error message here, but to match the controller's behavior, return an empty array
|
||||
return [];
|
||||
}
|
||||
|
||||
return collect($servers)->map(function (array $item) {
|
||||
return [
|
||||
$item['id'] => $item['sponsor'].' ('.$item['name'].', '.$item['id'].')',
|
||||
'id' => $item['id'],
|
||||
'host' => Arr::get($item, 'host', 'Unknown'),
|
||||
'name' => Arr::get($item, 'sponsor', 'Unknown'),
|
||||
'location' => Arr::get($item, 'name', 'Unknown'),
|
||||
'country' => Arr::get($item, 'country', 'Unknown'),
|
||||
];
|
||||
})->toArray();
|
||||
}
|
||||
|
||||
@@ -53,9 +53,9 @@ class ApiTokenResource extends Resource
|
||||
->required()
|
||||
->bulkToggleable()
|
||||
->descriptions([
|
||||
'results:read' => 'Allow this token to read results.',
|
||||
'speedtests:run' => 'Allow this token to run speedtests.',
|
||||
'ookla:list-servers' => 'Allow this token to list servers.',
|
||||
'results:read' => 'Grant this token permission to read results and statistics.',
|
||||
'speedtests:run' => 'Grant this token permission to run speedtests.',
|
||||
'ookla:list-servers' => 'Grant this token permission to list available servers.',
|
||||
]),
|
||||
DateTimePicker::make('expires_at')
|
||||
->label('Expires at')
|
||||
|
||||
@@ -1,52 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api\V1;
|
||||
|
||||
use App\Http\Resources\V1\ResultResource;
|
||||
use App\Models\Result;
|
||||
use Http\Discovery\Exception\NotFoundException;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\Response;
|
||||
use OpenApi\Attributes as OA;
|
||||
|
||||
class LatestResult extends ApiController
|
||||
{
|
||||
#[OA\Get(
|
||||
path: '/api/v1/results/latest',
|
||||
summary: 'Fetch the single most recent result',
|
||||
operationId: 'getLatestResult',
|
||||
tags: ['Results'],
|
||||
responses: [
|
||||
new OA\Response(
|
||||
response: Response::HTTP_OK,
|
||||
description: 'OK',
|
||||
content: new OA\JsonContent(ref: '#/components/schemas/Result')
|
||||
),
|
||||
new OA\Response(
|
||||
response: Response::HTTP_UNAUTHORIZED,
|
||||
description: 'Unauthenticated',
|
||||
content: new OA\JsonContent(ref: '#/components/schemas/UnauthenticatedError')
|
||||
),
|
||||
new OA\Response(
|
||||
response: Response::HTTP_NOT_FOUND,
|
||||
description: 'No result found',
|
||||
content: new OA\JsonContent(ref: '#/components/schemas/NotFoundError')
|
||||
),
|
||||
]
|
||||
)]
|
||||
public function __invoke(Request $request)
|
||||
{
|
||||
$result = Result::query()
|
||||
->latest()
|
||||
->firstOr(function () {
|
||||
self::throw(
|
||||
e: new NotFoundException('No result found.'),
|
||||
code: 404,
|
||||
);
|
||||
});
|
||||
|
||||
return self::sendResponse(
|
||||
data: new ResultResource($result),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,86 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api\V1;
|
||||
|
||||
use App\Http\Resources\V1\ResultResource;
|
||||
use App\Models\Result;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\Response;
|
||||
use Illuminate\Support\Facades\Validator;
|
||||
use OpenApi\Attributes as OA;
|
||||
use Spatie\QueryBuilder\AllowedFilter;
|
||||
use Spatie\QueryBuilder\Enums\FilterOperator;
|
||||
use Spatie\QueryBuilder\QueryBuilder;
|
||||
|
||||
class ListResults extends ApiController
|
||||
{
|
||||
#[OA\Get(
|
||||
path: '/api/v1/results',
|
||||
summary: 'List results',
|
||||
operationId: 'listResults',
|
||||
tags: ['Results'],
|
||||
responses: [
|
||||
new OA\Response(
|
||||
response: Response::HTTP_OK,
|
||||
description: 'OK',
|
||||
content: new OA\JsonContent(
|
||||
ref: '#/components/schemas/ResultsCollection'
|
||||
)
|
||||
),
|
||||
new OA\Response(
|
||||
response: Response::HTTP_UNAUTHORIZED,
|
||||
description: 'Unauthenticated',
|
||||
content: new OA\JsonContent(ref: '#/components/schemas/UnauthenticatedError')
|
||||
),
|
||||
new OA\Response(
|
||||
response: Response::HTTP_UNPROCESSABLE_ENTITY,
|
||||
description: 'Validation failed',
|
||||
content: new OA\JsonContent(ref: '#/components/schemas/ValidationError')
|
||||
),
|
||||
]
|
||||
)]
|
||||
public function __invoke(Request $request)
|
||||
{
|
||||
$validator = Validator::make($request->all(), [
|
||||
'per_page' => 'integer|min:1|max:500',
|
||||
]);
|
||||
|
||||
if ($validator->fails()) {
|
||||
return ApiController::sendResponse(
|
||||
data: $validator->errors(),
|
||||
message: 'Validation failed.',
|
||||
code: 422,
|
||||
);
|
||||
}
|
||||
|
||||
$results = QueryBuilder::for(Result::class)
|
||||
->allowedFilters([
|
||||
AllowedFilter::operator('ping', FilterOperator::DYNAMIC),
|
||||
AllowedFilter::operator('download', FilterOperator::DYNAMIC),
|
||||
AllowedFilter::operator('upload', FilterOperator::DYNAMIC),
|
||||
AllowedFilter::exact('healthy')->nullable(),
|
||||
AllowedFilter::exact('status'),
|
||||
AllowedFilter::exact('scheduled'),
|
||||
AllowedFilter::operator(
|
||||
name: 'start_at',
|
||||
internalName: 'created_at',
|
||||
filterOperator: FilterOperator::DYNAMIC,
|
||||
),
|
||||
AllowedFilter::operator(
|
||||
name: 'end_at',
|
||||
internalName: 'created_at',
|
||||
filterOperator: FilterOperator::DYNAMIC,
|
||||
),
|
||||
])
|
||||
->allowedSorts([
|
||||
'ping',
|
||||
'download',
|
||||
'upload',
|
||||
'created_at',
|
||||
'updated_at',
|
||||
])
|
||||
->jsonPaginate($request->input('per_page', 25));
|
||||
|
||||
return ResultResource::collection($results);
|
||||
}
|
||||
}
|
||||
@@ -1,57 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api\V1;
|
||||
|
||||
use App\Actions\GetOoklaSpeedtestServers;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\Response;
|
||||
use OpenApi\Attributes as OA;
|
||||
|
||||
class ListSpeedtestServers extends ApiController
|
||||
{
|
||||
#[OA\Get(
|
||||
path: '/api/v1/ookla/list-servers',
|
||||
summary: 'List available Ookla speedtest servers',
|
||||
operationId: 'listSpeedtestServers',
|
||||
tags: ['Servers'],
|
||||
responses: [
|
||||
new OA\Response(
|
||||
response: Response::HTTP_OK,
|
||||
description: 'OK',
|
||||
content: new OA\JsonContent(
|
||||
ref: '#/components/schemas/ServersCollection'
|
||||
)
|
||||
),
|
||||
new OA\Response(
|
||||
response: Response::HTTP_UNAUTHORIZED,
|
||||
description: 'Unauthenticated',
|
||||
content: new OA\JsonContent(ref: '#/components/schemas/UnauthenticatedError')
|
||||
),
|
||||
new OA\Response(
|
||||
response: Response::HTTP_FORBIDDEN,
|
||||
description: 'Forbidden',
|
||||
content: new OA\JsonContent(
|
||||
ref: '#/components/schemas/ForbiddenError',
|
||||
example: ['message' => 'You do not have permission to view speedtest servers.']
|
||||
)
|
||||
),
|
||||
]
|
||||
)]
|
||||
public function __invoke(Request $request)
|
||||
{
|
||||
if ($request->user()->tokenCant('ookla:list-servers')) {
|
||||
return self::sendResponse(
|
||||
data: null,
|
||||
message: 'You do not have permission to view speedtest servers.',
|
||||
code: Response::HTTP_FORBIDDEN,
|
||||
);
|
||||
}
|
||||
|
||||
$servers = GetOoklaSpeedtestServers::run();
|
||||
|
||||
return self::sendResponse(
|
||||
data: $servers,
|
||||
message: 'Speedtest servers fetched successfully.'
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api\V1;
|
||||
|
||||
use App\Actions\GetOoklaSpeedtestServers;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\Response;
|
||||
|
||||
class OoklaController extends ApiController
|
||||
{
|
||||
/**
|
||||
* GET /api/v1/ookla/list-servers
|
||||
* List available Ookla speedtest servers.
|
||||
*/
|
||||
public function __invoke(Request $request)
|
||||
{
|
||||
if ($request->user()->tokenCant('ookla:list-servers')) {
|
||||
return $this->sendResponse(
|
||||
data: null,
|
||||
message: 'You do not have permission to view speedtest servers.',
|
||||
code: Response::HTTP_FORBIDDEN,
|
||||
);
|
||||
}
|
||||
|
||||
$servers = GetOoklaSpeedtestServers::forApi();
|
||||
|
||||
return $this->sendResponse(
|
||||
data: $servers,
|
||||
message: 'Speedtest servers fetched successfully.'
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,118 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api\V1;
|
||||
|
||||
use App\Http\Resources\V1\ResultResource;
|
||||
use App\Models\Result;
|
||||
use Http\Discovery\Exception\NotFoundException;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\Response;
|
||||
use Illuminate\Support\Facades\Validator;
|
||||
use Spatie\QueryBuilder\AllowedFilter;
|
||||
use Spatie\QueryBuilder\Enums\FilterOperator;
|
||||
use Spatie\QueryBuilder\QueryBuilder;
|
||||
|
||||
class ResultsController extends ApiController
|
||||
{
|
||||
/**
|
||||
* GET /results
|
||||
* List or filter results with optional pagination.
|
||||
*/
|
||||
public function list(Request $request)
|
||||
{
|
||||
if ($request->user()->tokenCant('results:read')) {
|
||||
return $this->sendResponse(
|
||||
data: null,
|
||||
message: 'You do not have permission to view results.',
|
||||
code: Response::HTTP_FORBIDDEN
|
||||
);
|
||||
}
|
||||
$validator = Validator::make($request->all(), [
|
||||
'per_page' => 'integer|min:1|max:500',
|
||||
]);
|
||||
|
||||
if ($validator->fails()) {
|
||||
return $this->sendResponse(
|
||||
data: $validator->errors(),
|
||||
message: 'Validation failed.',
|
||||
code: 422
|
||||
);
|
||||
}
|
||||
|
||||
$results = QueryBuilder::for(Result::class)
|
||||
->allowedFilters([
|
||||
AllowedFilter::operator('ping', FilterOperator::DYNAMIC),
|
||||
AllowedFilter::operator('download', FilterOperator::DYNAMIC),
|
||||
AllowedFilter::operator('upload', FilterOperator::DYNAMIC),
|
||||
AllowedFilter::exact('healthy')->nullable(),
|
||||
AllowedFilter::exact('status'),
|
||||
AllowedFilter::exact('scheduled'),
|
||||
AllowedFilter::operator(
|
||||
name: 'start_at',
|
||||
internalName: 'created_at',
|
||||
filterOperator: FilterOperator::DYNAMIC,
|
||||
),
|
||||
AllowedFilter::operator(
|
||||
name: 'end_at',
|
||||
internalName: 'created_at',
|
||||
filterOperator: FilterOperator::DYNAMIC,
|
||||
),
|
||||
])
|
||||
->allowedSorts([
|
||||
'ping',
|
||||
'download',
|
||||
'upload',
|
||||
'created_at',
|
||||
'updated_at',
|
||||
])
|
||||
->jsonPaginate($request->input('per_page', 25));
|
||||
|
||||
return ResultResource::collection($results);
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /results/{id}
|
||||
* Fetch a single result by ID.
|
||||
*/
|
||||
public function show(Request $request, int $id)
|
||||
{
|
||||
if ($request->user()->tokenCant('results:read')) {
|
||||
return $this->sendResponse(
|
||||
data: null,
|
||||
message: 'You do not have permission to view results.',
|
||||
code: Response::HTTP_FORBIDDEN
|
||||
);
|
||||
}
|
||||
$result = Result::findOr($id, function () {
|
||||
self::throw(
|
||||
e: new NotFoundException('Result not found.'),
|
||||
code: 404
|
||||
);
|
||||
});
|
||||
|
||||
return $this->sendResponse(
|
||||
data: new ResultResource($result)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /results/latest
|
||||
* Fetch the single most recent result.
|
||||
*/
|
||||
public function latest(Request $request)
|
||||
{
|
||||
if ($request->user()->tokenCant('results:read')) {
|
||||
return $this->sendResponse(
|
||||
data: null,
|
||||
message: 'You do not have permission to view results.',
|
||||
code: Response::HTTP_FORBIDDEN
|
||||
);
|
||||
}
|
||||
$result = Result::latest()
|
||||
->firstOrFail();
|
||||
|
||||
return $this->sendResponse(
|
||||
data: new ResultResource($result)
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,59 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api\V1;
|
||||
|
||||
use App\Http\Resources\V1\ResultResource;
|
||||
use App\Models\Result;
|
||||
use Http\Discovery\Exception\NotFoundException;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\Response;
|
||||
use OpenApi\Attributes as OA;
|
||||
|
||||
class ShowResult extends ApiController
|
||||
{
|
||||
#[OA\Get(
|
||||
path: '/api/v1/results/{id}',
|
||||
summary: 'Fetch a single result by ID',
|
||||
operationId: 'getResult',
|
||||
tags: ['Results'],
|
||||
parameters: [
|
||||
new OA\Parameter(
|
||||
name: 'id',
|
||||
in: 'path',
|
||||
required: true,
|
||||
schema: new OA\Schema(type: 'integer'),
|
||||
description: 'The ID of the result'
|
||||
),
|
||||
],
|
||||
responses: [
|
||||
new OA\Response(
|
||||
response: Response::HTTP_OK,
|
||||
description: 'OK',
|
||||
content: new OA\JsonContent(ref: '#/components/schemas/ResultResponse')
|
||||
),
|
||||
new OA\Response(
|
||||
response: Response::HTTP_UNAUTHORIZED,
|
||||
description: 'Unauthenticated',
|
||||
content: new OA\JsonContent(ref: '#/components/schemas/UnauthenticatedError')
|
||||
),
|
||||
new OA\Response(
|
||||
response: Response::HTTP_NOT_FOUND,
|
||||
description: 'Result not found',
|
||||
content: new OA\JsonContent(ref: '#/components/schemas/NotFoundError')
|
||||
),
|
||||
]
|
||||
)]
|
||||
public function __invoke(Request $request, int $id)
|
||||
{
|
||||
$result = Result::findOr($id, function () {
|
||||
self::throw(
|
||||
e: new NotFoundException('Result not found.'),
|
||||
code: 404,
|
||||
);
|
||||
});
|
||||
|
||||
return self::sendResponse(
|
||||
data: new ResultResource($result),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api\V1;
|
||||
|
||||
use App\Actions\Ookla\RunSpeedtest as RunSpeedtestAction;
|
||||
use App\Http\Resources\V1\ResultResource;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\Response;
|
||||
use Illuminate\Support\Facades\Validator;
|
||||
|
||||
class SpeedtestController extends ApiController
|
||||
{
|
||||
/**
|
||||
* POST /api/v1/speedtests/run
|
||||
* Run a new Ookla speedtest.
|
||||
*/
|
||||
public function __invoke(Request $request)
|
||||
{
|
||||
if ($request->user()->tokenCant('speedtests:run')) {
|
||||
return $this->sendResponse(
|
||||
data: null,
|
||||
message: 'You do not have permission to run speedtests.',
|
||||
code: Response::HTTP_FORBIDDEN,
|
||||
);
|
||||
}
|
||||
|
||||
$validator = Validator::make($request->all(), [
|
||||
'server_id' => 'sometimes|integer',
|
||||
]);
|
||||
|
||||
if ($validator->fails()) {
|
||||
return $this->sendResponse(
|
||||
data: $validator->errors(),
|
||||
message: 'Validation failed.',
|
||||
code: Response::HTTP_UNPROCESSABLE_ENTITY,
|
||||
);
|
||||
}
|
||||
|
||||
$result = RunSpeedtestAction::run(
|
||||
serverId: $request->input('server_id'),
|
||||
);
|
||||
|
||||
return $this->sendResponse(
|
||||
data: new ResultResource($result),
|
||||
message: 'Speedtest added to the queue.',
|
||||
code: Response::HTTP_CREATED,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,83 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api\V1;
|
||||
|
||||
use App\Http\Resources\V1\StatResource;
|
||||
use App\Models\Result;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\Response;
|
||||
use OpenApi\Attributes as OA;
|
||||
use OpenApi\Attributes\Schema as OASchema;
|
||||
use Spatie\QueryBuilder\AllowedFilter;
|
||||
use Spatie\QueryBuilder\Enums\FilterOperator;
|
||||
use Spatie\QueryBuilder\QueryBuilder;
|
||||
|
||||
class Stats extends ApiController
|
||||
{
|
||||
/**
|
||||
* Handle the incoming request.
|
||||
*/
|
||||
#[OA\Get(
|
||||
path: '/api/v1/stats',
|
||||
summary: 'Fetch aggregated Speedtest statistics',
|
||||
operationId: 'getStats',
|
||||
tags: ['Stats'],
|
||||
parameters: [
|
||||
new OA\Parameter(
|
||||
name: 'start_at',
|
||||
in: 'query',
|
||||
description: 'ISO‑8601 start datetime filter',
|
||||
required: false,
|
||||
schema: new OASchema(type: 'string', format: 'date-time')
|
||||
),
|
||||
new OA\Parameter(
|
||||
name: 'end_at',
|
||||
in: 'query',
|
||||
description: 'ISO‑8601 end datetime filter',
|
||||
required: false,
|
||||
schema: new OASchema(type: 'string', format: 'date-time')
|
||||
),
|
||||
],
|
||||
responses: [
|
||||
new OA\Response(
|
||||
response: Response::HTTP_OK,
|
||||
description: 'OK',
|
||||
content: new OA\JsonContent(ref: '#/components/schemas/Stats')
|
||||
),
|
||||
new OA\Response(
|
||||
response: Response::HTTP_UNAUTHORIZED,
|
||||
description: 'Unauthenticated',
|
||||
content: new OA\JsonContent(ref: '#/components/schemas/UnauthenticatedError')
|
||||
),
|
||||
new OA\Response(
|
||||
response: Response::HTTP_UNPROCESSABLE_ENTITY,
|
||||
description: 'Validation failed',
|
||||
content: new OA\JsonContent(ref: '#/components/schemas/ValidationError')
|
||||
),
|
||||
]
|
||||
)]
|
||||
public function __invoke(Request $request)
|
||||
{
|
||||
$stats = QueryBuilder::for(Result::class)
|
||||
->selectRaw('count(*) as total_results')
|
||||
->selectRaw('avg(ping) as avg_ping')
|
||||
->selectRaw('avg(download) as avg_download')
|
||||
->selectRaw('avg(upload) as avg_upload')
|
||||
->selectRaw('min(ping) as min_ping')
|
||||
->selectRaw('min(download) as min_download')
|
||||
->selectRaw('min(upload) as min_upload')
|
||||
->selectRaw('max(ping) as max_ping')
|
||||
->selectRaw('max(download) as max_download')
|
||||
->selectRaw('max(upload) as max_upload')
|
||||
->AllowedFilters([
|
||||
AllowedFilter::operator(name: 'start_at', internalName: 'created_at', filterOperator: FilterOperator::DYNAMIC),
|
||||
AllowedFilter::operator(name: 'end_at', internalName: 'created_at', filterOperator: FilterOperator::DYNAMIC),
|
||||
])
|
||||
->first();
|
||||
|
||||
return self::sendResponse(
|
||||
data: new StatResource($stats),
|
||||
filters: $request->input('filter'),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api\V1;
|
||||
|
||||
use App\Http\Resources\V1\StatResource;
|
||||
use App\Models\Result;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\Response;
|
||||
use Spatie\QueryBuilder\AllowedFilter;
|
||||
use Spatie\QueryBuilder\Enums\FilterOperator;
|
||||
use Spatie\QueryBuilder\QueryBuilder;
|
||||
|
||||
class StatsController extends ApiController
|
||||
{
|
||||
/**
|
||||
* GET /api/v1/stats
|
||||
* Fetch aggregated Speedtest statistics with optional start/end filters.
|
||||
*/
|
||||
public function __invoke(Request $request)
|
||||
{
|
||||
if ($request->user()->tokenCant('results:read')) {
|
||||
return $this->sendResponse(
|
||||
data: null,
|
||||
message: 'You do not have permission to view statistics.',
|
||||
code: Response::HTTP_FORBIDDEN
|
||||
);
|
||||
}
|
||||
|
||||
// Build the stats query
|
||||
$stats = QueryBuilder::for(Result::class)
|
||||
->selectRaw('count(*) as total_results')
|
||||
->selectRaw('avg(ping) as avg_ping')
|
||||
->selectRaw('avg(download) as avg_download')
|
||||
->selectRaw('avg(upload) as avg_upload')
|
||||
->selectRaw('min(ping) as min_ping')
|
||||
->selectRaw('min(download) as min_download')
|
||||
->selectRaw('min(upload) as min_upload')
|
||||
->selectRaw('max(ping) as max_ping')
|
||||
->selectRaw('max(download) as max_download')
|
||||
->selectRaw('max(upload) as max_upload')
|
||||
->allowedFilters([
|
||||
AllowedFilter::operator(name: 'start_at', internalName: 'created_at', filterOperator: FilterOperator::DYNAMIC),
|
||||
AllowedFilter::operator(name: 'end_at', internalName: 'created_at', filterOperator: FilterOperator::DYNAMIC),
|
||||
])
|
||||
->first();
|
||||
|
||||
// Return wrapped in a resource
|
||||
return $this->sendResponse(
|
||||
data: new StatResource($stats)
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -13,6 +13,7 @@ use Filament\Forms\Concerns\InteractsWithForms;
|
||||
use Filament\Forms\Contracts\HasForms;
|
||||
use Filament\Notifications\Notification;
|
||||
use Filament\Support\Enums\IconPosition;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Livewire\Component;
|
||||
|
||||
class RunSpeedtestAction extends Component implements HasActions, HasForms
|
||||
@@ -68,7 +69,7 @@ class RunSpeedtestAction extends Component implements HasActions, HasForms
|
||||
->label('Speedtest')
|
||||
->icon('heroicon-o-rocket-launch')
|
||||
->iconPosition(IconPosition::Before)
|
||||
->hidden(! auth()->user()->is_admin)
|
||||
->hidden(! Auth::check() && Auth::user()->is_admin)
|
||||
->extraAttributes([
|
||||
'id' => 'speedtestAction',
|
||||
]);
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
<?php
|
||||
|
||||
namespace App\OpenApi\Annotations\V1;
|
||||
|
||||
use Illuminate\Http\Response;
|
||||
use OpenApi\Attributes as OA;
|
||||
|
||||
#[OA\PathItem(
|
||||
path: '/api/v1/ookla',
|
||||
description: 'Endpoints for retrieving Ookla speedtest servers and related resources.'
|
||||
)]
|
||||
class OoklaAnnotations
|
||||
{
|
||||
#[OA\Get(
|
||||
path: '/api/v1/ookla/list-servers',
|
||||
summary: 'List available Ookla speedtest servers',
|
||||
description: 'Returns an array of available Ookla speedtest servers. Requires an API token with `ookla:list-servers` scope.',
|
||||
operationId: 'listOoklaServers',
|
||||
tags: ['Servers'],
|
||||
responses: [
|
||||
new OA\Response(
|
||||
response: Response::HTTP_OK,
|
||||
description: 'Servers retrieved successfully',
|
||||
content: new OA\JsonContent(ref: '#/components/schemas/ServersCollection')
|
||||
),
|
||||
new OA\Response(
|
||||
response: Response::HTTP_UNAUTHORIZED,
|
||||
description: 'Unauthenticated',
|
||||
content: new OA\JsonContent(ref: '#/components/schemas/UnauthenticatedError')
|
||||
),
|
||||
new OA\Response(
|
||||
response: Response::HTTP_FORBIDDEN,
|
||||
description: 'Forbidden',
|
||||
content: new OA\JsonContent(ref: '#/components/schemas/ForbiddenError')
|
||||
),
|
||||
]
|
||||
)]
|
||||
public function listServers(): void {}
|
||||
}
|
||||
@@ -0,0 +1,112 @@
|
||||
<?php
|
||||
|
||||
namespace App\OpenApi\Annotations\V1;
|
||||
|
||||
use Illuminate\Http\Response;
|
||||
use OpenApi\Attributes as OA;
|
||||
|
||||
#[OA\PathItem(
|
||||
path: '/api/v1/results',
|
||||
description: 'Endpoints for retrieving speedtest results.'
|
||||
)]
|
||||
class ResultsAnnotations
|
||||
{
|
||||
#[OA\Get(
|
||||
path: '/api/v1/results',
|
||||
summary: 'List all results',
|
||||
operationId: 'listResults',
|
||||
tags: ['Results'],
|
||||
responses: [
|
||||
new OA\Response(
|
||||
response: Response::HTTP_OK,
|
||||
description: 'OK',
|
||||
content: new OA\JsonContent(ref: '#/components/schemas/ResultsCollection')
|
||||
),
|
||||
new OA\Response(
|
||||
response: Response::HTTP_FORBIDDEN,
|
||||
description: 'Forbidden',
|
||||
content: new OA\JsonContent(ref: '#/components/schemas/ForbiddenError')
|
||||
),
|
||||
new OA\Response(
|
||||
response: Response::HTTP_UNAUTHORIZED,
|
||||
description: 'Unauthenticated',
|
||||
content: new OA\JsonContent(ref: '#/components/schemas/UnauthenticatedError')
|
||||
),
|
||||
new OA\Response(
|
||||
response: Response::HTTP_UNPROCESSABLE_ENTITY,
|
||||
description: 'Validation failed',
|
||||
content: new OA\JsonContent(ref: '#/components/schemas/ValidationError')
|
||||
),
|
||||
]
|
||||
)]
|
||||
public function index(): void {}
|
||||
|
||||
#[OA\Get(
|
||||
path: '/api/v1/results/{id}',
|
||||
summary: 'Get a single result',
|
||||
operationId: 'getResult',
|
||||
tags: ['Results'],
|
||||
parameters: [
|
||||
new OA\Parameter(
|
||||
name: 'id',
|
||||
in: 'path',
|
||||
required: true,
|
||||
schema: new OA\Schema(type: 'integer'),
|
||||
description: 'The ID of the result'
|
||||
),
|
||||
],
|
||||
responses: [
|
||||
new OA\Response(
|
||||
response: Response::HTTP_OK,
|
||||
description: 'OK',
|
||||
content: new OA\JsonContent(ref: '#/components/schemas/ResultResponse')
|
||||
),
|
||||
new OA\Response(
|
||||
response: Response::HTTP_FORBIDDEN,
|
||||
description: 'Forbidden',
|
||||
content: new OA\JsonContent(ref: '#/components/schemas/ForbiddenError')
|
||||
),
|
||||
new OA\Response(
|
||||
response: Response::HTTP_UNAUTHORIZED,
|
||||
description: 'Unauthenticated',
|
||||
content: new OA\JsonContent(ref: '#/components/schemas/UnauthenticatedError')
|
||||
),
|
||||
new OA\Response(
|
||||
response: Response::HTTP_NOT_FOUND,
|
||||
description: 'Result not found',
|
||||
content: new OA\JsonContent(ref: '#/components/schemas/NotFoundError')
|
||||
),
|
||||
]
|
||||
)]
|
||||
public function show(): void {}
|
||||
|
||||
#[OA\Get(
|
||||
path: '/api/v1/results/latest',
|
||||
summary: 'Get the most recent result',
|
||||
operationId: 'getLatestResult',
|
||||
tags: ['Results'],
|
||||
responses: [
|
||||
new OA\Response(
|
||||
response: Response::HTTP_OK,
|
||||
description: 'OK',
|
||||
content: new OA\JsonContent(ref: '#/components/schemas/Result')
|
||||
),
|
||||
new OA\Response(
|
||||
response: Response::HTTP_FORBIDDEN,
|
||||
description: 'Forbidden',
|
||||
content: new OA\JsonContent(ref: '#/components/schemas/ForbiddenError')
|
||||
),
|
||||
new OA\Response(
|
||||
response: Response::HTTP_UNAUTHORIZED,
|
||||
description: 'Unauthenticated',
|
||||
content: new OA\JsonContent(ref: '#/components/schemas/UnauthenticatedError')
|
||||
),
|
||||
new OA\Response(
|
||||
response: Response::HTTP_NOT_FOUND,
|
||||
description: 'No result found',
|
||||
content: new OA\JsonContent(ref: '#/components/schemas/NotFoundError')
|
||||
),
|
||||
]
|
||||
)]
|
||||
public function latest(): void {}
|
||||
}
|
||||
+36
-36
@@ -1,15 +1,15 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api\V1;
|
||||
namespace App\OpenApi\Annotations\V1;
|
||||
|
||||
use App\Actions\Ookla\RunSpeedtest as RunSpeedtestAction;
|
||||
use App\Http\Resources\V1\ResultResource;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\Response;
|
||||
use Illuminate\Support\Facades\Validator;
|
||||
use OpenApi\Attributes as OA;
|
||||
|
||||
class RunSpeedtest extends ApiController
|
||||
#[OA\Tag(
|
||||
name: 'Speedtests',
|
||||
description: 'Endpoints for running speedtests and listing servers.'
|
||||
)]
|
||||
class SpeedtestAnnotations
|
||||
{
|
||||
#[OA\Post(
|
||||
path: '/api/v1/speedtests/run',
|
||||
@@ -48,36 +48,36 @@ class RunSpeedtest extends ApiController
|
||||
),
|
||||
]
|
||||
)]
|
||||
public function __invoke(Request $request)
|
||||
public function run(): void
|
||||
{
|
||||
if ($request->user()->tokenCant('speedtests:run')) {
|
||||
return self::sendResponse(
|
||||
data: null,
|
||||
message: 'You do not have permission to run speedtests.',
|
||||
code: Response::HTTP_FORBIDDEN,
|
||||
);
|
||||
}
|
||||
|
||||
$validator = Validator::make($request->all(), [
|
||||
'server_id' => 'sometimes|integer',
|
||||
]);
|
||||
|
||||
if ($validator->fails()) {
|
||||
return ApiController::sendResponse(
|
||||
data: $validator->errors(),
|
||||
message: 'Validation failed.',
|
||||
code: Response::HTTP_UNPROCESSABLE_ENTITY,
|
||||
);
|
||||
}
|
||||
|
||||
$result = RunSpeedtestAction::run(
|
||||
serverId: $request->input('server_id'),
|
||||
);
|
||||
|
||||
return self::sendResponse(
|
||||
data: new ResultResource($result),
|
||||
message: 'Speedtest added to the queue.',
|
||||
code: Response::HTTP_CREATED,
|
||||
);
|
||||
// Annotation placeholder for runSpeedtest
|
||||
}
|
||||
|
||||
#[OA\Get(
|
||||
path: '/api/v1/speedtests/list-servers',
|
||||
summary: 'List available Ookla speedtest servers',
|
||||
operationId: 'listSpeedtestServers',
|
||||
tags: ['Speedtests'],
|
||||
responses: [
|
||||
new OA\Response(
|
||||
response: Response::HTTP_OK,
|
||||
description: 'OK',
|
||||
content: new OA\JsonContent(ref: '#/components/schemas/ServersCollection')
|
||||
),
|
||||
new OA\Response(
|
||||
response: Response::HTTP_UNAUTHORIZED,
|
||||
description: 'Unauthenticated',
|
||||
content: new OA\JsonContent(ref: '#/components/schemas/UnauthenticatedError')
|
||||
),
|
||||
new OA\Response(
|
||||
response: Response::HTTP_FORBIDDEN,
|
||||
description: 'Forbidden',
|
||||
content: new OA\JsonContent(
|
||||
ref: '#/components/schemas/ForbiddenError',
|
||||
example: ['message' => 'You do not have permission to view speedtest servers.']
|
||||
)
|
||||
),
|
||||
]
|
||||
)]
|
||||
public function listServers(): void {}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
<?php
|
||||
|
||||
namespace App\OpenApi\Annotations\V1;
|
||||
|
||||
use Illuminate\Http\Response;
|
||||
use OpenApi\Attributes as OA;
|
||||
|
||||
#[OA\PathItem(
|
||||
path: '/api/v1/stats',
|
||||
description: 'Endpoints for viewing performance statistics.'
|
||||
)]
|
||||
class StatsAnnotations
|
||||
{
|
||||
#[OA\Get(
|
||||
path: '/api/v1/stats',
|
||||
summary: 'Fetch aggregated Speedtest statistics',
|
||||
operationId: 'getStats',
|
||||
tags: ['Stats'],
|
||||
parameters: [
|
||||
new OA\Parameter(
|
||||
name: 'start_at',
|
||||
in: 'query',
|
||||
description: 'Filter stats from this date/time (ISO 8601)',
|
||||
required: false,
|
||||
schema: new OA\Schema(type: 'string', format: 'date-time')
|
||||
),
|
||||
new OA\Parameter(
|
||||
name: 'end_at',
|
||||
in: 'query',
|
||||
description: 'Filter stats up to this date/time (ISO 8601)',
|
||||
required: false,
|
||||
schema: new OA\Schema(type: 'string', format: 'date-time')
|
||||
),
|
||||
],
|
||||
responses: [
|
||||
new OA\Response(
|
||||
response: Response::HTTP_OK,
|
||||
description: 'Statistics fetched successfully',
|
||||
content: new OA\JsonContent(ref: '#/components/schemas/Stats')
|
||||
),
|
||||
new OA\Response(
|
||||
response: Response::HTTP_UNAUTHORIZED,
|
||||
description: 'Unauthenticated',
|
||||
content: new OA\JsonContent(ref: '#/components/schemas/UnauthenticatedError')
|
||||
),
|
||||
new OA\Response(
|
||||
response: Response::HTTP_FORBIDDEN,
|
||||
description: 'Forbidden',
|
||||
content: new OA\JsonContent(ref: '#/components/schemas/ForbiddenError')
|
||||
),
|
||||
new OA\Response(
|
||||
response: Response::HTTP_UNPROCESSABLE_ENTITY,
|
||||
description: 'Validation error',
|
||||
content: new OA\JsonContent(ref: '#/components/schemas/ValidationError')
|
||||
),
|
||||
]
|
||||
)]
|
||||
public function getStats(): void {}
|
||||
}
|
||||
@@ -29,10 +29,18 @@ use OpenApi\Attributes as OA;
|
||||
]
|
||||
),
|
||||
tags: [
|
||||
new OA\Tag(name: 'Results', description: "Operations related to Speedtest results.\nRequires an API token with scope `results:read`."),
|
||||
new OA\Tag(name: 'Speedtests', description: "Operations to run speedtests.\nRequires an API token with scope `speedtests:run`."),
|
||||
new OA\Tag(name: 'Servers', description: "Operations for speedtest servers.\nRequires an API token with scope `ookla:list-servers`."),
|
||||
new OA\Tag(name: 'Stats', description: "Operations for statistics.\nRequires an API token with scope `results:read`."),
|
||||
new OA\Tag(
|
||||
name: 'Results',
|
||||
description: 'Endpoints for accessing and filtering speedtest results. Requires API token with `results:read` scope.'
|
||||
),
|
||||
new OA\Tag(
|
||||
name: 'Speedtests',
|
||||
description: 'Endpoints for initiating speedtests and retrieving available servers. Requires `speedtests:run` or `speedtests:read` token scopes.'
|
||||
),
|
||||
new OA\Tag(
|
||||
name: 'Stats',
|
||||
description: 'Endpoints for retrieving aggregated statistics and performance metrics. Requires `speedtests:read` token scope.'
|
||||
),
|
||||
]
|
||||
)]
|
||||
class OpenApiDefinition {}
|
||||
|
||||
@@ -7,17 +7,22 @@ use OpenApi\Attributes as OA;
|
||||
#[OA\Schema(
|
||||
schema: 'ServersCollection',
|
||||
type: 'object',
|
||||
description: 'Mapping of server IDs to display names',
|
||||
description: 'Collection of Ookla speedtest servers',
|
||||
properties: [
|
||||
new OA\Property(
|
||||
property: 'data',
|
||||
type: 'object',
|
||||
description: 'Map of server ID to display name',
|
||||
example: [
|
||||
'data' => [
|
||||
'12345' => 'Fibernet (New York, 12345)',
|
||||
],
|
||||
],
|
||||
type: 'array',
|
||||
description: 'List of server objects',
|
||||
items: new OA\Items(
|
||||
type: 'object',
|
||||
properties: [
|
||||
new OA\Property(property: 'id', type: 'string'),
|
||||
new OA\Property(property: 'host', type: 'string'),
|
||||
new OA\Property(property: 'name', type: 'string'),
|
||||
new OA\Property(property: 'location', type: 'string'),
|
||||
new OA\Property(property: 'country', type: 'string'),
|
||||
]
|
||||
)
|
||||
),
|
||||
new OA\Property(
|
||||
property: 'message',
|
||||
|
||||
+244
-143
@@ -5,53 +5,16 @@
|
||||
"version": "1.0.0"
|
||||
},
|
||||
"paths": {
|
||||
"/api/v1/results/latest": {
|
||||
"get": {
|
||||
"tags": [
|
||||
"Results"
|
||||
],
|
||||
"summary": "Fetch the single most recent result",
|
||||
"operationId": "getLatestResult",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/Result"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"401": {
|
||||
"description": "Unauthenticated",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/UnauthenticatedError"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"404": {
|
||||
"description": "No result found",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/NotFoundError"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
"/api/v1/ookla": {
|
||||
"description": "Endpoints for retrieving Ookla speedtest servers and related resources."
|
||||
},
|
||||
"/api/v1/results": {
|
||||
"description": "Endpoints for retrieving speedtest results.",
|
||||
"get": {
|
||||
"tags": [
|
||||
"Results"
|
||||
],
|
||||
"summary": "List results",
|
||||
"summary": "List all results",
|
||||
"operationId": "listResults",
|
||||
"responses": {
|
||||
"200": {
|
||||
@@ -64,6 +27,16 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"403": {
|
||||
"description": "Forbidden",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ForbiddenError"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"401": {
|
||||
"description": "Unauthenticated",
|
||||
"content": {
|
||||
@@ -87,16 +60,91 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/v1/stats": {
|
||||
"description": "Endpoints for viewing performance statistics.",
|
||||
"get": {
|
||||
"tags": [
|
||||
"Stats"
|
||||
],
|
||||
"summary": "Fetch aggregated Speedtest statistics",
|
||||
"operationId": "getStats",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "start_at",
|
||||
"in": "query",
|
||||
"description": "Filter stats from this date/time (ISO 8601)",
|
||||
"required": false,
|
||||
"schema": {
|
||||
"type": "string",
|
||||
"format": "date-time"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "end_at",
|
||||
"in": "query",
|
||||
"description": "Filter stats up to this date/time (ISO 8601)",
|
||||
"required": false,
|
||||
"schema": {
|
||||
"type": "string",
|
||||
"format": "date-time"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Statistics fetched successfully",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/Stats"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"401": {
|
||||
"description": "Unauthenticated",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/UnauthenticatedError"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"403": {
|
||||
"description": "Forbidden",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ForbiddenError"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"422": {
|
||||
"description": "Validation error",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ValidationError"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/v1/ookla/list-servers": {
|
||||
"get": {
|
||||
"tags": [
|
||||
"Servers"
|
||||
],
|
||||
"summary": "List available Ookla speedtest servers",
|
||||
"operationId": "listSpeedtestServers",
|
||||
"description": "Returns an array of available Ookla speedtest servers. Requires an API token with `ookla:list-servers` scope.",
|
||||
"operationId": "listOoklaServers",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"description": "Servers retrieved successfully",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
@@ -121,9 +169,119 @@
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ForbiddenError"
|
||||
},
|
||||
"example": {
|
||||
"message": "You do not have permission to view speedtest servers."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/v1/results/{id}": {
|
||||
"get": {
|
||||
"tags": [
|
||||
"Results"
|
||||
],
|
||||
"summary": "Get a single result",
|
||||
"operationId": "getResult",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "id",
|
||||
"in": "path",
|
||||
"description": "The ID of the result",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "integer"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ResultResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"403": {
|
||||
"description": "Forbidden",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ForbiddenError"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"401": {
|
||||
"description": "Unauthenticated",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/UnauthenticatedError"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"404": {
|
||||
"description": "Result not found",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/NotFoundError"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/v1/results/latest": {
|
||||
"get": {
|
||||
"tags": [
|
||||
"Results"
|
||||
],
|
||||
"summary": "Get the most recent result",
|
||||
"operationId": "getLatestResult",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/Result"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"403": {
|
||||
"description": "Forbidden",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ForbiddenError"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"401": {
|
||||
"description": "Unauthenticated",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/UnauthenticatedError"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"404": {
|
||||
"description": "No result found",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/NotFoundError"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -193,31 +351,20 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/v1/results/{id}": {
|
||||
"/api/v1/speedtests/list-servers": {
|
||||
"get": {
|
||||
"tags": [
|
||||
"Results"
|
||||
],
|
||||
"summary": "Fetch a single result by ID",
|
||||
"operationId": "getResult",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "id",
|
||||
"in": "path",
|
||||
"description": "The ID of the result",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "integer"
|
||||
}
|
||||
}
|
||||
"Speedtests"
|
||||
],
|
||||
"summary": "List available Ookla speedtest servers",
|
||||
"operationId": "listSpeedtestServers",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ResultResponse"
|
||||
"$ref": "#/components/schemas/ServersCollection"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -232,76 +379,15 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"404": {
|
||||
"description": "Result not found",
|
||||
"403": {
|
||||
"description": "Forbidden",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/NotFoundError"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/v1/stats": {
|
||||
"get": {
|
||||
"tags": [
|
||||
"Stats"
|
||||
],
|
||||
"summary": "Fetch aggregated Speedtest statistics",
|
||||
"description": "Handle the incoming request.",
|
||||
"operationId": "getStats",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "start_at",
|
||||
"in": "query",
|
||||
"description": "ISO‑8601 start datetime filter",
|
||||
"required": false,
|
||||
"schema": {
|
||||
"type": "string",
|
||||
"format": "date-time"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "end_at",
|
||||
"in": "query",
|
||||
"description": "ISO‑8601 end datetime filter",
|
||||
"required": false,
|
||||
"schema": {
|
||||
"type": "string",
|
||||
"format": "date-time"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/Stats"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"401": {
|
||||
"description": "Unauthenticated",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/UnauthenticatedError"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"422": {
|
||||
"description": "Validation failed",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ValidationError"
|
||||
"$ref": "#/components/schemas/ForbiddenError"
|
||||
},
|
||||
"example": {
|
||||
"message": "You do not have permission to view speedtest servers."
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -664,15 +750,30 @@
|
||||
"additionalProperties": false
|
||||
},
|
||||
"ServersCollection": {
|
||||
"description": "Mapping of server IDs to display names",
|
||||
"description": "Collection of Ookla speedtest servers",
|
||||
"properties": {
|
||||
"data": {
|
||||
"description": "Map of server ID to display name",
|
||||
"type": "object",
|
||||
"example": {
|
||||
"data": {
|
||||
"12345": "Fibernet (New York, 12345)"
|
||||
}
|
||||
"description": "List of server objects",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"host": {
|
||||
"type": "string"
|
||||
},
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"location": {
|
||||
"type": "string"
|
||||
},
|
||||
"country": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
}
|
||||
},
|
||||
"message": {
|
||||
@@ -850,19 +951,19 @@
|
||||
"tags": [
|
||||
{
|
||||
"name": "Results",
|
||||
"description": "Operations related to Speedtest results.\nRequires an API token with scope `results:read`."
|
||||
"description": "Endpoints for accessing and filtering speedtest results. Requires API token with `results:read` scope."
|
||||
},
|
||||
{
|
||||
"name": "Speedtests",
|
||||
"description": "Operations to run speedtests.\nRequires an API token with scope `speedtests:run`."
|
||||
},
|
||||
{
|
||||
"name": "Servers",
|
||||
"description": "Operations for speedtest servers.\nRequires an API token with scope `ookla:list-servers`."
|
||||
"description": "Endpoints for running speedtests and listing servers."
|
||||
},
|
||||
{
|
||||
"name": "Stats",
|
||||
"description": "Operations for statistics.\nRequires an API token with scope `results:read`."
|
||||
"description": "Endpoints for retrieving aggregated statistics and performance metrics. Requires `speedtests:read` token scope."
|
||||
},
|
||||
{
|
||||
"name": "Servers",
|
||||
"description": "Servers"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -5,8 +5,6 @@ use Illuminate\Support\Facades\Route;
|
||||
|
||||
/**
|
||||
* Health check route to ensure the API is up and running.
|
||||
*
|
||||
* @deprecated
|
||||
*/
|
||||
Route::get('/healthcheck', function () {
|
||||
return response()->json([
|
||||
|
||||
+11
-13
@@ -1,29 +1,27 @@
|
||||
<?php
|
||||
|
||||
use App\Http\Controllers\Api\V1\LatestResult;
|
||||
use App\Http\Controllers\Api\V1\ListResults;
|
||||
use App\Http\Controllers\Api\V1\ListSpeedtestServers;
|
||||
use App\Http\Controllers\Api\V1\RunSpeedtest;
|
||||
use App\Http\Controllers\Api\V1\ShowResult;
|
||||
use App\Http\Controllers\Api\V1\Stats;
|
||||
use App\Http\Controllers\Api\V1\OoklaController;
|
||||
use App\Http\Controllers\Api\V1\ResultsController;
|
||||
use App\Http\Controllers\Api\V1\SpeedtestController;
|
||||
use App\Http\Controllers\Api\V1\StatsController;
|
||||
use Illuminate\Support\Facades\Route;
|
||||
|
||||
Route::prefix('v1')->name('api.v1.')->group(function () {
|
||||
Route::get('/results', ListResults::class)
|
||||
Route::get('/results', [ResultsController::class, 'list'])
|
||||
->name('results.list');
|
||||
|
||||
Route::get('/results/latest', LatestResult::class)
|
||||
Route::get('/results/latest', [ResultsController::class, 'latest'])
|
||||
->name('results.latest');
|
||||
|
||||
Route::get('/results/{result}', ShowResult::class)
|
||||
Route::get('/results/{id}', [ResultsController::class, 'show'])
|
||||
->name('results.show');
|
||||
|
||||
Route::post('/speedtests/run', RunSpeedtest::class)
|
||||
Route::post('/speedtests/run', SpeedtestController::class)
|
||||
->name('speedtests.run');
|
||||
|
||||
Route::get('/ookla/list-servers', ListSpeedtestServers::class)
|
||||
Route::get('/ookla/list-servers', OoklaController::class)
|
||||
->name('ookla.list-servers');
|
||||
|
||||
Route::get('/stats', Stats::class)
|
||||
->name('stats');
|
||||
Route::get('/stats', StatsController::class)
|
||||
->name('stats.aggregated');
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user