Skip to content

Commit 789198b

Browse files
authored
Run Speedtests from the API (alexjustesen#2204)
1 parent dc89746 commit 789198b

File tree

10 files changed

+202
-11
lines changed

10 files changed

+202
-11
lines changed

app/Actions/Ookla/StartSpeedtest.php

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55
use App\Enums\ResultService;
66
use App\Enums\ResultStatus;
7-
use App\Events\SpeedtestStarted;
7+
use App\Events\SpeedtestWaiting;
88
use App\Jobs\Ookla\ProcessSpeedtestBatch;
99
use App\Models\Result;
1010
use Lorisleiva\Actions\Concerns\AsAction;
@@ -13,19 +13,21 @@ class StartSpeedtest
1313
{
1414
use AsAction;
1515

16-
public function handle(bool $scheduled = false, ?int $serverId = null): void
16+
public function handle(bool $scheduled = false, ?int $serverId = null): Result
1717
{
1818
$result = Result::create([
1919
'data->server->id' => $serverId,
2020
'service' => ResultService::Ookla,
21-
'status' => ResultStatus::Started,
21+
'status' => ResultStatus::Waiting,
2222
'scheduled' => $scheduled,
2323
]);
2424

2525
ProcessSpeedtestBatch::dispatch(
2626
result: $result,
2727
);
2828

29-
SpeedtestStarted::dispatch($result);
29+
SpeedtestWaiting::dispatch($result);
30+
31+
return $result;
3032
}
3133
}

app/Enums/ResultStatus.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ enum ResultStatus: string implements HasColor, HasLabel
1515
case Running = 'running';
1616
case Started = 'started';
1717
case Skipped = 'skipped';
18+
case Waiting = 'waiting';
1819

1920
public function getColor(): ?string
2021
{
@@ -26,6 +27,7 @@ public function getColor(): ?string
2627
self::Running => 'info',
2728
self::Started => 'info',
2829
self::Skipped => 'gray',
30+
self::Waiting => 'info',
2931
};
3032
}
3133

app/Events/SpeedtestWaiting.php

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
<?php
2+
3+
namespace App\Events;
4+
5+
use App\Models\Result;
6+
use Illuminate\Broadcasting\InteractsWithSockets;
7+
use Illuminate\Foundation\Events\Dispatchable;
8+
use Illuminate\Queue\SerializesModels;
9+
10+
class SpeedtestWaiting
11+
{
12+
use Dispatchable, InteractsWithSockets, SerializesModels;
13+
14+
/**
15+
* Create a new event instance.
16+
*/
17+
public function __construct(
18+
public Result $result,
19+
) {}
20+
}

app/Filament/Pages/ApiTokens.php

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
namespace App\Filament\Pages;
44

