diff --git a/app/Filament/Components/Actions/ExportEggAction.php b/app/Filament/Components/Actions/ExportEggAction.php index 0f5221d2c..22942a1b9 100644 --- a/app/Filament/Components/Actions/ExportEggAction.php +++ b/app/Filament/Components/Actions/ExportEggAction.php @@ -4,7 +4,6 @@ namespace App\Filament\Components\Actions; use App\Enums\EggFormat; use App\Models\Egg; -use App\Services\Eggs\Sharing\EggExporterService; use Filament\Actions\Action; use Filament\Infolists\Components\TextEntry; use Filament\Support\Enums\Alignment; @@ -38,17 +37,15 @@ class ExportEggAction extends Action $this->modalFooterActionsAlignment(Alignment::Center); - $this->modalFooterActions([ //TODO: Close modal after clicking ->close() does not allow action to preform before closing modal + $this->modalFooterActions([ Action::make('json') ->label(trans('admin/egg.export.as', ['format' => 'json'])) - ->action(fn (EggExporterService $service, Egg $egg) => response()->streamDownload(function () use ($service, $egg) { - echo $service->handle($egg->id, EggFormat::JSON); - }, 'egg-' . $egg->getKebabName() . '.json')), + ->url(fn (Egg $egg) => route('api.application.eggs.eggs.export', ['egg' => $egg, 'format' => EggFormat::JSON->value]), true) + ->close(), Action::make('yaml') ->label(trans('admin/egg.export.as', ['format' => 'yaml'])) - ->action(fn (EggExporterService $service, Egg $egg) => response()->streamDownload(function () use ($service, $egg) { - echo $service->handle($egg->id, EggFormat::YAML); - }, 'egg-' . $egg->getKebabName() . '.yaml')), + ->url(fn (Egg $egg) => route('api.application.eggs.eggs.export', ['egg' => $egg, 'format' => EggFormat::YAML->value]), true) + ->close(), ]); } } diff --git a/app/Filament/Components/Actions/ExportScheduleAction.php b/app/Filament/Components/Actions/ExportScheduleAction.php index 6e461767b..090f7c554 100644 --- a/app/Filament/Components/Actions/ExportScheduleAction.php +++ b/app/Filament/Components/Actions/ExportScheduleAction.php @@ -29,6 +29,8 @@ class ExportScheduleAction extends Action $this->action(fn (ScheduleExporterService $service, Schedule $schedule) => response()->streamDownload(function () use ($service, $schedule) { echo $service->handle($schedule); - }, 'schedule-' . str($schedule->name)->kebab()->lower()->trim() . '.json')); + }, 'schedule-' . str($schedule->name)->kebab()->lower()->trim() . '.json', [ + 'Content-Type' => 'application/json', + ])); } } diff --git a/app/Http/Controllers/Api/Application/Eggs/EggController.php b/app/Http/Controllers/Api/Application/Eggs/EggController.php index 62b1c213b..8ff9dfc7b 100644 --- a/app/Http/Controllers/Api/Application/Eggs/EggController.php +++ b/app/Http/Controllers/Api/Application/Eggs/EggController.php @@ -2,14 +2,24 @@ namespace App\Http\Controllers\Api\Application\Eggs; +use App\Enums\EggFormat; use App\Http\Controllers\Api\Application\ApplicationApiController; +use App\Http\Requests\Api\Application\Eggs\ExportEggRequest; use App\Http\Requests\Api\Application\Eggs\GetEggRequest; use App\Http\Requests\Api\Application\Eggs\GetEggsRequest; use App\Models\Egg; +use App\Services\Eggs\Sharing\EggExporterService; use App\Transformers\Api\Application\EggTransformer; +use Symfony\Component\HttpFoundation\StreamedResponse; class EggController extends ApplicationApiController { + public function __construct( + private EggExporterService $exporterService, + ) { + parent::__construct(); + } + /** * List eggs * @@ -37,4 +47,20 @@ class EggController extends ApplicationApiController ->transformWith($this->getTransformer(EggTransformer::class)) ->toArray(); } + + /** + * Export egg + * + * Return a single egg as yaml or json file (defaults to YAML) + */ + public function export(ExportEggRequest $request, Egg $egg): StreamedResponse + { + $format = EggFormat::tryFrom($request->input('format')) ?? EggFormat::YAML; + + return response()->streamDownload(function () use ($egg, $format) { + echo $this->exporterService->handle($egg->id, $format); + }, 'egg-' . $egg->getKebabName() . '.' . $format->value, [ + 'Content-Type' => 'application/' . $format->value, + ]); + } } diff --git a/app/Http/Middleware/Api/Application/AuthenticateApplicationUser.php b/app/Http/Middleware/Api/Application/AuthenticateApplicationUser.php index 71977cc0b..c53807afc 100644 --- a/app/Http/Middleware/Api/Application/AuthenticateApplicationUser.php +++ b/app/Http/Middleware/Api/Application/AuthenticateApplicationUser.php @@ -17,7 +17,7 @@ class AuthenticateApplicationUser { /** @var User|null $user */ $user = $request->user(); - if (!$user || !$user->isRootAdmin()) { + if (!$user || !$user->isAdmin()) { throw new AccessDeniedHttpException('This account does not have permission to access the API.'); } diff --git a/app/Http/Requests/Api/Application/ApplicationApiRequest.php b/app/Http/Requests/Api/Application/ApplicationApiRequest.php index e97356348..561c9f1e4 100644 --- a/app/Http/Requests/Api/Application/ApplicationApiRequest.php +++ b/app/Http/Requests/Api/Application/ApplicationApiRequest.php @@ -41,10 +41,14 @@ abstract class ApplicationApiRequest extends FormRequest $token = $this->user()->currentAccessToken(); if ($token instanceof TransientToken) { - return true; + return match ($this->permission) { + default => false, + AdminAcl::READ => $this->user()->can('viewList ' . $this->resource) && $this->user()->can('view ' . $this->resource), + AdminAcl::WRITE => $this->user()->can('update ' . $this->resource), + }; } - if ($token->key_type === ApiKey::TYPE_ACCOUNT) { + if ($this->user()->isRootAdmin() && $token->key_type === ApiKey::TYPE_ACCOUNT) { return true; } diff --git a/app/Http/Requests/Api/Application/Eggs/ExportEggRequest.php b/app/Http/Requests/Api/Application/Eggs/ExportEggRequest.php new file mode 100644 index 000000000..be83a8809 --- /dev/null +++ b/app/Http/Requests/Api/Application/Eggs/ExportEggRequest.php @@ -0,0 +1,13 @@ + 'nullable|string|in:yaml,json', + ]; + } +} diff --git a/routes/api-application.php b/routes/api-application.php index 3f0ba2a81..953aca4d7 100644 --- a/routes/api-application.php +++ b/routes/api-application.php @@ -101,6 +101,7 @@ Route::prefix('/servers')->group(function () { Route::prefix('/eggs')->group(function () { Route::get('/', [Application\Eggs\EggController::class, 'index'])->name('api.application.eggs.eggs'); Route::get('/{egg:id}', [Application\Eggs\EggController::class, 'view'])->name('api.application.eggs.eggs.view'); + Route::get('/{egg:id}/export', [Application\Eggs\EggController::class, 'export'])->name('api.application.eggs.eggs.export'); }); /* diff --git a/tests/Traits/Http/RequestMockHelpers.php b/tests/Traits/Http/RequestMockHelpers.php index f873c5493..9f777f5d7 100644 --- a/tests/Traits/Http/RequestMockHelpers.php +++ b/tests/Traits/Http/RequestMockHelpers.php @@ -35,10 +35,11 @@ trait RequestMockHelpers /** * Generates a new request user model and also returns the generated model. */ - public function generateRequestUserModel(bool $isRootAdmin, array $args = []): void + public function generateRequestUserModel(bool $isAdmin, bool $isRootAdmin, array $args = []): void { $user = User::factory()->make($args); $user = m::mock($user)->makePartial(); + $user->shouldReceive('isAdmin')->andReturn($isAdmin); $user->shouldReceive('isRootAdmin')->andReturn($isRootAdmin); /** @var User|Mock $user */ diff --git a/tests/Unit/Http/Middleware/Api/Application/AuthenticateUserTest.php b/tests/Unit/Http/Middleware/Api/Application/AuthenticateUserTest.php index 38fd35e8a..342c5b295 100644 --- a/tests/Unit/Http/Middleware/Api/Application/AuthenticateUserTest.php +++ b/tests/Unit/Http/Middleware/Api/Application/AuthenticateUserTest.php @@ -27,7 +27,7 @@ class AuthenticateUserTest extends MiddlewareTestCase { $this->expectException(AccessDeniedHttpException::class); - $this->generateRequestUserModel(false); + $this->generateRequestUserModel(false, false); $this->getMiddleware()->handle($this->request, $this->getClosureAssertions()); } @@ -37,7 +37,17 @@ class AuthenticateUserTest extends MiddlewareTestCase */ public function test_admin_user(): void { - $this->generateRequestUserModel(true); + $this->generateRequestUserModel(true, false); + + $this->getMiddleware()->handle($this->request, $this->getClosureAssertions()); + } + + /** + * Test that a root admin user continues though the middleware. + */ + public function test_root_admin_user(): void + { + $this->generateRequestUserModel(true, true); $this->getMiddleware()->handle($this->request, $this->getClosureAssertions()); }