diff --git a/app/Actions/Ookla/StartSpeedtest.php b/app/Actions/Ookla/StartSpeedtest.php index 9e10b5ca8..cbcda90dd 100644 --- a/app/Actions/Ookla/StartSpeedtest.php +++ b/app/Actions/Ookla/StartSpeedtest.php @@ -4,7 +4,7 @@ use App\Enums\ResultService; use App\Enums\ResultStatus; -use App\Events\SpeedtestStarted; +use App\Events\SpeedtestWaiting; use App\Jobs\Ookla\ProcessSpeedtestBatch; use App\Models\Result; use Lorisleiva\Actions\Concerns\AsAction; @@ -13,12 +13,12 @@ class StartSpeedtest { use AsAction; - public function handle(bool $scheduled = false, ?int $serverId = null): void + public function handle(bool $scheduled = false, ?int $serverId = null): Result { $result = Result::create([ 'data->server->id' => $serverId, 'service' => ResultService::Ookla, - 'status' => ResultStatus::Started, + 'status' => ResultStatus::Waiting, 'scheduled' => $scheduled, ]); @@ -26,6 +26,8 @@ public function handle(bool $scheduled = false, ?int $serverId = null): void result: $result, ); - SpeedtestStarted::dispatch($result); + SpeedtestWaiting::dispatch($result); + + return $result; } } diff --git a/app/Enums/ResultStatus.php b/app/Enums/ResultStatus.php index 539ee7a1c..8b554f426 100644 --- a/app/Enums/ResultStatus.php +++ b/app/Enums/ResultStatus.php @@ -15,6 +15,7 @@ enum ResultStatus: string implements HasColor, HasLabel case Running = 'running'; case Started = 'started'; case Skipped = 'skipped'; + case Waiting = 'waiting'; public function getColor(): ?string { @@ -26,6 +27,7 @@ public function getColor(): ?string self::Running => 'info', self::Started => 'info', self::Skipped => 'gray', + self::Waiting => 'info', }; } diff --git a/app/Events/SpeedtestWaiting.php b/app/Events/SpeedtestWaiting.php new file mode 100644 index 000000000..3da212a30 --- /dev/null +++ b/app/Events/SpeedtestWaiting.php @@ -0,0 +1,20 @@ +maxLength('100') ->required() ->autocomplete(false), + CheckboxList::make('abilities') + ->options([ + 'results:read' => 'Read results', + 'speedtests:run' => 'Run speedtest', + ]) + ->descriptions([ + 'results:read' => 'Allow this token to read results.', + 'speedtests:run' => 'Allow this token to run speedtests.', + ]) + ->bulkToggleable(), DateTimePicker::make('token_expires_at') ->label('Expires at') ->nullable() @@ -81,6 +92,7 @@ public function table(Table $table): Table ->action(function (array $data) { $token = Auth::user()->createToken( name: $data['token_name'], + abilities: $data['abilities'], expiresAt: $data['token_expires_at'] ? Carbon::parse($data['token_expires_at']) : null, ); @@ -91,11 +103,14 @@ public function table(Table $table): Table ->modalWidth(MaxWidth::ExtraLarge), ]) ->columns([ - TextColumn::make('id') - ->label('ID') - ->sortable(), TextColumn::make('name') ->searchable(), + TextColumn::make('abilities') + ->badge(), + TextColumn::make('created_at') + ->alignEnd() + ->dateTime() + ->sortable(), TextColumn::make('last_used_at') ->alignEnd() ->dateTime() diff --git a/app/Http/Controllers/Api/V1/ApiController.php b/app/Http/Controllers/Api/V1/ApiController.php index 7c77c242a..132798dfb 100644 --- a/app/Http/Controllers/Api/V1/ApiController.php +++ b/app/Http/Controllers/Api/V1/ApiController.php @@ -13,6 +13,7 @@ abstract class ApiController * Send a response. * * @param mixed $data + * @param array $filters * @param string $message * @param int $code * @return \Illuminate\Http\JsonResponse @@ -29,7 +30,10 @@ public static function sendResponse($data, $filters = [], $message = 'ok', $code $response['message'] = $message; } - return response()->json($response, $code); + return response()->json( + data: $response, + status: $code, + ); } /** @@ -44,10 +48,15 @@ public static function throw($e, $code = 500) { Log::info($e); + $response = [ + 'message' => $e->getMessage(), + ]; + throw new HttpResponseException( - response: response()->json([ - 'message' => $e->getMessage(), - ], $code) + response: response()->json( + data: $response, + status: $code, + ), ); } } diff --git a/app/Http/Controllers/Api/V1/RunSpeedtest.php b/app/Http/Controllers/Api/V1/RunSpeedtest.php new file mode 100644 index 000000000..a68f6baa7 --- /dev/null +++ b/app/Http/Controllers/Api/V1/RunSpeedtest.php @@ -0,0 +1,64 @@ +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 = StartSpeedtest::run( + serverId: $validated['server_id'] ?? null, + ); + + return self::sendResponse( + data: new ResultResource($result), + message: 'Speedtest added to the queue.', + code: Response::HTTP_CREATED, + ); + } +} diff --git a/app/Jobs/Ookla/ProcessSpeedtestBatch.php b/app/Jobs/Ookla/ProcessSpeedtestBatch.php index ad7d698a0..71575fdc7 100644 --- a/app/Jobs/Ookla/ProcessSpeedtestBatch.php +++ b/app/Jobs/Ookla/ProcessSpeedtestBatch.php @@ -29,6 +29,7 @@ public function handle(): void { Bus::batch([ [ + new StartSpeedtestJob($this->result), new CheckForInternetConnectionJob($this->result), new SkipSpeedtestJob($this->result), new SelectSpeedtestServerJob($this->result), diff --git a/app/Jobs/Ookla/StartSpeedtestJob.php b/app/Jobs/Ookla/StartSpeedtestJob.php new file mode 100644 index 000000000..806aee592 --- /dev/null +++ b/app/Jobs/Ookla/StartSpeedtestJob.php @@ -0,0 +1,45 @@ +result->update([ + 'status' => ResultStatus::Started, + ]); + + SpeedtestStarted::dispatch($this->result); + } +} diff --git a/database/migrations/2025_05_19_160458_reset_existing_api_token_abilities.php b/database/migrations/2025_05_19_160458_reset_existing_api_token_abilities.php new file mode 100644 index 000000000..7d1fb7b3f --- /dev/null +++ b/database/migrations/2025_05_19_160458_reset_existing_api_token_abilities.php @@ -0,0 +1,29 @@ +each(function (PersonalAccessToken $token) { + $token->abilities = [ + 'results:read', + ]; + + $token->save(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + // + } +}; diff --git a/routes/api/v1/routes.php b/routes/api/v1/routes.php index f553176cc..deedca9cf 100644 --- a/routes/api/v1/routes.php +++ b/routes/api/v1/routes.php @@ -2,6 +2,7 @@ use App\Http\Controllers\Api\V1\LatestResult; use App\Http\Controllers\Api\V1\ListResults; +use App\Http\Controllers\Api\V1\RunSpeedtest; use App\Http\Controllers\Api\V1\ShowResult; use App\Http\Controllers\Api\V1\Stats; use Illuminate\Support\Facades\Route; @@ -16,6 +17,9 @@ Route::get('/results/{result}', ShowResult::class) ->name('results.show'); + Route::post('/speedtests/run', RunSpeedtest::class) + ->name('speedtests.run'); + Route::get('/stats', Stats::class) ->name('stats'); });