55
use Carbon\Carbon;
6+
use Filament\Forms\Components\CheckboxList;
67
use Filament\Forms\Components\DateTimePicker;
78
use Filament\Forms\Components\TextInput;
89
use Filament\Forms\Concerns\InteractsWithForms;
@@ -73,6 +74,16 @@ public function table(Table $table): Table
7374
->maxLength('100')
7475
->required()
7576
->autocomplete(false),
77+
CheckboxList::make('abilities')
78+
->options([
79+
'results:read' => 'Read results',
80+
'speedtests:run' => 'Run speedtest',
81+
])
82+
->descriptions([
83+
'results:read' => 'Allow this token to read results.',
84+
'speedtests:run' => 'Allow this token to run speedtests.',
85+
])
86+
->bulkToggleable(),
7687
DateTimePicker::make('token_expires_at')
7788
->label('Expires at')
7889
->nullable()
@@ -81,6 +92,7 @@ public function table(Table $table): Table
8192
->action(function (array $data) {
8293
$token = Auth::user()->createToken(
8394
name: $data['token_name'],
95+
abilities: $data['abilities'],
8496
expiresAt: $data['token_expires_at'] ? Carbon::parse($data['token_expires_at']) : null,
8597
);
8698

@@ -91,11 +103,14 @@ public function table(Table $table): Table
91103
->modalWidth(MaxWidth::ExtraLarge),
92104
])
93105
->columns([
94-
TextColumn::make('id')
95-
->label('ID')
96-
->sortable(),
97106
TextColumn::make('name')
98107
->searchable(),
108+
TextColumn::make('abilities')
109+
->badge(),
110+
TextColumn::make('created_at')
111+
->alignEnd()
112+
->dateTime()
113+
->sortable(),
99114
TextColumn::make('last_used_at')
100115
->alignEnd()
101116
->dateTime()

app/Http/Controllers/Api/V1/ApiController.php

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ abstract class ApiController
1313
* Send a response.
1414
*
1515
* @param mixed $data
16+
* @param array $filters
1617
* @param string $message
1718
* @param int $code
1819
* @return \Illuminate\Http\JsonResponse
@@ -29,7 +30,10 @@ public static function sendResponse($data, $filters = [], $message = 'ok', $code
2930
$response['message'] = $message;
3031
}
3132

32-
return response()->json($response, $code);
33+
return response()->json(
34+
data: $response,
35+
status: $code,
36+
);
3337
}
3438

3539
/**
@@ -44,10 +48,15 @@ public static function throw($e, $code = 500)
4448
{
4549
Log::info($e);
4650

51+
$response = [
52+
'message' => $e->getMessage(),
53+
];
54+
4755
throw new HttpResponseException(
48-
response: response()->json([
49-
'message' => $e->getMessage(),
50-
], $code)
56+
response: response()->json(
57+
data: $response,
58+
status: $code,
59+
),
5160
);
5261
}
5362
}
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
<?php
2+
3+
namespace App\Http\Controllers\Api\V1;
4+
5+
use App\Actions\Ookla\StartSpeedtest;
6+
use App\Http\Resources\V1\ResultResource;
7+
use Illuminate\Http\Request;
8+
use Illuminate\Http\Response;
9+
use Illuminate\Support\Facades\Validator;
10+
use OpenApi\Attributes as OA;
11+
12+
class RunSpeedtest extends ApiController
13+
{
14+
#[OA\Post(
15+
path: '/api/v1/speedtests/run',
16+
description: 'Run a new Ookla speedtest. Optionally provide a server_id.',
17+
parameters: [
18+
new OA\Parameter(
19+
name: 'server_id',
20+
in: 'query',
21+
required: false,
22+
schema: new OA\Schema(type: 'integer'),
23+
description: 'Optional Ookla speedtest server ID'
24+
),
25+
],
26+
responses: [
27+
new OA\Response(response: Response::HTTP_CREATED, description: 'Created'),
28+
new OA\Response(response: Response::HTTP_FORBIDDEN, description: 'Forbidden'),
29+
new OA\Response(response: Response::HTTP_UNPROCESSABLE_ENTITY, description: 'Validation error'),
30+
]
31+
)]
32+
public function __invoke(Request $request)
33+
{
34+
if ($request->user()->tokenCant('speedtests:run')) {
35+
return self::sendResponse(
36+
data: null,
37+
message: 'You do not have permission to run speedtests.',
38+
code: Response::HTTP_FORBIDDEN,
39+
);
40+
}
41+
42+
$validator = Validator::make($request->all(), [
43+
'server_id' => 'sometimes|integer',
44+
]);
45+
46+
if ($validator->fails()) {
47+
return ApiController::sendResponse(
48+
data: $validator->errors(),
49+
message: 'Validation failed.',
50+
code: Response::HTTP_UNPROCESSABLE_ENTITY,
51+
);
52+
}
53+
54+
$result = StartSpeedtest::run(
55+
serverId: $validated['server_id'] ?? null,
56+
);
57+
58+
return self::sendResponse(
59+
data: new ResultResource($result),
60+
message: 'Speedtest added to the queue.',
61+
code: Response::HTTP_CREATED,
62+
);
63+
}
64+
}

app/Jobs/Ookla/ProcessSpeedtestBatch.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ public function handle(): void
2929
{
3030
Bus::batch([
3131
[
32+
new StartSpeedtestJob($this->result),
3233
new CheckForInternetConnectionJob($this->result),
3334
new SkipSpeedtestJob($this->result),
3435
new SelectSpeedtestServerJob($this->result),
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
<?php
2+
3+
namespace App\Jobs\Ookla;
4+
5+
use App\Enums\ResultStatus;
6+
use App\Events\SpeedtestStarted;
7+
use App\Models\Result;
8+
use Illuminate\Bus\Batchable;
9+
use Illuminate\Contracts\Queue\ShouldQueue;
10+
use Illuminate\Foundation\Queue\Queueable;
11+
use Illuminate\Queue\Middleware\SkipIfBatchCancelled;
12+
13+
class StartSpeedtestJob implements ShouldQueue
14+
{
15+
use Batchable, Queueable;
16+
17+
/**
18+
* Create a new job instance.
19+
*/
20+
public function __construct(
21+
public Result $result,
22+
) {}
23+
24+
/**
25+
* Get the middleware the job should pass through.
26+
*/
27+
public function middleware(): array
28+
{
29+
return [
30+
new SkipIfBatchCancelled,
31+
];
32+
}
33+
34+
/**
35+
* Execute the job.
36+
*/
37+
public function handle(): void
38+
{
39+
$this->result->update([
40+
'status' => ResultStatus::Started,
41+
]);
42+
43+
SpeedtestStarted::dispatch($this->result);
44+
}
45+
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
<?php
2+
3+
use Illuminate\Database\Migrations\Migration;
4+
use Laravel\Sanctum\PersonalAccessToken;
5+
6+
return new class extends Migration
7+
{
8+
/**
9+
* Run the migrations.
10+
*/
11+
public function up(): void
12+
{
13+
PersonalAccessToken::all()->each(function (PersonalAccessToken $token) {
14+
$token->abilities = [
15+
'results:read',
16+
];
17+
18+
$token->save();
19+
});
20+
}
21+
22+
/**
23+
* Reverse the migrations.
24+
*/
25+
public function down(): void
26+
{
27+
//
28+
}
29+
};

routes/api/v1/routes.php

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
use App\Http\Controllers\Api\V1\LatestResult;
44
use App\Http\Controllers\Api\V1\ListResults;
5+
use App\Http\Controllers\Api\V1\RunSpeedtest;
56
use App\Http\Controllers\Api\V1\ShowResult;
67
use App\Http\Controllers\Api\V1\Stats;
78
use Illuminate\Support\Facades\Route;
@@ -16,6 +17,9 @@
1617
Route::get('/results/{result}', ShowResult::class)
1718
->name('results.show');
1819

20+
Route::post('/speedtests/run', RunSpeedtest::class)
21+
->name('speedtests.run');
22+
1923
Route::get('/stats', Stats::class)
2024
->name('stats');
2125
});

0 commit comments

Comments
 (0)