From 20ada7c2b46befb307945683506d624b054857d2 Mon Sep 17 00:00:00 2001 From: fchiapella Date: Fri, 12 Jun 2026 11:13:44 +0200 Subject: [PATCH 01/12] feat: add and setup scramble dependency --- .env.example | 4 + app/Http/Controllers/ChannelController.php | 4 +- composer.json | 1 + composer.lock | 312 +++++++++++++++++---- config/scramble.php | 198 +++++++++++++ routes/api.php | 4 +- routes/web.php | 2 +- 7 files changed, 458 insertions(+), 67 deletions(-) create mode 100644 config/scramble.php diff --git a/.env.example b/.env.example index 94ddbae..82c8408 100644 --- a/.env.example +++ b/.env.example @@ -57,6 +57,10 @@ TELESCOPE_VIEW_WATCHER=true L5_SWAGGER_GENERATE_ALWAYS=true +# Scramble API Documentation +SCRAMBLE_ENABLED=true +API_VERSION=1.0.0 + CLIENT_ID_SENDY= CLIENT_SECRET_SENDY= URL_SENDY=https://sendy.zanichelli.it/ diff --git a/app/Http/Controllers/ChannelController.php b/app/Http/Controllers/ChannelController.php index 9f7a3df..daaa816 100644 --- a/app/Http/Controllers/ChannelController.php +++ b/app/Http/Controllers/ChannelController.php @@ -393,7 +393,7 @@ public function sendMessage(MessageChannelRequest $request) * summary="Send a message to Channel", * tags={"messages"}, * description="Use to send message to the subscribers of a channel", - * operationId="ChannelController.SendMessageToChannel", + * operationId="ChannelController.sendMessageToChannel", * security = {{"basicAuth": {}}}, * @OA\Parameter( * in="path", @@ -437,7 +437,7 @@ public function sendMessage(MessageChannelRequest $request) * @return Response * */ - public function SendMessageToChannel(MessageRequest $request, $channelName) + public function sendMessageToChannel(MessageRequest $request, $channelName) { $channel = $this->getCachedChannelByName($channelName); diff --git a/composer.json b/composer.json index 2ec8877..0307ebb 100644 --- a/composer.json +++ b/composer.json @@ -10,6 +10,7 @@ "require": { "php": "^8.2", "darkaonline/l5-swagger": "^8.3", + "dedoc/scramble": "^0.13.26", "laravel/framework": "^12.0", "laravel/legacy-factories": "^1.1", "laravel/passport": "^12.0", diff --git a/composer.lock b/composer.lock index ce94207..c92a0d5 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "3d4d62613976b9efcddda22e8ebb1b49", + "content-hash": "c76827acfde3850b27da33b314df0424", "packages": [ { "name": "aws/aws-crt-php", @@ -366,6 +366,86 @@ ], "time": "2025-02-06T14:54:32+00:00" }, + { + "name": "dedoc/scramble", + "version": "v0.13.26", + "source": { + "type": "git", + "url": "https://github.com/dedoc/scramble.git", + "reference": "5ca42b5e23b9d5c120607138f790b51e22d8b4a1" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/dedoc/scramble/zipball/5ca42b5e23b9d5c120607138f790b51e22d8b4a1", + "reference": "5ca42b5e23b9d5c120607138f790b51e22d8b4a1", + "shasum": "" + }, + "require": { + "illuminate/contracts": "^10.0|^11.0|^12.0|^13.0", + "myclabs/deep-copy": "^1.12", + "nikic/php-parser": "^5.0", + "php": "^8.1", + "phpstan/phpdoc-parser": "^1.0|^2.0", + "spatie/laravel-package-tools": "^1.9.2" + }, + "require-dev": { + "larastan/larastan": "^3.3", + "laravel/pint": "^v1.1.0", + "nunomaduro/collision": "^7.0|^8.0", + "orchestra/testbench": "^8.0|^9.0|^10.0|^11.0", + "pestphp/pest": "^2.34|^3.7|^4.4", + "pestphp/pest-plugin-laravel": "^2.3|^3.1|^4.1", + "phpstan/extension-installer": "^1.4", + "phpstan/phpstan-deprecation-rules": "^2.0", + "phpstan/phpstan-phpunit": "^2.0", + "phpunit/phpunit": "^10.5|^11.5.3|^12.5.12", + "spatie/laravel-permission": "^6.10|^7.2", + "spatie/pest-plugin-snapshots": "^2.1" + }, + "type": "library", + "extra": { + "laravel": { + "providers": [ + "Dedoc\\Scramble\\ScrambleServiceProvider" + ] + } + }, + "autoload": { + "psr-4": { + "Dedoc\\Scramble\\": "src", + "Dedoc\\Scramble\\Database\\Factories\\": "database/factories" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Roman Lytvynenko", + "email": "litvinenko95@gmail.com", + "role": "Developer" + } + ], + "description": "Automatic generation of API documentation for Laravel applications.", + "homepage": "https://github.com/dedoc/scramble", + "keywords": [ + "documentation", + "laravel", + "openapi" + ], + "support": { + "issues": "https://github.com/dedoc/scramble/issues", + "source": "https://github.com/dedoc/scramble/tree/v0.13.26" + }, + "funding": [ + { + "url": "https://github.com/romalytvynenko", + "type": "github" + } + ], + "time": "2026-06-02T14:43:17+00:00" + }, { "name": "defuse/php-encryption", "version": "v2.4.0", @@ -3210,6 +3290,66 @@ }, "time": "2024-09-04T18:46:31+00:00" }, + { + "name": "myclabs/deep-copy", + "version": "1.13.1", + "source": { + "type": "git", + "url": "https://github.com/myclabs/DeepCopy.git", + "reference": "1720ddd719e16cf0db4eb1c6eca108031636d46c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/1720ddd719e16cf0db4eb1c6eca108031636d46c", + "reference": "1720ddd719e16cf0db4eb1c6eca108031636d46c", + "shasum": "" + }, + "require": { + "php": "^7.1 || ^8.0" + }, + "conflict": { + "doctrine/collections": "<1.6.8", + "doctrine/common": "<2.13.3 || >=3 <3.2.2" + }, + "require-dev": { + "doctrine/collections": "^1.6.8", + "doctrine/common": "^2.13.3 || ^3.2.2", + "phpspec/prophecy": "^1.10", + "phpunit/phpunit": "^7.5.20 || ^8.5.23 || ^9.5.13" + }, + "type": "library", + "autoload": { + "files": [ + "src/DeepCopy/deep_copy.php" + ], + "psr-4": { + "DeepCopy\\": "src/DeepCopy/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Create deep copies (clones) of your objects", + "keywords": [ + "clone", + "copy", + "duplicate", + "object", + "object graph" + ], + "support": { + "issues": "https://github.com/myclabs/DeepCopy/issues", + "source": "https://github.com/myclabs/DeepCopy/tree/1.13.1" + }, + "funding": [ + { + "url": "https://tidelift.com/funding/github/packagist/myclabs/deep-copy", + "type": "tidelift" + } + ], + "time": "2025-04-29T12:36:36+00:00" + }, { "name": "nesbot/carbon", "version": "3.9.1", @@ -3989,6 +4129,53 @@ ], "time": "2024-12-14T21:12:59+00:00" }, + { + "name": "phpstan/phpdoc-parser", + "version": "2.3.2", + "source": { + "type": "git", + "url": "https://github.com/phpstan/phpdoc-parser.git", + "reference": "a004701b11273a26cd7955a61d67a7f1e525a45a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpstan/phpdoc-parser/zipball/a004701b11273a26cd7955a61d67a7f1e525a45a", + "reference": "a004701b11273a26cd7955a61d67a7f1e525a45a", + "shasum": "" + }, + "require": { + "php": "^7.4 || ^8.0" + }, + "require-dev": { + "doctrine/annotations": "^2.0", + "nikic/php-parser": "^5.3.0", + "php-parallel-lint/php-parallel-lint": "^1.2", + "phpstan/extension-installer": "^1.0", + "phpstan/phpstan": "^2.0", + "phpstan/phpstan-phpunit": "^2.0", + "phpstan/phpstan-strict-rules": "^2.0", + "phpunit/phpunit": "^9.6", + "symfony/process": "^5.2" + }, + "type": "library", + "autoload": { + "psr-4": { + "PHPStan\\PhpDocParser\\": [ + "src/" + ] + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "PHPDoc parser with support for nullable, intersection and generic types", + "support": { + "issues": "https://github.com/phpstan/phpdoc-parser/issues", + "source": "https://github.com/phpstan/phpdoc-parser/tree/2.3.2" + }, + "time": "2026-01-25T14:56:51+00:00" + }, { "name": "psr/cache", "version": "3.0.0", @@ -4741,6 +4928,67 @@ ], "time": "2024-04-27T21:32:50+00:00" }, + { + "name": "spatie/laravel-package-tools", + "version": "1.93.1", + "source": { + "type": "git", + "url": "https://github.com/spatie/laravel-package-tools.git", + "reference": "d5552849801f2642aea710557463234b59ef65eb" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/spatie/laravel-package-tools/zipball/d5552849801f2642aea710557463234b59ef65eb", + "reference": "d5552849801f2642aea710557463234b59ef65eb", + "shasum": "" + }, + "require": { + "illuminate/contracts": "^10.0|^11.0|^12.0|^13.0", + "php": "^8.1" + }, + "require-dev": { + "mockery/mockery": "^1.5", + "orchestra/testbench": "^8.0|^9.2|^10.0|^11.0", + "pestphp/pest": "^2.1|^3.1|^4.0", + "phpunit/php-code-coverage": "^10.0|^11.0|^12.0", + "phpunit/phpunit": "^10.5|^11.5|^12.5", + "spatie/pest-plugin-test-time": "^2.2|^3.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Spatie\\LaravelPackageTools\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Freek Van der Herten", + "email": "freek@spatie.be", + "role": "Developer" + } + ], + "description": "Tools for creating Laravel packages", + "homepage": "https://github.com/spatie/laravel-package-tools", + "keywords": [ + "laravel-package-tools", + "spatie" + ], + "support": { + "issues": "https://github.com/spatie/laravel-package-tools/issues", + "source": "https://github.com/spatie/laravel-package-tools/tree/1.93.1" + }, + "funding": [ + { + "url": "https://github.com/spatie", + "type": "github" + } + ], + "time": "2026-05-19T14:06:37+00:00" + }, { "name": "swagger-api/swagger-ui", "version": "v5.21.0", @@ -7982,66 +8230,6 @@ }, "time": "2024-05-16T03:13:13+00:00" }, - { - "name": "myclabs/deep-copy", - "version": "1.13.1", - "source": { - "type": "git", - "url": "https://github.com/myclabs/DeepCopy.git", - "reference": "1720ddd719e16cf0db4eb1c6eca108031636d46c" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/1720ddd719e16cf0db4eb1c6eca108031636d46c", - "reference": "1720ddd719e16cf0db4eb1c6eca108031636d46c", - "shasum": "" - }, - "require": { - "php": "^7.1 || ^8.0" - }, - "conflict": { - "doctrine/collections": "<1.6.8", - "doctrine/common": "<2.13.3 || >=3 <3.2.2" - }, - "require-dev": { - "doctrine/collections": "^1.6.8", - "doctrine/common": "^2.13.3 || ^3.2.2", - "phpspec/prophecy": "^1.10", - "phpunit/phpunit": "^7.5.20 || ^8.5.23 || ^9.5.13" - }, - "type": "library", - "autoload": { - "files": [ - "src/DeepCopy/deep_copy.php" - ], - "psr-4": { - "DeepCopy\\": "src/DeepCopy/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "description": "Create deep copies (clones) of your objects", - "keywords": [ - "clone", - "copy", - "duplicate", - "object", - "object graph" - ], - "support": { - "issues": "https://github.com/myclabs/DeepCopy/issues", - "source": "https://github.com/myclabs/DeepCopy/tree/1.13.1" - }, - "funding": [ - { - "url": "https://tidelift.com/funding/github/packagist/myclabs/deep-copy", - "type": "tidelift" - } - ], - "time": "2025-04-29T12:36:36+00:00" - }, { "name": "nunomaduro/collision", "version": "v8.8.0", @@ -9729,5 +9917,5 @@ "php": "^8.2" }, "platform-dev": {}, - "plugin-api-version": "2.6.0" + "plugin-api-version": "2.9.0" } diff --git a/config/scramble.php b/config/scramble.php new file mode 100644 index 0000000..2338bb3 --- /dev/null +++ b/config/scramble.php @@ -0,0 +1,198 @@ + [ + * 'include' => 'api', + * 'exclude' => ['api/internal'], + * ], + * + * Without *, patterns match path segments (api matches api and api/users, not apiary). + * With *, Str::is is used (e.g. api/v*). + * + * One static include → default server is /{include} and paths are stripped (/users). + * Multiple includes or wildcards → server defaults to / and paths stay full (/api/users). + * Override with `servers`, or use Scramble::registerApi() for separate bases. + */ + 'api_path' => [ + 'include' => 'api', + 'exclude' => [ + 'api/documentation', // l5-swagger UI + 'api/oauth2-callback', // l5-swagger OAuth callback + 'docs', // l5-swagger docs JSON + 'api/debug', // Debug endpoint + 'api/basic', // Test endpoint + 'api/oauth', // Test endpoint + 'api/none', // Test endpoint + ], + ], + + /* + * Your API domain. By default, app domain is used. This is also a part of the default API routes + * matcher, so when implementing your own, make sure you use this config if needed. + */ + 'api_domain' => null, + + /* + * The path where your OpenAPI specification will be exported. + */ + 'export_path' => 'api.json', + + 'info' => [ + /* + * API version. + */ + 'version' => env('API_VERSION', '1.0.0'), + + /* + * Description rendered on the home page of the API documentation (`/docs/api`). + */ + 'description' => 'Auto-generated API documentation for Buzzer - A publish/subscribe messaging system', + ], + + 'ui' => [ + 'title' => 'Buzzer API - Scramble Documentation', + ], + + 'renderer' => 'elements', + + 'renderers' => [ + /* + * Stoplight Elements config options: https://docs.stoplight.io/docs/elements/b074dc47b2826-elements-configuration-options + */ + 'elements' => [ + 'view' => 'scramble::docs', + 'theme' => 'dark', + 'hideTryIt' => false, + 'hideSchemas' => false, + 'logo' => '', + 'tryItCredentialsPolicy' => 'include', + 'layout' => 'responsive', + 'router' => 'hash', + ], + /* + * Scalar API reference config options: https://scalar.com/products/api-references/configuration + */ + 'scalar' => [ + 'view' => 'scramble::scalar', + 'cdn' => 'https://cdn.jsdelivr.net/npm/@scalar/api-reference', + 'theme' => 'laravel', + 'proxyUrl' => 'https://proxy.scalar.com', + 'darkMode' => false, + 'showDeveloperTools' => 'never', + 'agent' => ['disabled' => true], + 'credentials' => 'include', + ], + ], + + /* + * The list of servers of the API. By default, when `null`, server URL will be created from + * `scramble.api_path` and `scramble.api_domain` config variables. When providing an array, you + * will need to specify the local server URL manually (if needed). + * + * Example of non-default config (final URLs are generated using Laravel `url` helper): + * + * ```php + * 'servers' => [ + * 'Live' => 'api', + * 'Prod' => 'https://scramble.dedoc.co/api', + * ], + * ``` + */ + 'servers' => [ + 'Local' => env('APP_URL', 'http://localhost:8085') . '/api', + ], + + /** + * Determines how Scramble stores the descriptions of enum cases. + * Available options: + * - 'description' – Case descriptions are stored as the enum schema's description using table formatting. + * - 'extension' – Case descriptions are stored in the `x-enumDescriptions` enum schema extension. + * + * @see https://redocly.com/docs-legacy/api-reference-docs/specification-extensions/x-enum-descriptions + * - false - Case descriptions are ignored. + */ + 'enum_cases_description_strategy' => 'description', + + /** + * Determines how Scramble stores the names of enum cases. + * Available options: + * - 'names' – Case names are stored in the `x-enumNames` enum schema extension. + * - 'varnames' - Case names are stored in the `x-enum-varnames` enum schema extension. + * - false - Case names are not stored. + */ + 'enum_cases_names_strategy' => false, + + /** + * When Scramble encounters deep objects in query parameters, it flattens the parameters so the generated + * OpenAPI document correctly describes the API. Flattening deep query parameters is relevant until + * OpenAPI 3.2 is released and query string structure can be described properly. + * + * For example, this nested validation rule describes the object with `bar` property: + * `['foo.bar' => ['required', 'int']]`. + * + * When `flatten_deep_query_parameters` is `true`, Scramble will document the parameter like so: + * `{"name":"foo[bar]", "schema":{"type":"int"}, "required":true}`. + * + * When `flatten_deep_query_parameters` is `false`, Scramble will document the parameter like so: + * `{"name":"foo", "schema": {"type":"object", "properties":{"bar":{"type": "int"}}, "required": ["bar"]}, "required":true}`. + */ + 'flatten_deep_query_parameters' => true, + + 'middleware' => [ + // Empty to allow public access like l5-swagger + // 'web' middleware would trigger IDP authentication + ], + + 'extensions' => [], + + /* + * Security schemes for the API. Defines the authentication methods available. + */ + 'security_schemes' => [ + 'passport' => [ + 'type' => 'oauth2', + 'description' => 'OAuth2 client credentials flow for admin API access', + 'flows' => [ + 'clientCredentials' => [ + 'tokenUrl' => env('APP_URL', 'http://localhost:8085') . '/oauth/token', + 'scopes' => [], + ], + ], + ], + 'basicAuth' => [ + 'type' => 'http', + 'scheme' => 'basic', + 'description' => 'Basic HTTP authentication for publishers sending messages', + ], + ], + + /* + * Automatically document API security (OpenAPI `security` / `securitySchemes`) based on route + * middleware. + * + * Disabled by default. Uncomment the line below to enable `MiddlewareAuthSecurityStrategy`. + * When at least one documented route uses middleware matching the configured patterns (by default + * `auth` and `auth:*`), bearer auth is applied globally. Routes without matching middleware are + * marked as public (`security: []`). + * + * Set to `null` explicitly to disable. If you already configure security manually via + * `afterOpenApiGenerated` / `extendOpenApi`, keep this disabled to avoid duplicate schemes. + * + * Customize with a class-string or [class, options]: + * + * 'security_strategy' => [ + * \Dedoc\Scramble\SecurityDocumentation\MiddlewareAuthSecurityStrategy::class, + * [ + * 'middleware' => ['auth', 'auth:*'], + * 'scheme' => \Dedoc\Scramble\Support\Generator\SecurityScheme::http('bearer'), + * ], + * ], + */ + // 'security_strategy' => \Dedoc\Scramble\SecurityDocumentation\MiddlewareAuthSecurityStrategy::class, + 'security_strategy' => null, +]; diff --git a/routes/api.php b/routes/api.php index 42c8a78..cf22fef 100644 --- a/routes/api.php +++ b/routes/api.php @@ -5,8 +5,8 @@ use Illuminate\Support\Facades\Route; Route::middleware(['basicAuth'])->group(function () { - Route::post('/sendMessage', 'ChannelController@SendMessage'); - Route::post('/sendMessage/{channelName}', 'ChannelController@SendMessageToChannel'); + Route::post('/sendMessage', 'ChannelController@sendMessage'); + Route::post('/sendMessage/{channelName}', 'ChannelController@sendMessageToChannel'); }); Route::post('/logout-idp', 'Auth\LoginController@logoutIdp')->name('logoutIdp'); diff --git a/routes/web.php b/routes/web.php index 51bf6b7..43740a6 100644 --- a/routes/web.php +++ b/routes/web.php @@ -70,7 +70,7 @@ Route::get('/{any}', function () { return view('app'); - })->where("any", ".*"); + })->where("any", "^(?!docs).*"); // Exclude /docs/* paths (for Scramble documentation) }); }); From 1f337b0b9b454e95b7125172305acd148b2e79ef Mon Sep 17 00:00:00 2001 From: fchiapella Date: Tue, 16 Jun 2026 14:34:04 +0200 Subject: [PATCH 02/12] feat: generates correct response for Scramble doc --- app/Providers/ScrambleServiceProvider.php | 179 ++++++++++++++++++++++ config/app.php | 1 + 2 files changed, 180 insertions(+) create mode 100644 app/Providers/ScrambleServiceProvider.php diff --git a/app/Providers/ScrambleServiceProvider.php b/app/Providers/ScrambleServiceProvider.php new file mode 100644 index 0000000..598243b --- /dev/null +++ b/app/Providers/ScrambleServiceProvider.php @@ -0,0 +1,179 @@ +getAction(); + if (isset($action['controller'])) { + [$controller, $method] = explode('@', $action['controller']); + $key = $controller . '@' . $method; + + try { + $reflection = new \ReflectionMethod($controller, $method); + $filename = $reflection->getFileName(); + $startLine = $reflection->getStartLine(); + $endLine = $reflection->getEndLine(); + + if ($filename && $startLine && $endLine) { + $lines = file($filename); + $source = implode('', array_slice($lines, $startLine - 1, $endLine - $startLine + 1)); + $routeCache[$key] = $source; + } + } catch (\Throwable $e) { + // Skip if reflection fails + } + } + } + + // Define response schemas using Type objects + $responseSchemas = [ + 'error404' => [ + 'code' => 404, + 'description' => 'Resource not found', + 'schema' => $this->createErrorSchema('Resource not found'), + ], + 'error500' => [ + 'code' => 500, + 'description' => 'Internal server error', + 'schema' => $this->createErrorSchema('An error occurred while processing your request'), + ], + 'success200' => [ + 'code' => 200, + 'description' => 'Success', + 'schema' => $this->createSuccessSchema('Operation completed successfully'), + ], + 'success201' => [ + 'code' => 201, + 'description' => 'Resource created', + 'schema' => $this->createSuccessSchema('Resource created successfully'), + ], + 'error422' => [ + 'code' => 422, + 'description' => 'Validation error', + 'schema' => $this->createValidationErrorSchema(), + ], + ]; + + // Add responses to operations based on their source code + foreach ($openApi->paths as $pathKey => $path) { + foreach ($path->operations as $operation) { + $existingCodes = collect($operation->responses)->pluck('code')->toArray(); + + // Try to find matching route source code + $source = null; + $operationId = $operation->operationId ?? ''; + + foreach ($routeCache as $routeKey => $routeSource) { + // Extract controller and method from route key (e.g., "App\Http\Controllers\PublisherController@destroy") + $parts = explode('@', $routeKey); + if (count($parts) !== 2) { + continue; + } + + [$controllerClass, $method] = $parts; + $controllerShortName = class_basename($controllerClass); + + // Try to match with operationId patterns like "publisher.destroy" + $expectedOperationId = strtolower(str_replace('Controller', '', $controllerShortName)) . '.' . $method; + + if ($operationId === $expectedOperationId) { + $source = $routeSource; + break; + } + } + + if (!$source) { + continue; + } + + // Check if source code contains response macro calls and add/replace corresponding responses + foreach ($responseSchemas as $macroName => $responseInfo) { + if (str_contains($source, "->$macroName(")) { + // Remove existing response with same code if present + // Handle both Response objects and Reference objects + $operation->responses = collect($operation->responses) + ->reject(function($r) use ($responseInfo) { + // Check if it's a Response object with a code property + if (property_exists($r, 'code')) { + return $r->code === $responseInfo['code']; + } + return false; + }) + ->values() + ->all(); + + // Add the correct response + $response = Response::make($responseInfo['code']) + ->description($responseInfo['description']) + ->setContent('application/json', $responseInfo['schema']); + + $operation->addResponse($response); + } + } + } + } + }); + } + + /** + * Create a simple error response schema + */ + private function createErrorSchema(string $exampleMessage): Schema + { + $objectType = new ObjectType(); + $messageType = (new StringType())->example($exampleMessage); + $objectType->addProperty('message', $messageType); + $objectType->setRequired(['message']); + + return Schema::fromType($objectType); + } + + /** + * Create a simple success response schema + */ + private function createSuccessSchema(string $exampleMessage): Schema + { + $objectType = new ObjectType(); + $messageType = (new StringType())->example($exampleMessage); + $objectType->addProperty('message', $messageType); + + return Schema::fromType($objectType); + } + + /** + * Create a validation error response schema + */ + private function createValidationErrorSchema(): Schema + { + $objectType = new ObjectType(); + + $messageType = (new StringType())->example('Data is invalid'); + $objectType->addProperty('message', $messageType); + + // errors is an object with arbitrary string array properties + $errorsObjectType = new ObjectType(); + $errorsObjectType->additionalProperties = new ArrayType(); + $objectType->addProperty('errors', $errorsObjectType); + + $objectType->setRequired(['message', 'errors']); + + return Schema::fromType($objectType); + } +} diff --git a/config/app.php b/config/app.php index 310247a..1854482 100644 --- a/config/app.php +++ b/config/app.php @@ -177,6 +177,7 @@ App\Providers\TelescopeServiceProvider::class, App\Providers\RouteServiceProvider::class, App\Providers\ResponseMacroServiceProvider::class, + App\Providers\ScrambleServiceProvider::class, ], From c396b8a81d348670a4332b220d55c9aa78491519 Mon Sep 17 00:00:00 2001 From: fchiapella Date: Fri, 19 Jun 2026 17:03:29 +0200 Subject: [PATCH 03/12] feat: add passport auth button to scramble ui --- app/Providers/ScrambleServiceProvider.php | 72 ++++ .../views/vendor/scramble/docs.blade.php | 375 ++++++++++++++++++ 2 files changed, 447 insertions(+) create mode 100644 resources/views/vendor/scramble/docs.blade.php diff --git a/app/Providers/ScrambleServiceProvider.php b/app/Providers/ScrambleServiceProvider.php index 598243b..81d59a1 100644 --- a/app/Providers/ScrambleServiceProvider.php +++ b/app/Providers/ScrambleServiceProvider.php @@ -6,6 +6,9 @@ use Dedoc\Scramble\Support\Generator\OpenApi; use Dedoc\Scramble\Support\Generator\Response; use Dedoc\Scramble\Support\Generator\Schema; +use Dedoc\Scramble\Support\Generator\SecurityRequirement; +use Dedoc\Scramble\Support\Generator\SecurityScheme; +use Dedoc\Scramble\Support\Generator\SecuritySchemes\OAuthFlow; use Dedoc\Scramble\Support\Generator\Types\ObjectType; use Dedoc\Scramble\Support\Generator\Types\StringType; use Dedoc\Scramble\Support\Generator\Types\ArrayType; @@ -17,6 +20,8 @@ class ScrambleServiceProvider extends ServiceProvider public function boot(): void { Scramble::afterOpenApiGenerated(function (OpenApi $openApi) { + // Add security schemes to OpenAPI spec + $this->addSecuritySchemes($openApi); // Cache route information $routeCache = []; foreach (Route::getRoutes() as $route) { @@ -129,6 +134,47 @@ public function boot(): void } } } + + // Apply security requirements based on route middleware + $routeMiddlewareMap = []; + foreach (Route::getRoutes() as $route) { + // Only process API routes + if (!str_starts_with($route->uri(), 'api/')) { + continue; + } + + $action = $route->getAction(); + if (isset($action['controller'])) { + $parts = explode('@', $action['controller']); + if (count($parts) === 2) { + [$controller, $method] = $parts; + $controllerShortName = class_basename($controller); + $operationId = strtolower(str_replace('Controller', '', $controllerShortName)) . '.' . $method; + + // Get all middleware for this route + $middlewares = $route->gatherMiddleware(); + $routeMiddlewareMap[$operationId] = $middlewares; + } + } + } + + // Apply security schemes to operations based on their middleware + foreach ($openApi->paths as $path) { + foreach ($path->operations as $operation) { + $operationId = $operation->operationId ?? ''; + $middlewares = $routeMiddlewareMap[$operationId] ?? []; + + // Apply appropriate security scheme based on middleware + if (in_array('client', $middlewares)) { + // OAuth2 client credentials for admin API routes + $operation->security = [new SecurityRequirement(['passport' => []])]; + } elseif (in_array('basicAuth', $middlewares)) { + // HTTP Basic Auth for publisher message routes + $operation->security = [new SecurityRequirement(['basicAuth' => []])]; + } + // Routes without these middleware will have no security requirement + } + } }); } @@ -176,4 +222,30 @@ private function createValidationErrorSchema(): Schema return Schema::fromType($objectType); } + + /** + * Add security schemes to the OpenAPI specification + */ + private function addSecuritySchemes(OpenApi $openApi): void + { + // Add OAuth2 (Laravel Passport) security scheme + $clientCredentialsFlow = new OAuthFlow(); + $clientCredentialsFlow->tokenUrl(config('app.url') . '/oauth/token'); + + $oauth2Scheme = SecurityScheme::oauth2() + ->as('passport') + ->setDescription('OAuth2 client credentials flow for admin API access') + ->flows(function ($flows) use ($clientCredentialsFlow) { + $flows->clientCredentials($clientCredentialsFlow); + }); + + $openApi->components->addSecurityScheme('passport', $oauth2Scheme); + + // Add HTTP Basic Auth security scheme + $basicAuthScheme = SecurityScheme::http('basic') + ->as('basicAuth') + ->setDescription('Basic HTTP authentication for publishers sending messages'); + + $openApi->components->addSecurityScheme('basicAuth', $basicAuthScheme); + } } diff --git a/resources/views/vendor/scramble/docs.blade.php b/resources/views/vendor/scramble/docs.blade.php new file mode 100644 index 0000000..1108382 --- /dev/null +++ b/resources/views/vendor/scramble/docs.blade.php @@ -0,0 +1,375 @@ + + + + + + + {{ $config->get('ui.title') ?? config('app.name') . ' - API Docs' }} + + + + + + + + + + + + + + + + +renderer()->all(except: ['theme']) as $key => $value) + @continue(! $value) + {{ $key }}="{{ $value === true ? 'true' : ($value === false ? 'false' : $value) }}" + @endforeach +/> + + +@if($config->renderer()->get('theme', 'light') === 'system') + +@endif + + + + + From 648b923177b9e17f1ab457468914e4bbc3868fa0 Mon Sep 17 00:00:00 2001 From: fchiapella Date: Fri, 26 Jun 2026 15:53:22 +0200 Subject: [PATCH 04/12] feat: update token correctly, add missing return code and example of overwritten return code --- app/Http/Controllers/ChannelController.php | 4 ++ .../Controllers/ChannelPublishController.php | 5 ++ app/Providers/ScrambleServiceProvider.php | 58 +++++++++++++--- .../views/vendor/scramble/docs.blade.php | 68 +++++++++++++++++-- 4 files changed, 120 insertions(+), 15 deletions(-) diff --git a/app/Http/Controllers/ChannelController.php b/app/Http/Controllers/ChannelController.php index daaa816..d164e88 100644 --- a/app/Http/Controllers/ChannelController.php +++ b/app/Http/Controllers/ChannelController.php @@ -17,6 +17,7 @@ use App\Http\Repositories\PublisherRepository; use App\Http\Resources\ChannelSubscribeResource; use App\Http\Repositories\ChannelSubscribeRepository; +use Dedoc\Scramble\Attributes\Response as ScrambleResponse; class ChannelController extends Controller { @@ -183,6 +184,9 @@ public function getChannel($id) * ) * ) */ + #[ScrambleResponse(status: 201, description: 'Channel created successfully')] + #[ScrambleResponse(status: 422, description: 'Validation error')] + #[ScrambleResponse(status: 500, description: 'Internal server error')] public function store(ChannelRequest $request) { $channel = $request->only(['name', 'priority']); diff --git a/app/Http/Controllers/ChannelPublishController.php b/app/Http/Controllers/ChannelPublishController.php index 4801b3c..1fcf64c 100644 --- a/app/Http/Controllers/ChannelPublishController.php +++ b/app/Http/Controllers/ChannelPublishController.php @@ -7,6 +7,7 @@ use App\Http\Repositories\PublisherRepository; use App\Http\Repositories\RepositoryInterface; use App\Http\Resources\PublisherChannelResource; +use Dedoc\Scramble\Attributes\Response as ScrambleResponse; class ChannelPublishController extends Controller { @@ -67,6 +68,10 @@ public function __construct(RepositoryInterface $channelPublishRepository, Publi * ) * ) */ + #[ScrambleResponse(status: 201, description: 'Publisher successfully registered to channel')] + #[ScrambleResponse(status: 409, description: 'Conflict - Subscription already exists')] + #[ScrambleResponse(status: 422, description: 'Validation error or publisher not found')] + #[ScrambleResponse(status: 500, description: 'Internal server error')] public function store(ChannelPublishRequest $request, $id) { if (!$this->publisherRepository->find($id)) { diff --git a/app/Providers/ScrambleServiceProvider.php b/app/Providers/ScrambleServiceProvider.php index 81d59a1..c13ea56 100644 --- a/app/Providers/ScrambleServiceProvider.php +++ b/app/Providers/ScrambleServiceProvider.php @@ -14,6 +14,7 @@ use Dedoc\Scramble\Support\Generator\Types\ArrayType; use Illuminate\Support\ServiceProvider; use Illuminate\Support\Facades\Route; +use Dedoc\Scramble\Attributes\Response as ScrambleResponse; class ScrambleServiceProvider extends ServiceProvider { @@ -22,8 +23,9 @@ public function boot(): void Scramble::afterOpenApiGenerated(function (OpenApi $openApi) { // Add security schemes to OpenAPI spec $this->addSecuritySchemes($openApi); - // Cache route information + // Cache route information and check for Scramble Response attributes $routeCache = []; + $routeScrambleResponseCodes = []; foreach (Route::getRoutes() as $route) { $action = $route->getAction(); if (isset($action['controller'])) { @@ -41,6 +43,17 @@ public function boot(): void $source = implode('', array_slice($lines, $startLine - 1, $endLine - $startLine + 1)); $routeCache[$key] = $source; } + + // Check if method has Scramble Response attributes and collect their status codes + $attributes = $reflection->getAttributes(ScrambleResponse::class); + if (!empty($attributes)) { + $statusCodes = []; + foreach ($attributes as $attribute) { + $instance = $attribute->newInstance(); + $statusCodes[] = (int) $instance->status; + } + $routeScrambleResponseCodes[$key] = $statusCodes; + } } catch (\Throwable $e) { // Skip if reflection fails } @@ -54,6 +67,16 @@ public function boot(): void 'description' => 'Resource not found', 'schema' => $this->createErrorSchema('Resource not found'), ], + 'error409' => [ + 'code' => 409, + 'description' => 'Conflict - Resource already exists', + 'schema' => $this->createErrorSchema('The resource already exists'), + ], + 'error422' => [ + 'code' => 422, + 'description' => 'Validation error', + 'schema' => $this->createValidationErrorSchema(), + ], 'error500' => [ 'code' => 500, 'description' => 'Internal server error', @@ -69,11 +92,6 @@ public function boot(): void 'description' => 'Resource created', 'schema' => $this->createSuccessSchema('Resource created successfully'), ], - 'error422' => [ - 'code' => 422, - 'description' => 'Validation error', - 'schema' => $this->createValidationErrorSchema(), - ], ]; // Add responses to operations based on their source code @@ -84,6 +102,7 @@ public function boot(): void // Try to find matching route source code $source = null; $operationId = $operation->operationId ?? ''; + $matchedRouteKey = null; foreach ($routeCache as $routeKey => $routeSource) { // Extract controller and method from route key (e.g., "App\Http\Controllers\PublisherController@destroy") @@ -95,11 +114,15 @@ public function boot(): void [$controllerClass, $method] = $parts; $controllerShortName = class_basename($controllerClass); - // Try to match with operationId patterns like "publisher.destroy" - $expectedOperationId = strtolower(str_replace('Controller', '', $controllerShortName)) . '.' . $method; + // Try to match with operationId patterns like "publisher.destroy" or "channelPublish.store" + // Scramble may use camelCase for multi-word controllers + $controllerBaseName = str_replace('Controller', '', $controllerShortName); + $expectedOperationId1 = strtolower($controllerBaseName) . '.' . $method; + $expectedOperationId2 = lcfirst($controllerBaseName) . '.' . $method; - if ($operationId === $expectedOperationId) { + if ($operationId === $expectedOperationId1 || $operationId === $expectedOperationId2) { $source = $routeSource; + $matchedRouteKey = $routeKey; break; } } @@ -108,6 +131,23 @@ public function boot(): void continue; } + // If the method has Scramble Response attributes, keep only those response codes + if ($matchedRouteKey && isset($routeScrambleResponseCodes[$matchedRouteKey])) { + $allowedCodes = $routeScrambleResponseCodes[$matchedRouteKey]; + // Remove any responses NOT in the attribute-defined codes + $operation->responses = collect($operation->responses) + ->filter(function($r) use ($allowedCodes) { + if (property_exists($r, 'code')) { + return in_array((int) $r->code, $allowedCodes, true); + } + return true; + }) + ->values() + ->all(); + // Skip adding responses from source code - attributes take precedence + continue; + } + // Check if source code contains response macro calls and add/replace corresponding responses foreach ($responseSchemas as $macroName => $responseInfo) { if (str_contains($source, "->$macroName(")) { diff --git a/resources/views/vendor/scramble/docs.blade.php b/resources/views/vendor/scramble/docs.blade.php index 1108382..6aaee67 100644 --- a/resources/views/vendor/scramble/docs.blade.php +++ b/resources/views/vendor/scramble/docs.blade.php @@ -47,7 +47,6 @@ // Add OAuth2 Bearer token if available if (window.oauth2Token) { - console.log('FRAPPA QUA', url); updateFetchHeaders(headers, 'Authorization', `Bearer ${window.oauth2Token}`); } @@ -165,7 +164,6 @@ function updateTheme(e) { // OAuth2 Panel JavaScript (function() { // Store the OAuth2 token globally - console.log('FRAPPA NULL'); window.oauth2Token = null; const getCookie = (name) => { @@ -258,11 +256,16 @@ function updateTheme(e) { // Function to update Stoplight Elements Authorization fields function updateStoplightAuthDisplay() { - if (!window.oauth2Token) return; + // Read token from textarea instead of window.oauth2Token + const tokenValue = document.getElementById('token-value').value.trim(); + if (!tokenValue) return; - const token = window.oauth2Token; + const token = tokenValue; const bearerToken = `Bearer ${token}`; + // Also update window.oauth2Token to keep it in sync + window.oauth2Token = token; + // Wait for Stoplight Elements to render setTimeout(() => { // Try multiple selectors to find authorization input fields @@ -305,7 +308,16 @@ function updateStoplightAuthDisplay() { } document.getElementById('use-token-btn').addEventListener('click', () => { - // Token is already stored in window.oauth2Token, which is used by fetch interceptor + // Read token from textarea and update both Stoplight and window.oauth2Token + const tokenValue = document.getElementById('token-value').value.trim(); + + if (!tokenValue) { + return; + } + + // Update window.oauth2Token from textarea value + window.oauth2Token = tokenValue; + const btn = document.getElementById('use-token-btn'); const originalText = btn.textContent; const originalBg = btn.style.background; @@ -313,7 +325,7 @@ function updateStoplightAuthDisplay() { btn.textContent = 'Token Applied!'; btn.style.background = '#059669'; - // Update Stoplight Elements display + // Update Stoplight Elements display (which now reads from textarea) updateStoplightAuthDisplay(); setTimeout(() => { @@ -361,6 +373,50 @@ function showCopyFeedback() { }, 2000); } + // Sync textarea token-value changes to window.oauth2Token + document.getElementById('token-value').addEventListener('input', (e) => { + const newToken = e.target.value.trim(); + if (newToken) { + window.oauth2Token = newToken; + } else { + window.oauth2Token = null; + } + }); + + // ====== REVERSE SYNC: Stoplight Elements Input → window.oauth2Token ====== + // Listen for changes in Stoplight-generated auth inputs and sync back to window.oauth2Token + (function setupStoplightTokenSync() { + // Use event delegation to handle dynamically created inputs + document.addEventListener('input', (e) => { + const target = e.target; + + // Check if this is a Stoplight auth input by: + // 1. ID pattern: id_auth_Token_* (dynamic suffix) + // 2. OR name/placeholder patterns for authorization + const isStoplightAuthInput = + (target.id && target.id.match(/^id_auth_Token_/)) || + (target.name && target.name.toLowerCase().includes('authorization')) || + (target.placeholder && ( + target.placeholder.includes('Bearer') || + target.placeholder.includes('Authorization') || + target.placeholder.toLowerCase().includes('token') + )); + + if (isStoplightAuthInput && target.tagName === 'INPUT') { + let newToken = target.value.trim(); + + // Remove "Bearer " prefix if user includes it + newToken = newToken.replace(/^Bearer\s+/i, ''); + + if (newToken) { + window.oauth2Token = newToken; + } else { + window.oauth2Token = null; + } + } + }, true); // Use capture phase to ensure early interception + })(); + // Allow Enter key to submit document.getElementById('client-id').addEventListener('keypress', (e) => { if (e.key === 'Enter') document.getElementById('get-token-btn').click(); From 9e0762a8297b34b52b2991ae6e009d56f58655f3 Mon Sep 17 00:00:00 2001 From: Michela Date: Fri, 10 Jul 2026 18:04:52 +0200 Subject: [PATCH 05/12] BZR-74: remove macroResponseProvider and remove swagger --- .env.example | 3 - app/Http/Controllers/ChannelController.php | 385 +--------------- .../Controllers/ChannelPublishController.php | 154 +------ .../ChannelSubscribeController.php | 167 +------ app/Http/Controllers/Controller.php | 166 +++---- app/Http/Controllers/FailedJobController.php | 206 +-------- app/Http/Controllers/PublisherController.php | 187 +------- app/Http/Controllers/SubscriberController.php | 256 +---------- .../ResponseMacroServiceProvider.php | 96 ---- app/Providers/ScrambleServiceProvider.php | 218 --------- composer.json | 4 +- composer.lock | 421 +----------------- config/app.php | 2 - config/l5-swagger.php | 310 ------------- tests/Feature/ScrambleTest.php | 14 + tests/Feature/SwaggerTest.php | 26 -- 16 files changed, 207 insertions(+), 2408 deletions(-) delete mode 100644 app/Providers/ResponseMacroServiceProvider.php delete mode 100644 config/l5-swagger.php create mode 100644 tests/Feature/ScrambleTest.php delete mode 100644 tests/Feature/SwaggerTest.php diff --git a/.env.example b/.env.example index 82c8408..f978f5d 100644 --- a/.env.example +++ b/.env.example @@ -55,10 +55,7 @@ TELESCOPE_GATE_WATCHER=true TELESCOPE_SCHEDULE_WATCHER=false TELESCOPE_VIEW_WATCHER=true -L5_SWAGGER_GENERATE_ALWAYS=true - # Scramble API Documentation -SCRAMBLE_ENABLED=true API_VERSION=1.0.0 CLIENT_ID_SENDY= diff --git a/app/Http/Controllers/ChannelController.php b/app/Http/Controllers/ChannelController.php index d164e88..6e16e0c 100644 --- a/app/Http/Controllers/ChannelController.php +++ b/app/Http/Controllers/ChannelController.php @@ -21,9 +21,9 @@ class ChannelController extends Controller { - protected $channelRepository; - protected $channelSubscribeRepository; - protected $publisherRepository; + protected ChannelRepository $channelRepository; + protected ChannelSubscribeRepository $channelSubscribeRepository; + protected PublisherRepository $publisherRepository; public function __construct(ChannelRepository $channelRepository, ChannelSubscribeRepository $ChannelSubscribeRepository, PublisherRepository $publisherRepository) { @@ -32,62 +32,6 @@ public function __construct(ChannelRepository $channelRepository, ChannelSubscri $this->channelSubscribeRepository = $ChannelSubscribeRepository; } - /** - * @OA\Get( - * path="/api/channels", - * summary="List of all channels", - * tags={"channels"}, - * security={{"passport":{}}}, - * description="Use to get the list of all channels", - * @OA\Parameter( - * name="q", - * in="query", - * description="values to filter returned data (name values)", - * required=false, - * @OA\Schema( - * type="string" - * ) - * ), - * @OA\Parameter( - * name="limit", - * in="query", - * description="maximum number of results to return", - * required=false, - * @OA\Schema( - * type="integer", - * format="int32", - * minimum=1 - * ) - * ), - * @OA\Parameter( - * name="order", - * in="query", - * description="type of order: ASC, DESC", - * required=false, - * @OA\Schema( - * type="string", - * ) - * ), - * @OA\Parameter( - * name="orderBy", - * in="query", - * description="field to order: id - name(default) - created_at - updated_at", - * required=false, - * @OA\Schema( - * type="string", - * ) - * ), - * @OA\Response( - * response=200, - * ref="#/components/responses/Success200" - * ), - * @OA\Response( - * response=500, - * ref="#/components/responses/Error500", - * ) - * - * ) - */ public function getList(Request $request) { $query = $request->input('q'); @@ -98,95 +42,16 @@ public function getList(Request $request) return ChannelResource::collection($retriviedChannels); } - - /** - * @OA\Get( - * path="/api/channels/{id}", - * summary="Find a channel by id", - * tags={"channels"}, - * security={{"passport":{}}}, - * description="Use to get a channel by id", - * operationId="channelController.getchannel", - * @OA\Parameter( - * in="path", - * required=true, - * description="channel id", - * name="id", - * @OA\Schema( - * type="integer", - * minimum=1 - * ) - * ), - * @OA\Response( - * response=200, - * ref="#/components/responses/Success200" - * ), - * @OA\Response( - * response=404, - * ref="#/components/responses/Error404" - * ) - * ) - * - * Response to route /api/channels/{id} - * - * @param int $id - * @return ChannelResource $channel - * - */ public function getChannel($id) { $channel = $this->channelRepository->find($id); if (!$channel) { - return response()->error404(__('messages.Channel') . $id); + return $this->error404(__('messages.Channel') . $id); } return new ChannelResource($channel); } - /** - * @OA\Post( - * path="/api/channels", - * summary="Save new channel", - * tags={"channels"}, - * security={{"passport":{}}}, - * description="Use to store a new channel", - * @OA\RequestBody( - * description="Channel object that needs to be created", - * @OA\MediaType( - * mediaType="application/json", - * @OA\Schema( - * schema="Channel", - * type="object", - * @OA\Property( - * property="name", - * type="string", - * example="channel1" - * ), - * @OA\Property( - * property="priority", - * type="string", - * example="default", - * enum={"high", "medium", "low", "default"}, - * ) - * ), - * ) - * ), - * @OA\Response( - * response=201, - * ref="#/components/responses/Success201", - * ), - * @OA\Response( - * response=500, - * ref="#/components/responses/Error500", - * ), - * @OA\Response( - * response=422, - * ref="#/components/responses/Error422", - * ) - * ) - */ - #[ScrambleResponse(status: 201, description: 'Channel created successfully')] - #[ScrambleResponse(status: 422, description: 'Validation error')] - #[ScrambleResponse(status: 500, description: 'Internal server error')] + #[ScrambleResponse(status: 201, description: 'Channel created successfully', type: 'array{message: string, channel: array{name: string, priority: string}}')] public function store(ChannelRequest $request) { $channel = $request->only(['name', 'priority']); @@ -194,58 +59,27 @@ public function store(ChannelRequest $request) try { $this->channelRepository->save((object) $channel); } catch (\Exception $e) { - return response()->error500(__('messages.SaveError') . ' ' . json_encode($channel)); + return $this->error500(__('messages.SaveError') . ' ' . json_encode($channel)); } - return response()->success201("Channel successfully saved", "channel", $channel); + return $this->success201("Channel successfully saved", "channel", $channel); } - /** - * @OA\Delete( - * path="/api/channels/{id}", - * summary="Delete the channel", - * tags={"channels"}, - * security={{"passport":{}}}, - * description="Delete channel and its relations", - * @OA\Parameter( - * in="path", - * required=true, - * description="channel id", - * name="id", - * @OA\Schema( - * type="integer", - * minimum=1 - * ) - * ), - * @OA\Response( - * response=500, - * ref="#/components/responses/Error500" - * ), - * @OA\Response( - * response=404, - * ref="#/components/responses/Error404" - * ), - * @OA\Response( - * response=200, - * ref="#/components/responses/Success200" - * ) - * ) - */ public function destroy(int $id) { $channel = $this->channelRepository->find($id); if (!$channel) { - return response()->error404(__('messages.Channel') . $id); + return $this->error404(__('messages.Channel') . $id); } Cache::forget(Config::get('cache.channel_key_prefix') . $channel->name); if (!$this->channelRepository->delete($channel)) { - return response()->error500(__('messages.DeleteError') . $channel); + return $this->error500(__('messages.DeleteError') . $channel->id); } - return response()->success200(__('messages.DeleteSuccess'), [ + return $this->success200(__('messages.DeleteSuccess'), [ 'action' => 'DELETE', // TO DO: add user id key value pair 'object_type' => 'channel', @@ -253,137 +87,24 @@ public function destroy(int $id) ]); } - /** - * @OA\Get( - * path="/api/channels/{id}/subscribers", - * summary="List of all subscribers of a channel", - * tags={"channels"}, - * security={{"passport":{}}}, - * description="Use to get the list of all the subscribers of a channel", - * operationId="ChannelController.getChannelSubscribers", - * @OA\Parameter( - * in="path", - * required=true, - * description="Channel id", - * name="id", - * @OA\Schema( - * type="integer", - * minimum=1 - * ) - * ), - * @OA\Response( - * response=200, - * ref="#/components/responses/Success200" - * ), - * @OA\Response( - * response=404, - * ref="#/components/responses/Error404" - * ) - * ) - * - * Response to route /api/channels/{id}/subscribers - * - * @param int $id - * @return $subscriber - * - */ - public function getChannelSubscribers($id) + public function getChannelSubscribers(int $id) { $channel = $this->channelRepository->find($id); if (!$channel) { - return response()->error404("Channel " . $id); + return $this->error404("Channel " . $id); } return ChannelSubscribeResource::collection($channel->registrations); } - /** - * @OA\Get( - * path="/api/channels/{id}/publishers", - * summary="List of all publishers of a channel", - * tags={"channels"}, - * security={{"passport":{}}}, - * description="Use to get the list of all the publishers of a channel", - * operationId="ChannelController.getChannelPublishers", - * @OA\Parameter( - * in="path", - * required=true, - * description="Channel id", - * name="id", - * @OA\Schema( - * type="integer", - * minimum=1 - * ) - * ), - * @OA\Response( - * response=200, - * ref="#/components/responses/Success200" - * ), - * @OA\Response( - * response=404, - * ref="#/components/responses/Error404" - * ) - * ) - * - * Response to route /api/channels/{id}/publishers - * - * @param int $id - * @return $publisher - * - */ - public function getChannelPublishers($id) + public function getChannelPublishers(int $id) { $channel = $this->channelRepository->find($id); if (!$channel) { - return response()->error404("Channel " . $id); + return $this->error404("Channel " . $id); } return PublisherResource::collection(($channel->publishers)->unique()); } - /** - * @OA\Post( - * path="/api/sendMessage", - * summary="Send a message", - * tags={"messages"}, - * description="Use to send messages to the subscribers of a channel", - * operationId="ChannelController.sendMessage", - * security = {{"basicAuth": {}}}, - * @OA\RequestBody( - * description="Message that needs to be sent and channel name", - * @OA\MediaType( - * mediaType="application/json", - * @OA\Schema( - * schema="SendMessage", - * type="object", - * required={"channel", "message"}, - * @OA\Property( - * property="channel", - * type="string", - * example="channel name" - * ), - * @OA\Property( - * property="message", - * type="string", - * example="message example" - * ), - * ), - * ) - * ), - * @OA\Response( - * response=200, - * ref="#/components/responses/Success200" - * ), - * @OA\Response( - * response=422, - * ref="#/components/responses/Error422" - * ) - * ) - * - * Response to route /api/sendMessage - * - * @param MessageChannelRequest $request - * @return Response - * - */ public function sendMessage(MessageChannelRequest $request) { $channel = $this->getCachedChannelByName($request->channel); @@ -391,79 +112,21 @@ public function sendMessage(MessageChannelRequest $request) return $this->sendMessageTo($request, $channel); } - /** - * @OA\Post( - * path="/api/sendMessage/{channelName}", - * summary="Send a message to Channel", - * tags={"messages"}, - * description="Use to send message to the subscribers of a channel", - * operationId="ChannelController.sendMessageToChannel", - * security = {{"basicAuth": {}}}, - * @OA\Parameter( - * in="path", - * required=true, - * description="channel name", - * name="channelName", - * @OA\Schema( - * type="string", - * ) - * ), - * @OA\RequestBody( - * description="Message that needs to be sent", - * @OA\MediaType( - * mediaType="application/json", - * @OA\Schema( - * schema="Message", - * type="object", - * required={"message"}, - * @OA\Property( - * property="message", - * type="string", - * example="message example" - * ), - * ) - * ) - * ), - * @OA\Response( - * response=200, - * ref="#/components/responses/Success200" - * ), - * @OA\Response( - * response=422, - * ref="#/components/responses/Error422" - * ) - * ) - * - * Response to route /api/sendMessage/{channelName} - * - * @param MessageRequest $request - * @param String $channelName - * @return Response - * - */ - public function sendMessageToChannel(MessageRequest $request, $channelName) + public function sendMessageToChannel(MessageRequest $request, string $channelName) { $channel = $this->getCachedChannelByName($channelName); if (!$channel) { - return response()->error404(__('messages.Channel')); + return $this->error404(__('messages.Channel')); } return $this->sendMessageTo($request, $channel); } - /** - * Manage SendMessage request - * - * @param Request $request - * @param mixed $channel - * @return Response - * - */ - private function sendMessageTo($request, $channel) + private function sendMessageTo(Request $request, $channel) { $publisher = Auth::guard('api')->user(); if (!$this->publisherRepository->hasChannel($publisher, $channel->id)) { - return response()->error403( + return $this->error403( "Not authorized to send message on requested channel", [ 'publisherId' => $publisher->id, @@ -479,18 +142,10 @@ private function sendMessageTo($request, $channel) event(new SendMessageEvent($message, $subscriber->host, $relation, $channel->priority, $channel?->name ?? "", $subscriber->name)); } } - return response()->json([ - "message" => "Message dispatched", - ], 200); + return $this->success200("Message dispatched"); } - /** - * Save or retrieve channel and its subscribers from cache - * - * @param string $channelName - * @return mixed|boolean - */ - private function getCachedChannelByName($channelName) + private function getCachedChannelByName(string $channelName) { $cacheKey = Config::get('cache.channel_key_prefix') . $channelName; diff --git a/app/Http/Controllers/ChannelPublishController.php b/app/Http/Controllers/ChannelPublishController.php index 1fcf64c..4341851 100644 --- a/app/Http/Controllers/ChannelPublishController.php +++ b/app/Http/Controllers/ChannelPublishController.php @@ -5,14 +5,15 @@ use App\Exceptions\DuplicateEntryException; use App\Http\Requests\ChannelPublishRequest; use App\Http\Repositories\PublisherRepository; +use App\Http\Repositories\ChannelPublishRepository; use App\Http\Repositories\RepositoryInterface; use App\Http\Resources\PublisherChannelResource; use Dedoc\Scramble\Attributes\Response as ScrambleResponse; class ChannelPublishController extends Controller { - protected $channelPublishRepository; - protected $publisherRepository; + protected ChannelPublishRepository $channelPublishRepository; + protected PublisherRepository $publisherRepository; public function __construct(RepositoryInterface $channelPublishRepository, PublisherRepository $publisherRepository) { @@ -20,62 +21,11 @@ public function __construct(RepositoryInterface $channelPublishRepository, Publi $this->publisherRepository = $publisherRepository; } - /** - * @OA\Post( - * path="/api/publishers/{publisher_id}/channels", - * summary="Save new publisher registration to a channel", - * tags={"publishers"}, - * security={{"passport":{}}}, - * description="Use to register a publisher to a channel", - * operationId="ChannelPublishController.store", - * @OA\Parameter( - * in="path", - * required=true, - * description="publisher id", - * name="publisher_id", - * @OA\Schema( - * type="integer", - * minimum=1 - * ) - * ), - * @OA\RequestBody( - * description="Registration object that needs to be created", - * @OA\MediaType( - * mediaType="application/json", - * @OA\Schema( - * schema="ChannelPublish", - * type="object", - * required={"channel_id"}, - * @OA\Property( - * property="channel_id", - * type="integer", - * example=1 - * ) - * ), - * ) - * ), - * @OA\Response( - * response=201, - * ref="#/components/responses/Success201", - * ), - * @OA\Response( - * response=500, - * ref="#/components/responses/Error500", - * ), - * @OA\Response( - * response=422, - * ref="#/components/responses/Error422", - * ) - * ) - */ - #[ScrambleResponse(status: 201, description: 'Publisher successfully registered to channel')] - #[ScrambleResponse(status: 409, description: 'Conflict - Subscription already exists')] - #[ScrambleResponse(status: 422, description: 'Validation error or publisher not found')] - #[ScrambleResponse(status: 500, description: 'Internal server error')] - public function store(ChannelPublishRequest $request, $id) + #[ScrambleResponse(status: 201, description: 'The publisher has been successfully registered to the channel', type: 'array{message: string, channelpublish: array{channel_id: int, publisher_id: int}}')] + public function store(ChannelPublishRequest $request, int $id) { if (!$this->publisherRepository->find($id)) { - return response()->error422('publisher_id', "Publisher with id = " . $id . " not Found"); + return $this->error422('publisher_id', "Publisher with id = " . $id . " not Found"); } $channelPublish = $request->only(['channel_id']); $channelPublish['publisher_id'] = $id; @@ -83,99 +33,23 @@ public function store(ChannelPublishRequest $request, $id) try { $this->channelPublishRepository->save((object) $channelPublish); } catch (DuplicateEntryException $e) { - return response()->error409("The subscription already exists", $channelPublish); + return $this->error409("The subscription already exists", $channelPublish); } catch (\Exception $e) { - return response()->error500(__('messages.SaveError') . ' ' . json_encode($channelPublish)); + return $this->error500(__('messages.SaveError') . ' ' . json_encode($channelPublish)); } - return response()->success201("The publisher has been successfully registered to the channel", "channelpublish", $channelPublish); + return $this->success201("The publisher has been successfully registered to the channel", "channelpublish", $channelPublish); } - /** - * @OA\Get( - * path="/api/publishers/{publisher_id}/channels", - * summary="Find a publisher registration by id", - * tags={"publishers"}, - * security={{"passport":{}}}, - * description="Use to get a publisher registration by id", - * operationId="ChannelPublishController.getChannelPublish", - * @OA\Parameter( - * in="path", - * required=true, - * description="Publisher id", - * name="publisher_id", - * @OA\Schema( - * type="integer", - * minimum=1 - * ) - * ), - * @OA\Response( - * response=200, - * ref="#/components/responses/Success200" - * ), - * @OA\Response( - * response=404, - * ref="#/components/responses/Error404" - * ) - * ) - * - * Response to route /api/publishers/{id}/channels - * - * @param int $id - * @return $channelPublish - * - */ - public function getChannelPublish($id) + public function getChannelPublish(int $id) { $publisher = $this->publisherRepository->find($id); if (!$publisher || count($publisher->registrations) == 0) { - return response()->error404(__('messages.channelPublish')); + return $this->error404(__('messages.channelPublish')); } return PublisherChannelResource::collection($publisher->registrations); } - /** - * @OA\Delete( - * path="/api/publishers/{publisher_id}/channels/{channel_id}", - * summary="Delete a publisher registration", - * tags={"publishers"}, - * security={{"passport":{}}}, - * description="Insert the publisher id and channel id that you want to delete", - * operationId="ChannelPublishController.destroy", - * @OA\Parameter( - * in="path", - * required=true, - * description="Publisher id", - * name="publisher_id", - * @OA\Schema( - * type="integer", - * minimum=1 - * ) - * ), - * @OA\Parameter( - * in="path", - * required=true, - * description="Channel id", - * name="channel_id", - * @OA\Schema( - * type="integer", - * minimum=1 - * ) - * ), - * @OA\Response( - * response=500, - * ref="#/components/responses/Error500" - * ), - * @OA\Response( - * response=404, - * ref="#/components/responses/Error404" - * ), - * @OA\Response( - * response=200, - * ref="#/components/responses/Success200" - * ) - * ) - */ public function destroy(int $publisher_id, int $channel_id) { $channelPublish = $this->channelPublishRepository->getByFilter( @@ -185,13 +59,13 @@ public function destroy(int $publisher_id, int $channel_id) ] ); if (!$channelPublish) { - return response()->error404(__('messages.channelPublish')); + return $this->error404(__('messages.channelPublish')); } if (!$this->channelPublishRepository->delete($channelPublish)) { - return response()->error500(__('messages.DeleteError') . $channelPublish); + return $this->error500(__('messages.DeleteError') . $channelPublish->id); } - return response()->success200(__('messages.DeleteSuccess'), [ + return $this->success200(__('messages.DeleteSuccess'), [ 'action' => 'DELETE', 'object_type' => 'ChannelPublish', 'object_id' => $channelPublish->id diff --git a/app/Http/Controllers/ChannelSubscribeController.php b/app/Http/Controllers/ChannelSubscribeController.php index 0959a68..e66e465 100644 --- a/app/Http/Controllers/ChannelSubscribeController.php +++ b/app/Http/Controllers/ChannelSubscribeController.php @@ -6,16 +6,18 @@ use Illuminate\Support\Facades\Config; use App\Exceptions\DuplicateEntryException; use App\Http\Repositories\ChannelRepository; +use App\Http\Repositories\ChannelSubscribeRepository; use App\Http\Repositories\RepositoryInterface; use App\Http\Requests\ChannelSubscribeRequest; use App\Http\Repositories\SubscriberRepository; use App\Http\Resources\SubscriberChannelResource; +use Dedoc\Scramble\Attributes\Response as ScrambleResponse; class ChannelSubscribeController extends Controller { - protected $channelSubscribeRepository; - protected $subscriberRepository; - protected $channelRepository; + protected ChannelSubscribeRepository $channelSubscribeRepository; + protected SubscriberRepository $subscriberRepository; + protected ChannelRepository $channelRepository; const CHANNEL_CACHE_KEY_PREFIX = "channel."; @@ -26,86 +28,15 @@ public function __construct(RepositoryInterface $channelSubscribeRepository, Sub $this->channelRepository = $channelRepository; } - /** - * @OA\Post( - * path="/api/subscribers/{subscriber_id}/channels", - * summary="Save new subscriber registration to a channel", - * tags={"subscribers"}, - * security={{"passport":{}}}, - * description="Use to register a subscriber to a channel", - * operationId="ChannelSubscribeController.store", - * @OA\Parameter( - * in="path", - * required=true, - * description="Subscribers id", - * name="subscriber_id", - * @OA\Schema( - * type="integer", - * minimum=1 - * ) - * ), - * @OA\RequestBody( - * description="Registration object that needs to be created", - * @OA\MediaType( - * mediaType="application/x-www-form-urlencoded", - * @OA\Schema( - * schema="ChannelSubscribe", - * type="object", - * required={"channel_id", "endpoint", "authentication"}, - * @OA\Property( - * property="channel_id", - * type="integer", - * example=1 - * ), - * @OA\Property( - * property="endpoint", - * type="string", - * example="api/test" - * ), - * @OA\Property( - * property="authentication", - * type="string", - * enum={"NONE", "OAUTH2", "BASIC"}, - * default="NONE" - * ), - * @OA\Property( - * property="username", - * type="string" - * ), - * @OA\Property( - * property="password", - * type="string", - * format="password" - * ) - * ), - * ) - * ), - * @OA\Response( - * response=201, - * ref="#/components/responses/Success201", - * ), - * @OA\Response( - * response=500, - * ref="#/components/responses/Error500", - * ), - * @OA\Response( - * response=409, - * ref="#/components/responses/Error409", - * ), - * @OA\Response( - * response=422, - * ref="#/components/responses/Error422", - * ) - * ) - */ + #[ScrambleResponse(status: 201, description: 'The subscriber has been successfully registered to the channel', type: 'array{message: string, channelsubscribe: array{channel_id: int, endpoint: string, authentication: string, username: string, password: string,subscriber_id: int }}')] public function store(ChannelSubscribeRequest $request, int $id) { if (!$this->subscriberRepository->find($id)) { - return response()->error422('subscriber_id', "Subscriber with id = " . $id . " not Found"); + return $this->error422('subscriber_id', "Subscriber with id = " . $id . " not Found"); } if (!$channel = $this->channelRepository->find($request->get("channel_id"))) { - return response()->error422('channel_id', "Channel with id = " . $request->get("channel_id") . " not found"); + return $this->error422('channel_id', "Channel with id = " . $request->get("channel_id") . " not found"); } $channelSubscribe = $request->only([ @@ -125,108 +56,42 @@ public function store(ChannelSubscribeRequest $request, int $id) try { $this->channelSubscribeRepository->save((object) $channelSubscribe); } catch (DuplicateEntryException $e) { - return response()->error409("The subscription already exists", $channelSubscribe); + return $this->error409("The subscription already exists", $channelSubscribe); } catch (\Exception $e) { - return response()->error500(__('messages.SaveError') . ' ' . json_encode($channelSubscribe)); + return $this->error500(__('messages.SaveError') . ' ' . json_encode($channelSubscribe)); } - return response()->success201("The subscriber has been successfully registered to the channel", "channelsubscribe", $channelSubscribe); + return $this->success201("The subscriber has been successfully registered to the channel", "channelsubscribe", $channelSubscribe); } - /** - * @OA\Get( - * path="/api/subscribers/{id}/channels", - * summary="Find a subscriber registration by id of subscribers", - * tags={"subscribers"}, - * security={{"passport":{}}}, - * description="Use to get a subscriber registrations by id", - * operationId="ChannelSubscribeController.getChannelSubscribe", - * @OA\Parameter( - * in="path", - * required=true, - * description="subscriber id", - * name="id", - * @OA\Schema( - * type="integer", - * minimum=1 - * ) - * ), - * @OA\Response( - * response=200, - * ref="#/components/responses/Success200" - * ), - * @OA\Response( - * response=404, - * ref="#/components/responses/Error404" - * ) - * ) - * - * Response to route /api/subscribers/{id}/channels - * - * @param int $id subscriber_id - * @return $channelSubscribe - * - */ - public function getChannelSubscribe($id) + public function getChannelSubscribe(int $id) { $subscriber = $this->subscriberRepository->find($id); if (!$subscriber || count($subscriber->registrations) == 0) { - return response()->error404(__('messages.subscriber') . $id); + return $this->error404(__('messages.subscriber') . $id); } return SubscriberChannelResource::collection($subscriber->registrations); } - /** - * @OA\Delete( - * path="/api/channel-subscriber/{id}", - * summary="Delete a subscriber registration", - * tags={"channel-subscriber"}, - * security={{"passport":{}}}, - * description="Insert the channel-subscriber relation id that you want to delete", - * operationId="ChannelSubscribeController.destroy", - * @OA\Parameter( - * in="path", - * required=true, - * description="channel-subscriber id", - * name="id", - * @OA\Schema( - * type="integer", - * minimum=1 - * ) - * ), - * @OA\Response( - * response=500, - * ref="#/components/responses/Error500" - * ), - * @OA\Response( - * response=404, - * ref="#/components/responses/Error404" - * ), - * @OA\Response( - * response=200, - * ref="#/components/responses/Success200" - * ) - * ) - */ public function destroy(int $id) { try { $channelSubscribe = $this->channelSubscribeRepository->find($id); if (!$channelSubscribe) { - return response()->error404(__('messages.channelSubscribe')); + return $this->error404(__('messages.channelSubscribe')); } Cache::forget(Config::get('cache.channel_key_prefix') . $channelSubscribe->channel->name); $this->channelSubscribeRepository->delete($channelSubscribe); - return response()->success200(__('messages.DeleteSuccess'), [ + return $this->success200(__('messages.DeleteSuccess'), [ 'action' => 'DELETE', 'object_type' => 'ChannelSubscribe', 'object_id' => $channelSubscribe->id ]); } catch (\Exception $e) { - return response()->error500(__('messages.DeleteError') . $channelSubscribe); + return $this->error500(__('messages.DeleteError') . $id); } } } diff --git a/app/Http/Controllers/Controller.php b/app/Http/Controllers/Controller.php index b42e59a..0382cf3 100644 --- a/app/Http/Controllers/Controller.php +++ b/app/Http/Controllers/Controller.php @@ -5,88 +5,98 @@ use Illuminate\Routing\Controller as BaseController; use Illuminate\Foundation\Validation\ValidatesRequests; use Illuminate\Foundation\Auth\Access\AuthorizesRequests; - -/** - *@OA\Info( - * version="1.0.0", - * title="Zanichelli API Buzzer projects", - * description="REST APIs to get channels info", - * @OA\Contact( - * name="Zanichelli DEV team", - * email="developers@zanichelli.it" - * ) - *), - * @OA\Components( - * @OA\Response( - * response="Success200", - * description="Operation successful", - * @OA\MediaType( - * mediaType="application/json") - * ), - * @OA\Response( - * response="Success201", - * description="Created", - * @OA\MediaType( - * mediaType="application/json") - * ), - * @OA\Response( - * response="Success204", - * description="No Content", - * @OA\MediaType( - * mediaType="application/json") - * ), - * @OA\Response( - * response="Error404", - * description="Not Found", - * @OA\MediaType( - * mediaType="application/json", - * @OA\Schema(ref="#/components/schemas/Message404") - * ) - * ), - * @OA\Response( - * response="Error409", - * description="Conflict", - * @OA\MediaType( - * mediaType="application/json") - * ), - * @OA\Response( - * response="Error422", - * description="Unprocessable entity: data validation error", - * @OA\MediaType( - * mediaType="application/json") - * ), - * @OA\Response( - * response="Error500", - * description="Internal Server Error", - * @OA\MediaType( - * mediaType="application/json", - * @OA\Schema(ref="#/components/schemas/Message500") - * ) - * ), - * @OA\Schema( - * schema="Message404", - * type="object", - * @OA\Property( - * property="message", - * type="string", - * default="Object not found" - * ) - * ), - * @OA\Schema( - * schema="Message500", - * type="object", - * @OA\Property( - * property="message", - * type="string", - * default="System error" - * ) - * ), - * ), - */ +use Illuminate\Support\Facades\Log; +use Illuminate\Support\Facades\Response; class Controller extends BaseController { use AuthorizesRequests, ValidatesRequests; const PAGINATION = 12; + + public function success200($value = "", $params = []) + { + $params['content'] = $value; + Log::info('200* ' . json_encode($params)); + if ($value) { + return Response::make(['message' => $value], 200); + } + return Response::make('', 200); + } + + public function success201($value, $type, $object) + { + $response = [ + 'message' => $value, + $type => $object + ]; + Log::info('201* ' . json_encode($response)); + return Response::make($response, 201); + } + + public function success204() + { + return Response::make('', 204); + } + + public function error401($value = '') + { + $message = $value; + Log::error('401* ' . json_encode(['content' => $message])); + return Response::make(['message' => $message], 401); + } + + public function error403($value = '', $details = []) + { + $message = ($value ? $value : __('messages.Unauthorized')); + Log::error('403* ' . json_encode([ + 'content' => $message, + 'details' => $details + ])); + return Response::make(['message' => $message], 403); + } + + public function error404($value = '') + { + $message = ($value ? $value : __('messages.Object')) . __('messages.NotFound'); + Log::error('404* ' . json_encode(['content' => $message])); + return Response::make(['message' => $message], 404); + } + + public function error409($value = '', $params = []) + { + $params['content'] = $value; + Log::error('409* ' . json_encode($params)); + return Response::make(['message' => $value], 409); + } + + public function error422($field, $error) + { + if (!$field) { + return Response::make( + [ + 'message' => 'Data is invalid', + 'errors' => $error + ], + 422 + ); + } + + return Response::make( + [ + 'message' => 'Data is invalid', + 'errors' => [ + $field => [$error] + ] + ], + 422 + ); + } + + public function error500($value = '') + { + $message = $value ? $value : __('messages.SystemError'); + Log::error('500* ' . json_encode(['content' => $message])); + return Response::make(['message' => $message], 500); + } } diff --git a/app/Http/Controllers/FailedJobController.php b/app/Http/Controllers/FailedJobController.php index ec9fe3e..dd01192 100644 --- a/app/Http/Controllers/FailedJobController.php +++ b/app/Http/Controllers/FailedJobController.php @@ -13,70 +13,13 @@ class FailedJobController extends Controller { - protected $failedJobRepository; + protected FailedJobRepository $failedJobRepository; public function __construct(FailedJobRepository $failedJobRepository) { $this->failedJobRepository = $failedJobRepository; - } - /** - * @OA\Get( - * path="/api/failedJobs", - * summary="List of all the failed jobs", - * tags={"jobs"}, - * security={{"passport":{}}}, - * description="Use to get the list of all failed jobs", - * @OA\Parameter( - * name="q", - * in="query", - * description="values to filter returned data (payload values)", - * required=false, - * @OA\Schema( - * type="string" - * ) - * ), - * @OA\Parameter( - * name="limit", - * in="query", - * description="maximum number of results to return", - * required=false, - * @OA\Schema( - * type="integer", - * format="int32", - * minimum=1 - * ) - * ), - * @OA\Parameter( - * name="order", - * in="query", - * description="type of order: ASC, DESC", - * required=false, - * @OA\Schema( - * type="string", - * ) - * ), - * @OA\Parameter( - * name="orderBy", - * in="query", - * description="field to order: id - name(default) - created_at - updated_at", - * required=false, - * @OA\Schema( - * type="string", - * ) - * ), - * @OA\Response( - * response=200, - * ref="#/components/responses/Success200" - * ), - * @OA\Response( - * response=500, - * ref="#/components/responses/Error500", - * ) - * - * ) - */ public function getList(Request $request) { $validator = Validator::make($request->all(), [ @@ -87,7 +30,7 @@ public function getList(Request $request) ]); if ($validator->fails()) { - return response()->error422(null, $validator->errors()); + return $this->error422(null, $validator->errors()); } $query = $request->input('q'); @@ -98,167 +41,66 @@ public function getList(Request $request) return FailedJobResource::collection($retriviedFailedJobs); } - /** - * @OA\Get( - * path="/api/failedJobs/retry/{id}", - * summary="Retry a failed jobs", - * tags={"jobs"}, - * security={{"passport":{}}}, - * description="Use to retry a failed job", - * operationId="failedJobController.retryJob", - * @OA\Parameter( - * in="path", - * required=true, - * description="id of the job that you want to retry", - * name="id", - * @OA\Schema( - * type="integer", - * minimum=1 - * ) - * ), - * @OA\Response( - * response=500, - * ref="#/components/responses/Error500" - * ), - * @OA\Response( - * response=200, - * ref="#/components/responses/Success200" - * ), - * @OA\Response( - * response=404, - * ref="#/components/responses/Error404" - * ) - * ) - */ - - public function retryJob($id) + public function retryJob(int $id) { - $failedJob= $this->failedJobRepository->find($id); - if(!$failedJob){ - return response()->error404(__('messages.FailedJob') . $id); + $failedJob = $this->failedJobRepository->find($id); + if (!$failedJob) { + return $this->error404(__('messages.FailedJob') . $id); } - try{ + try { $result = Artisan::call('queue:retry', ['id' => $id]); if ($result != 0) { - return response()->error500(__('messages.RetryError') . $failedJob); + return $this->error500(__('messages.RetryError') . $failedJob->id); } - } catch(Exception $e) { - return response()->error500(__('messages.RetryError') . $failedJob); + } catch (Exception $e) { + return $this->error500(__('messages.RetryError') . $failedJob->id); } - return response()->success200(__('messages.RetrySuccess'), [ + return $this->success200(__('messages.RetrySuccess'), [ 'action' => 'RETRY', 'object_type' => 'failedJob', 'object_id' => $id ]); } - /** - * @OA\Delete( - * path="/api/failedJobs/{id}", - * summary="Delete the failed Job", - * tags={"jobs"}, - * security={{"passport":{}}}, - * description="Insert the failed job id that you want to delete", - * operationId="FailedJobController.destroy", - * @OA\Parameter( - * in="path", - * required=true, - * description="id of the job that you want to delete", - * name="id", - * @OA\Schema( - * type="integer", - * minimum=1 - * ) - * ), - * @OA\Response( - * response=500, - * ref="#/components/responses/Error500" - * ), - * @OA\Response( - * response=404, - * ref="#/components/responses/Error404" - * ), - * @OA\Response( - * response=200, - * ref="#/components/responses/Success200" - * ) - * ) - */ public function destroy(int $id) { - $failedJob= $this->failedJobRepository->find($id); - if(!$failedJob){ - return response()->error404(__('messages.FailedJob') . $id); + $failedJob = $this->failedJobRepository->find($id); + if (!$failedJob) { + return $this->error404(__('messages.FailedJob') . $id); } - try{ + try { $result = Artisan::call('queue:forget', ['id' => $id]); - if($result != 0){ - return response()->error500(__('messages.DeleteError') . $failedJob); + if ($result != 0) { + return $this->error500(__('messages.DeleteError') . $failedJob->id); } - } catch(Exception $e) { - return response()->error500(__('messages.DeleteError') . $failedJob); + } catch (Exception $e) { + return $this->error500(__('messages.DeleteError') . $failedJob->id); } - return response()->success200(__('messages.DeleteSuccess'), [ + return $this->success200(__('messages.DeleteSuccess'), [ 'action' => 'DELETE', 'object_type' => 'failedJob', 'object_id' => $failedJob->id ]); } - /** - * @OA\Delete( - * path="/api/failedJobs/all", - * summary="Delete all the failed Jobs", - * tags={"jobs"}, - * security={{"passport":{}}}, - * description="Delete all jobs in failed_jobs table", - * operationId="FailedJobController.destroyAll", - * @OA\Response( - * response=500, - * ref="#/components/responses/Error500" - * ), - * @OA\Response( - * response=200, - * ref="#/components/responses/Success200" - * ) - * ) - */ public function destroyAll() { $result = Artisan::call('queue:flush'); if ($result != 0) { - return response()->error500(__('messages.DeleteError')); + return $this->error500(__('messages.DeleteError')); } - return response()->success200(__('messages.DeleteSuccess')); + return $this->success200(__('messages.DeleteSuccess')); } - /** - * @OA\Get( - * path="/api/failedJobs/retry/all", - * summary="Retry all the failed Jobs", - * tags={"jobs"}, - * security={{"passport":{}}}, - * description="Retry all jobs in failed_jobs table", - * operationId="FailedJobController.retryAll", - * @OA\Response( - * response=500, - * ref="#/components/responses/Error500" - * ), - * @OA\Response( - * response=200, - * ref="#/components/responses/Success200" - * ) - * ) - */ public function retryAll() { $result = Artisan::call('queue:lazy-retry'); if ($result != 0) { - return response()->error500(__('messages.RetryError') . $result); + return $this->error500(__('messages.RetryError') . $result); } - return response()->success200(__('messages.RetrySuccess')); + return $this->success200(__('messages.RetrySuccess')); } } diff --git a/app/Http/Controllers/PublisherController.php b/app/Http/Controllers/PublisherController.php index 877e4e9..bc86c28 100644 --- a/app/Http/Controllers/PublisherController.php +++ b/app/Http/Controllers/PublisherController.php @@ -2,71 +2,22 @@ namespace App\Http\Controllers; +use App\Http\Repositories\PublisherRepository; use Illuminate\Http\Request; use App\Http\Requests\PublisherRequest; use App\Http\Resources\PublisherResource; use App\Http\Repositories\RepositoryInterface; +use Dedoc\Scramble\Attributes\Response as ScrambleResponse; class PublisherController extends Controller { - protected $publisherRepository; + protected PublisherRepository $publisherRepository; public function __construct(RepositoryInterface $publisherRepository) { $this->publisherRepository = $publisherRepository; } - /** - * @OA\Get( - * path="/api/publishers", - * summary="List of all publishers", - * tags={"publishers"}, - * security={{"passport":{}}}, - * description="Use to get the list of all publishers", - * @OA\Parameter( - * name="q", - * in="query", - * description="values to filter returned data", - * required=false, - * @OA\Schema( - * type="string" - * ) - * ), - * @OA\Parameter( - * name="limit", - * in="query", - * description="maximum number of results to return", - * required=false, - * @OA\Schema( - * type="integer", - * format="int32" - * ) - * ), - * @OA\Parameter( - * name="order", - * in="query", - * description="type of order: ASC, DESC", - * required=false, - * @OA\Schema( - * type="string", - * ) - * ), - * @OA\Parameter( - * name="orderBy", - * in="query", - * description="field to order: id - name(default) - host - created_at - updated_at", - * required=false, - * @OA\Schema( - * type="string", - * ) - * ), - * @OA\Response( - * response=200, - * ref="#/components/responses/Success200" - * ) - * - * ) - */ public function getList(Request $request) { $query = $request->input('q'); @@ -77,57 +28,7 @@ public function getList(Request $request) return PublisherResource::collection($retriviedPublishers); } - /** - * @OA\Post( - * path="/api/publishers", - * summary="Save new publisher", - * tags={"publishers"}, - * security={{"passport":{}}}, - * description="Use to store a new publisher", - * @OA\RequestBody( - * description="Publisher object that needs to be created", - * @OA\MediaType( - * mediaType="application/json", - * @OA\Schema( - * schema="Publisher", - * type="object", - * @OA\Property( - * property="name", - * type="string", - * example="publisher1" - * ), - * @OA\Property( - * property="host", - * type="string", - * example="https://my-host.it" - * ), - * @OA\Property( - * property="username", - * type="string", - * example="username" - * ), - * @OA\Property( - * property="password", - * type="string", - * example="password" - * ), - * ), - * ) - * ), - * @OA\Response( - * response=201, - * ref="#/components/responses/Success201", - * ), - * @OA\Response( - * response=500, - * ref="#/components/responses/Error500", - * ), - * @OA\Response( - * response=422, - * ref="#/components/responses/Error422", - * ) - * ) - */ + #[ScrambleResponse(status: 201, description: 'Publisher successfully saved', type: 'array{message: string, publisher: PublisherResource}')] public function store(PublisherRequest $request) { $publisher = $request->only([ @@ -140,100 +41,34 @@ public function store(PublisherRequest $request) try { $this->publisherRepository->save((object) $publisher); } catch (\Exception $e) { - return response()->error500(__('messages.SaveError') . ' ' . json_encode($publisher)); + return $this->error500(__('messages.SaveError') . ' ' . json_encode($publisher)); } - return response()->success201("Publisher successfully saved", "publisher", $publisher); + return $this->success201("Publisher successfully saved", "publisher", $publisher); } - /** - * @OA\Delete( - * path="/api/publishers/{id}", - * summary="Delete the publisher", - * tags={"publishers"}, - * security={{"passport":{}}}, - * description="Insert the publisher id that you want to delete", - * operationId="PublisherController.destroy", - * @OA\Parameter( - * in="path", - * required=true, - * description="publisher id", - * name="id", - * @OA\Schema( - * type="integer", - * minimum=1 - * ) - * ), - * @OA\Response( - * response=500, - * ref="#/components/responses/Error500" - * ), - * @OA\Response( - * response=404, - * ref="#/components/responses/Error404" - * ), - * @OA\Response( - * response=200, - * ref="#/components/responses/Success200" - * ) - * ) - */ public function destroy(int $id) { $publisher = $this->publisherRepository->find($id); if (!$publisher) { - return response()->error404(__('messages.Publisher') . $id); + return $this->error404(__('messages.Publisher') . $id); } if (!$this->publisherRepository->delete($publisher)) { - return response()->error500(__('messages.DeleteError') . $publisher); + return $this->error500(__('messages.DeleteError') . $publisher->id); } - return response()->success200(__('messages.DeleteSuccess'), [ + return $this->success200(__('messages.DeleteSuccess'), [ 'action' => 'DELETE', 'object_type' => 'publisher', 'object_id' => $publisher->id ]); } - /** - * @OA\Get( - * path="/api/publishers/{id}", - * summary="Find a publisher by id", - * tags={"publishers"}, - * security={{"passport":{}}}, - * description="Use to get a publisher by id", - * operationId="PublisherController.getPublisher", - * @OA\Parameter( - * in="path", - * required=true, - * description="publisher id", - * name="id", - * @OA\Schema( - * type="integer", - * minimum=1 - * ) - * ), - * @OA\Response( - * response=200, - * ref="#/components/responses/Success200" - * ), - * @OA\Response( - * response=404, - * ref="#/components/responses/Error404" - * ) - * ) - * - * Response to route /api/publishers/{id} - * - * @param int $id - * @return $publisher - * - */ - public function getPublisher($id) + public function getPublisher(int $id) { $publisher = $this->publisherRepository->find($id); if (!$publisher) { - return response()->error404(__('messages.Publisher') . $id); + return $this->error404(__('messages.Publisher') . $id); } return new PublisherResource($publisher); } diff --git a/app/Http/Controllers/SubscriberController.php b/app/Http/Controllers/SubscriberController.php index f64bbdf..e277d66 100644 --- a/app/Http/Controllers/SubscriberController.php +++ b/app/Http/Controllers/SubscriberController.php @@ -6,69 +6,20 @@ use App\Http\Requests\SubscriberRequest; use App\Http\Resources\SubscriberResource; use App\Http\Repositories\RepositoryInterface; +use App\Http\Repositories\SubscriberRepository; use Illuminate\Support\Facades\Cache; use Illuminate\Support\Facades\Config; +use Dedoc\Scramble\Attributes\Response as ScrambleResponse; class SubscriberController extends Controller { - protected $subscriberRepository; + protected SubscriberRepository $subscriberRepository; public function __construct(RepositoryInterface $subscriberRepository) { $this->subscriberRepository = $subscriberRepository; } - /** - * @OA\Get( - * path="/api/subscribers", - * summary="List of all subscribers", - * tags={"subscribers"}, - * security={{"passport":{}}}, - * description="Use to get the list of all subscribers", - * @OA\Parameter( - * name="q", - * in="query", - * description="values to filter returned data", - * required=false, - * @OA\Schema( - * type="string" - * ) - * ), - * @OA\Parameter( - * name="limit", - * in="query", - * description="maximum number of results to return", - * required=false, - * @OA\Schema( - * type="integer", - * format="int32" - * ) - * ), - * @OA\Parameter( - * name="order", - * in="query", - * description="type of order: ASC, DESC", - * required=false, - * @OA\Schema( - * type="string", - * ) - * ), - * @OA\Parameter( - * name="orderBy", - * in="query", - * description="field to order: id - name(default) - host - created_at - updated_at", - * required=false, - * @OA\Schema( - * type="string", - * ) - * ), - * @OA\Response( - * response=200, - * ref="#/components/responses/Success200" - * ) - * - * ) - */ public function getList(Request $request) { $query = $request->input('q'); @@ -79,47 +30,7 @@ public function getList(Request $request) return SubscriberResource::collection($retriviedSubscribers); } - /** - * @OA\Post( - * path="/api/subscribers", - * summary="Save new subscriber", - * tags={"subscribers"}, - * security={{"passport":{}}}, - * description="Use to store a new subscriber", - * @OA\RequestBody( - * description="Subscriber object that needs to be created", - * @OA\MediaType( - * mediaType="application/json", - * @OA\Schema( - * schema="Subscriber", - * type="object", - * @OA\Property( - * property="name", - * type="string", - * example="subscriber1" - * ), - * @OA\Property( - * property="host", - * type="string", - * example="https://host1-test/" - * ) - * ), - * ) - * ), - * @OA\Response( - * response=201, - * ref="#/components/responses/Success201", - * ), - * @OA\Response( - * response=500, - * ref="#/components/responses/Error500", - * ), - * @OA\Response( - * response=422, - * ref="#/components/responses/Error422", - * ) - * ) - */ + #[ScrambleResponse(status: 201, description: 'Subscriber successfully saved', type: 'array{message: string, subscriber: SubscriberResource}')] public function store(SubscriberRequest $request) { $subscriber = $request->only([ @@ -131,60 +42,29 @@ public function store(SubscriberRequest $request) $subscriber['host'] = trim($subscriber['host'], '/') . '/'; if ($this->subscriberRepository->get($subscriber['host'])) { - return response()->error422('host', "Duplicated subscriber."); + return $this->error422('host', "Duplicated subscriber."); } try { $createdSubscriber = $this->subscriberRepository->save((object) $subscriber); } catch (\Exception $e) { - return response()->error500(__('messages.SaveError') . ' ' . json_encode($subscriber)); + return $this->error500(__('messages.SaveError') . ' ' . json_encode($subscriber)); } - return response()->success201("Subscriber successfully saved", "subscriber", $createdSubscriber); + return $this->success201("Subscriber successfully saved", "subscriber", $createdSubscriber); } - /** - * @OA\Delete( - * path="/api/subscribers/{id}", - * summary="Delete the subscriber", - * tags={"subscribers"}, - * security={{"passport":{}}}, - * description="Insert the subscriber id that you want to delete", - * @OA\Parameter( - * in="path", - * required=true, - * description="subscriber id", - * name="id", - * @OA\Schema( - * type="integer", - * minimum=1 - * ) - * ), - * @OA\Response( - * response=500, - * ref="#/components/responses/Error500" - * ), - * @OA\Response( - * response=404, - * ref="#/components/responses/Error404" - * ), - * @OA\Response( - * response=200, - * ref="#/components/responses/Success200" - * ) - * ) - */ public function destroy(int $id) { $subscriber = $this->subscriberRepository->find($id); if (!$subscriber) { - return response()->error404(__('messages.Subscriber') . $id); + return $this->error404(__('messages.Subscriber') . $id); } if (!$this->subscriberRepository->delete($subscriber)) { - return response()->error500(__('messages.DeleteError') . $subscriber); + return $this->error500(__('messages.DeleteError') . $subscriber->id); } - return response()->success200(__('messages.DeleteSuccess'), [ + return $this->success200(__('messages.DeleteSuccess'), [ 'action' => 'DELETE', // TO DO: add user id key value pair 'object_type' => 'subscriber', @@ -192,132 +72,32 @@ public function destroy(int $id) ]); } - /** - * @OA\Get( - * path="/api/subscribers/{id}", - * summary="Find a subscriber by id", - * tags={"subscribers"}, - * security={{"passport":{}}}, - * description="Use to get a subscriber by id", - * operationId="SubscriberController.getSubscriber", - * @OA\Parameter( - * in="path", - * required=true, - * description="Subscriber id", - * name="id", - * @OA\Schema( - * type="integer", - * minimum=1 - * ) - * ), - * @OA\Response( - * response=200, - * ref="#/components/responses/Success200" - * ), - * @OA\Response( - * response=404, - * ref="#/components/responses/Error404" - * ) - * ) - * - * Response to route /api/subscribers/{id} - * - * @param int $id - * @return $subscriber - * - */ - public function getSubscriber($id) + public function getSubscriber(int $id) { $subscriber = $this->subscriberRepository->find($id); if (!$subscriber) { - return response()->error404(__('messages.Subscriber') . $id); + return $this->error404(__('messages.Subscriber') . $id); } return new SubscriberResource($subscriber); } - /** - * @OA\Post( - * path="/api/subscribers/{id}/pause", - * summary="Pauses a subscriber from receiving messages", - * tags={"subscribers"}, - * security={{"passport":{}}}, - * description="Use to pauses a subscriber from receiving messages for 1 hour", - * operationId="SubscriberController.pauseSubscriber", - * @OA\Parameter( - * in="path", - * required=true, - * description="Subscriber id", - * name="id", - * @OA\Schema( - * type="integer", - * minimum=1 - * ) - * ), - * @OA\Response( - * response=200, - * ref="#/components/responses/Success200" - * ), - * @OA\Response( - * response=404, - * ref="#/components/responses/Error404" - * ) - * ) - * - * Response to route /api/subscribers/{id}/pause - * - * @param int $id - * - */ - public function pauseSubscriber($id) + public function pauseSubscriber(int $id) { $subscriber = $this->subscriberRepository->find($id); if (!$subscriber) { - return response()->error404(__('messages.Subscriber') . $id); + return $this->error404(__('messages.Subscriber') . $id); } Cache::put(config('cache.subscriber_paused_key_prefix') . $id, true, config('cache.subscriber_paused_ttl')); - return response()->success204(); + return $this->success204(); } - /** - * @OA\Post( - * path="/api/subscribers/{id}/restore", - * summary="Restore a subscriber from receiving messages", - * tags={"subscribers"}, - * security={{"passport":{}}}, - * description="Use to restore a subscriber from receiving messages", - * operationId="SubscriberController.restoreSubscriber", - * @OA\Parameter( - * in="path", - * required=true, - * description="Subscriber id", - * name="id", - * @OA\Schema( - * type="integer", - * minimum=1 - * ) - * ), - * @OA\Response( - * response=200, - * ref="#/components/responses/Success200" - * ), - * @OA\Response( - * response=404, - * ref="#/components/responses/Error404" - * ) - * ) - * - * Response to route /api/subscribers/{id}/restore - * - * @param int $id - * - */ - public function restoreSubscriber($id) + public function restoreSubscriber(int $id) { $subscriber = $this->subscriberRepository->find($id); if (!$subscriber) { - return response()->error404(__('messages.Subscriber') . $id); + return $this->error404(__('messages.Subscriber') . $id); } Cache::forget(Config::get('cache.subscriber_paused_key_prefix') . $id); - return response()->success204(); + return $this->success204(); } } diff --git a/app/Providers/ResponseMacroServiceProvider.php b/app/Providers/ResponseMacroServiceProvider.php deleted file mode 100644 index b2a3799..0000000 --- a/app/Providers/ResponseMacroServiceProvider.php +++ /dev/null @@ -1,96 +0,0 @@ - $value], 200); - } - return Response::make('', 200); - }); - - Response::macro('success201', function ($value, $type, $object) { - $response = [ - 'message' => $value, - $type => $object - ]; - Log::info('201* ' . json_encode($response)); - return Response::make($response, 201); - }); - - Response::macro('success204', function () { - return Response::make('', 204); - }); - - Response::macro('error401', function ($value = '') { - $message = $value; - Log::error('401* ' . json_encode(['content' => $message])); - return Response::make(['message' => $message], 401); - }); - - Response::macro('error403', function ($value = '', $details = []) { - $message = ($value ? $value : __('messages.Unauthorized')); - Log::error('403* ' . json_encode([ - 'content' => $message, - 'details' => $details - ])); - return Response::make(['message' => $message], 403); - }); - - Response::macro('error404', function ($value = '') { - $message = ($value ? $value : __('messages.Object')) . __('messages.NotFound'); - Log::error('404* ' . json_encode(['content' => $message])); - return Response::make(['message' => $message], 404); - }); - - Response::macro('error409', function ($value = "", $params = []) { - $params['content'] = $value; - Log::error('409* ' . json_encode($params)); - return Response::make(['message' => $value], 409); - }); - - Response::macro('error422', function ($field, $error) { - - if (!$field) { - return Response::make( - [ - 'message' => 'Data is invalid', - 'errors' => $error - ], - 422 - ); - } - - return Response::make( - [ - 'message' => 'Data is invalid', - 'errors' => [ - $field => [$error] - ] - ], - 422 - ); - }); - - Response::macro('error500', function ($value = '') { - $message = $value ? $value : __('messages.SystemError'); - Log::error('500* ' . json_encode(['content' => $message])); - return Response::make(['message' => $message], 500); - }); - } -} diff --git a/app/Providers/ScrambleServiceProvider.php b/app/Providers/ScrambleServiceProvider.php index c13ea56..a9146e4 100644 --- a/app/Providers/ScrambleServiceProvider.php +++ b/app/Providers/ScrambleServiceProvider.php @@ -4,185 +4,23 @@ use Dedoc\Scramble\Scramble; use Dedoc\Scramble\Support\Generator\OpenApi; -use Dedoc\Scramble\Support\Generator\Response; -use Dedoc\Scramble\Support\Generator\Schema; use Dedoc\Scramble\Support\Generator\SecurityRequirement; use Dedoc\Scramble\Support\Generator\SecurityScheme; use Dedoc\Scramble\Support\Generator\SecuritySchemes\OAuthFlow; -use Dedoc\Scramble\Support\Generator\Types\ObjectType; -use Dedoc\Scramble\Support\Generator\Types\StringType; -use Dedoc\Scramble\Support\Generator\Types\ArrayType; use Illuminate\Support\ServiceProvider; use Illuminate\Support\Facades\Route; -use Dedoc\Scramble\Attributes\Response as ScrambleResponse; class ScrambleServiceProvider extends ServiceProvider { public function boot(): void { Scramble::afterOpenApiGenerated(function (OpenApi $openApi) { - // Add security schemes to OpenAPI spec $this->addSecuritySchemes($openApi); - // Cache route information and check for Scramble Response attributes - $routeCache = []; - $routeScrambleResponseCodes = []; - foreach (Route::getRoutes() as $route) { - $action = $route->getAction(); - if (isset($action['controller'])) { - [$controller, $method] = explode('@', $action['controller']); - $key = $controller . '@' . $method; - - try { - $reflection = new \ReflectionMethod($controller, $method); - $filename = $reflection->getFileName(); - $startLine = $reflection->getStartLine(); - $endLine = $reflection->getEndLine(); - - if ($filename && $startLine && $endLine) { - $lines = file($filename); - $source = implode('', array_slice($lines, $startLine - 1, $endLine - $startLine + 1)); - $routeCache[$key] = $source; - } - - // Check if method has Scramble Response attributes and collect their status codes - $attributes = $reflection->getAttributes(ScrambleResponse::class); - if (!empty($attributes)) { - $statusCodes = []; - foreach ($attributes as $attribute) { - $instance = $attribute->newInstance(); - $statusCodes[] = (int) $instance->status; - } - $routeScrambleResponseCodes[$key] = $statusCodes; - } - } catch (\Throwable $e) { - // Skip if reflection fails - } - } - } - - // Define response schemas using Type objects - $responseSchemas = [ - 'error404' => [ - 'code' => 404, - 'description' => 'Resource not found', - 'schema' => $this->createErrorSchema('Resource not found'), - ], - 'error409' => [ - 'code' => 409, - 'description' => 'Conflict - Resource already exists', - 'schema' => $this->createErrorSchema('The resource already exists'), - ], - 'error422' => [ - 'code' => 422, - 'description' => 'Validation error', - 'schema' => $this->createValidationErrorSchema(), - ], - 'error500' => [ - 'code' => 500, - 'description' => 'Internal server error', - 'schema' => $this->createErrorSchema('An error occurred while processing your request'), - ], - 'success200' => [ - 'code' => 200, - 'description' => 'Success', - 'schema' => $this->createSuccessSchema('Operation completed successfully'), - ], - 'success201' => [ - 'code' => 201, - 'description' => 'Resource created', - 'schema' => $this->createSuccessSchema('Resource created successfully'), - ], - ]; - - // Add responses to operations based on their source code - foreach ($openApi->paths as $pathKey => $path) { - foreach ($path->operations as $operation) { - $existingCodes = collect($operation->responses)->pluck('code')->toArray(); - - // Try to find matching route source code - $source = null; - $operationId = $operation->operationId ?? ''; - $matchedRouteKey = null; - - foreach ($routeCache as $routeKey => $routeSource) { - // Extract controller and method from route key (e.g., "App\Http\Controllers\PublisherController@destroy") - $parts = explode('@', $routeKey); - if (count($parts) !== 2) { - continue; - } - - [$controllerClass, $method] = $parts; - $controllerShortName = class_basename($controllerClass); - - // Try to match with operationId patterns like "publisher.destroy" or "channelPublish.store" - // Scramble may use camelCase for multi-word controllers - $controllerBaseName = str_replace('Controller', '', $controllerShortName); - $expectedOperationId1 = strtolower($controllerBaseName) . '.' . $method; - $expectedOperationId2 = lcfirst($controllerBaseName) . '.' . $method; - - if ($operationId === $expectedOperationId1 || $operationId === $expectedOperationId2) { - $source = $routeSource; - $matchedRouteKey = $routeKey; - break; - } - } - - if (!$source) { - continue; - } - - // If the method has Scramble Response attributes, keep only those response codes - if ($matchedRouteKey && isset($routeScrambleResponseCodes[$matchedRouteKey])) { - $allowedCodes = $routeScrambleResponseCodes[$matchedRouteKey]; - // Remove any responses NOT in the attribute-defined codes - $operation->responses = collect($operation->responses) - ->filter(function($r) use ($allowedCodes) { - if (property_exists($r, 'code')) { - return in_array((int) $r->code, $allowedCodes, true); - } - return true; - }) - ->values() - ->all(); - // Skip adding responses from source code - attributes take precedence - continue; - } - - // Check if source code contains response macro calls and add/replace corresponding responses - foreach ($responseSchemas as $macroName => $responseInfo) { - if (str_contains($source, "->$macroName(")) { - // Remove existing response with same code if present - // Handle both Response objects and Reference objects - $operation->responses = collect($operation->responses) - ->reject(function($r) use ($responseInfo) { - // Check if it's a Response object with a code property - if (property_exists($r, 'code')) { - return $r->code === $responseInfo['code']; - } - return false; - }) - ->values() - ->all(); - - // Add the correct response - $response = Response::make($responseInfo['code']) - ->description($responseInfo['description']) - ->setContent('application/json', $responseInfo['schema']); - - $operation->addResponse($response); - } - } - } - } - - // Apply security requirements based on route middleware $routeMiddlewareMap = []; foreach (Route::getRoutes() as $route) { - // Only process API routes if (!str_starts_with($route->uri(), 'api/')) { continue; } - $action = $route->getAction(); if (isset($action['controller'])) { $parts = explode('@', $action['controller']); @@ -190,82 +28,27 @@ public function boot(): void [$controller, $method] = $parts; $controllerShortName = class_basename($controller); $operationId = strtolower(str_replace('Controller', '', $controllerShortName)) . '.' . $method; - - // Get all middleware for this route $middlewares = $route->gatherMiddleware(); $routeMiddlewareMap[$operationId] = $middlewares; } } } - // Apply security schemes to operations based on their middleware foreach ($openApi->paths as $path) { foreach ($path->operations as $operation) { $operationId = $operation->operationId ?? ''; $middlewares = $routeMiddlewareMap[$operationId] ?? []; - // Apply appropriate security scheme based on middleware if (in_array('client', $middlewares)) { - // OAuth2 client credentials for admin API routes $operation->security = [new SecurityRequirement(['passport' => []])]; } elseif (in_array('basicAuth', $middlewares)) { - // HTTP Basic Auth for publisher message routes $operation->security = [new SecurityRequirement(['basicAuth' => []])]; } - // Routes without these middleware will have no security requirement } } }); } - /** - * Create a simple error response schema - */ - private function createErrorSchema(string $exampleMessage): Schema - { - $objectType = new ObjectType(); - $messageType = (new StringType())->example($exampleMessage); - $objectType->addProperty('message', $messageType); - $objectType->setRequired(['message']); - - return Schema::fromType($objectType); - } - - /** - * Create a simple success response schema - */ - private function createSuccessSchema(string $exampleMessage): Schema - { - $objectType = new ObjectType(); - $messageType = (new StringType())->example($exampleMessage); - $objectType->addProperty('message', $messageType); - - return Schema::fromType($objectType); - } - - /** - * Create a validation error response schema - */ - private function createValidationErrorSchema(): Schema - { - $objectType = new ObjectType(); - - $messageType = (new StringType())->example('Data is invalid'); - $objectType->addProperty('message', $messageType); - - // errors is an object with arbitrary string array properties - $errorsObjectType = new ObjectType(); - $errorsObjectType->additionalProperties = new ArrayType(); - $objectType->addProperty('errors', $errorsObjectType); - - $objectType->setRequired(['message', 'errors']); - - return Schema::fromType($objectType); - } - - /** - * Add security schemes to the OpenAPI specification - */ private function addSecuritySchemes(OpenApi $openApi): void { // Add OAuth2 (Laravel Passport) security scheme @@ -281,7 +64,6 @@ private function addSecuritySchemes(OpenApi $openApi): void $openApi->components->addSecurityScheme('passport', $oauth2Scheme); - // Add HTTP Basic Auth security scheme $basicAuthScheme = SecurityScheme::http('basic') ->as('basicAuth') ->setDescription('Basic HTTP authentication for publishers sending messages'); diff --git a/composer.json b/composer.json index 0307ebb..98cef9b 100644 --- a/composer.json +++ b/composer.json @@ -9,7 +9,6 @@ "license": "MIT", "require": { "php": "^8.2", - "darkaonline/l5-swagger": "^8.3", "dedoc/scramble": "^0.13.26", "laravel/framework": "^12.0", "laravel/legacy-factories": "^1.1", @@ -18,8 +17,7 @@ "laravel/tinker": "^2.0", "laravel/ui": "^4.0", "zanichelli/healthcheck": "^1.1", - "zanichelli/idp-extensions": "^3.8", - "zircote/swagger-php": "4.*" + "zanichelli/idp-extensions": "^3.8" }, "require-dev": { "beyondcode/laravel-dump-server": "^2.1", diff --git a/composer.lock b/composer.lock index c92a0d5..beaef43 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "c76827acfde3850b27da33b314df0424", + "content-hash": "5dfa88acfe012dfa74afdc0ff89e7ee2", "packages": [ { "name": "aws/aws-crt-php", @@ -286,86 +286,6 @@ ], "time": "2024-02-09T16:56:22+00:00" }, - { - "name": "darkaonline/l5-swagger", - "version": "8.6.5", - "source": { - "type": "git", - "url": "https://github.com/DarkaOnLine/L5-Swagger.git", - "reference": "4cf2b3faae9e9cffd05e4eb6e066741bf56f0a85" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/DarkaOnLine/L5-Swagger/zipball/4cf2b3faae9e9cffd05e4eb6e066741bf56f0a85", - "reference": "4cf2b3faae9e9cffd05e4eb6e066741bf56f0a85", - "shasum": "" - }, - "require": { - "doctrine/annotations": "^1.0 || ^2.0", - "ext-json": "*", - "laravel/framework": "^11.0 || ^10.0 || ^9.0 || >=8.40.0 || ^7.0", - "php": "^7.2 || ^8.0", - "swagger-api/swagger-ui": "^3.0 || >=4.1.3", - "symfony/yaml": "^5.0 || ^6.0 || ^7.0", - "zircote/swagger-php": "^3.2.0 || ^4.0.0" - }, - "require-dev": { - "mockery/mockery": "1.*", - "orchestra/testbench": "^9.0 || ^8.0 || 7.* || ^6.15 || 5.*", - "php-coveralls/php-coveralls": "^2.0", - "phpunit/phpunit": "^11.0 || ^10.0 || ^9.5" - }, - "type": "library", - "extra": { - "laravel": { - "aliases": { - "L5Swagger": "L5Swagger\\L5SwaggerFacade" - }, - "providers": [ - "L5Swagger\\L5SwaggerServiceProvider" - ] - } - }, - "autoload": { - "files": [ - "src/helpers.php" - ], - "psr-4": { - "L5Swagger\\": "src" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Darius Matulionis", - "email": "darius@matulionis.lt" - } - ], - "description": "OpenApi or Swagger integration to Laravel", - "keywords": [ - "api", - "documentation", - "laravel", - "openapi", - "specification", - "swagger", - "ui" - ], - "support": { - "issues": "https://github.com/DarkaOnLine/L5-Swagger/issues", - "source": "https://github.com/DarkaOnLine/L5-Swagger/tree/8.6.5" - }, - "funding": [ - { - "url": "https://github.com/DarkaOnLine", - "type": "github" - } - ], - "time": "2025-02-06T14:54:32+00:00" - }, { "name": "dedoc/scramble", "version": "v0.13.26", @@ -588,82 +508,6 @@ }, "time": "2024-07-08T12:26:09+00:00" }, - { - "name": "doctrine/annotations", - "version": "2.0.2", - "source": { - "type": "git", - "url": "https://github.com/doctrine/annotations.git", - "reference": "901c2ee5d26eb64ff43c47976e114bf00843acf7" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/doctrine/annotations/zipball/901c2ee5d26eb64ff43c47976e114bf00843acf7", - "reference": "901c2ee5d26eb64ff43c47976e114bf00843acf7", - "shasum": "" - }, - "require": { - "doctrine/lexer": "^2 || ^3", - "ext-tokenizer": "*", - "php": "^7.2 || ^8.0", - "psr/cache": "^1 || ^2 || ^3" - }, - "require-dev": { - "doctrine/cache": "^2.0", - "doctrine/coding-standard": "^10", - "phpstan/phpstan": "^1.10.28", - "phpunit/phpunit": "^7.5 || ^8.5 || ^9.5", - "symfony/cache": "^5.4 || ^6.4 || ^7", - "vimeo/psalm": "^4.30 || ^5.14" - }, - "suggest": { - "php": "PHP 8.0 or higher comes with attributes, a native replacement for annotations" - }, - "type": "library", - "autoload": { - "psr-4": { - "Doctrine\\Common\\Annotations\\": "lib/Doctrine/Common/Annotations" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Guilherme Blanco", - "email": "guilhermeblanco@gmail.com" - }, - { - "name": "Roman Borschel", - "email": "roman@code-factory.org" - }, - { - "name": "Benjamin Eberlei", - "email": "kontakt@beberlei.de" - }, - { - "name": "Jonathan Wage", - "email": "jonwage@gmail.com" - }, - { - "name": "Johannes Schmitt", - "email": "schmittjoh@gmail.com" - } - ], - "description": "Docblock Annotations Parser", - "homepage": "https://www.doctrine-project.org/projects/annotations.html", - "keywords": [ - "annotations", - "docblock", - "parser" - ], - "support": { - "issues": "https://github.com/doctrine/annotations/issues", - "source": "https://github.com/doctrine/annotations/tree/2.0.2" - }, - "time": "2024-09-05T10:17:24+00:00" - }, { "name": "doctrine/inflector", "version": "2.0.10", @@ -4176,55 +4020,6 @@ }, "time": "2026-01-25T14:56:51+00:00" }, - { - "name": "psr/cache", - "version": "3.0.0", - "source": { - "type": "git", - "url": "https://github.com/php-fig/cache.git", - "reference": "aa5030cfa5405eccfdcb1083ce040c2cb8d253bf" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/php-fig/cache/zipball/aa5030cfa5405eccfdcb1083ce040c2cb8d253bf", - "reference": "aa5030cfa5405eccfdcb1083ce040c2cb8d253bf", - "shasum": "" - }, - "require": { - "php": ">=8.0.0" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.0.x-dev" - } - }, - "autoload": { - "psr-4": { - "Psr\\Cache\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "PHP-FIG", - "homepage": "https://www.php-fig.org/" - } - ], - "description": "Common interface for caching libraries", - "keywords": [ - "cache", - "psr", - "psr-6" - ], - "support": { - "source": "https://github.com/php-fig/cache/tree/3.0.0" - }, - "time": "2021-02-03T23:26:27+00:00" - }, { "name": "psr/clock", "version": "1.0.0", @@ -4989,67 +4784,6 @@ ], "time": "2026-05-19T14:06:37+00:00" }, - { - "name": "swagger-api/swagger-ui", - "version": "v5.21.0", - "source": { - "type": "git", - "url": "https://github.com/swagger-api/swagger-ui.git", - "reference": "fceaec605072fbc717a04895bd19814d9a1c8e6d" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/swagger-api/swagger-ui/zipball/fceaec605072fbc717a04895bd19814d9a1c8e6d", - "reference": "fceaec605072fbc717a04895bd19814d9a1c8e6d", - "shasum": "" - }, - "type": "library", - "notification-url": "https://packagist.org/downloads/", - "license": [ - "Apache-2.0" - ], - "authors": [ - { - "name": "Anna Bodnia", - "email": "anna.bodnia@gmail.com" - }, - { - "name": "Buu Nguyen", - "email": "buunguyen@gmail.com" - }, - { - "name": "Josh Ponelat", - "email": "jponelat@gmail.com" - }, - { - "name": "Kyle Shockey", - "email": "kyleshockey1@gmail.com" - }, - { - "name": "Robert Barnwell", - "email": "robert@robertismy.name" - }, - { - "name": "Sahar Jafari", - "email": "shr.jafari@gmail.com" - } - ], - "description": " Swagger UI is a collection of HTML, Javascript, and CSS assets that dynamically generate beautiful documentation from a Swagger-compliant API.", - "homepage": "http://swagger.io", - "keywords": [ - "api", - "documentation", - "openapi", - "specification", - "swagger", - "ui" - ], - "support": { - "issues": "https://github.com/swagger-api/swagger-ui/issues", - "source": "https://github.com/swagger-api/swagger-ui/tree/v5.21.0" - }, - "time": "2025-04-13T19:37:38+00:00" - }, { "name": "symfony/clock", "version": "v7.2.0", @@ -7362,78 +7096,6 @@ ], "time": "2025-04-09T08:14:01+00:00" }, - { - "name": "symfony/yaml", - "version": "v7.2.6", - "source": { - "type": "git", - "url": "https://github.com/symfony/yaml.git", - "reference": "0feafffb843860624ddfd13478f481f4c3cd8b23" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/yaml/zipball/0feafffb843860624ddfd13478f481f4c3cd8b23", - "reference": "0feafffb843860624ddfd13478f481f4c3cd8b23", - "shasum": "" - }, - "require": { - "php": ">=8.2", - "symfony/deprecation-contracts": "^2.5|^3.0", - "symfony/polyfill-ctype": "^1.8" - }, - "conflict": { - "symfony/console": "<6.4" - }, - "require-dev": { - "symfony/console": "^6.4|^7.0" - }, - "bin": [ - "Resources/bin/yaml-lint" - ], - "type": "library", - "autoload": { - "psr-4": { - "Symfony\\Component\\Yaml\\": "" - }, - "exclude-from-classmap": [ - "/Tests/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Fabien Potencier", - "email": "fabien@symfony.com" - }, - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" - } - ], - "description": "Loads and dumps YAML files", - "homepage": "https://symfony.com", - "support": { - "source": "https://github.com/symfony/yaml/tree/v7.2.6" - }, - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], - "time": "2025-04-04T10:10:11+00:00" - }, { "name": "tijsverkoyen/css-to-inline-styles", "version": "v2.3.0", @@ -7810,87 +7472,6 @@ "source": "https://github.com/ZanichelliEditore/idp-extension/tree/v3.8.0" }, "time": "2025-05-06T12:44:57+00:00" - }, - { - "name": "zircote/swagger-php", - "version": "4.11.1", - "source": { - "type": "git", - "url": "https://github.com/zircote/swagger-php.git", - "reference": "7df10e8ec47db07c031db317a25bef962b4e5de1" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/zircote/swagger-php/zipball/7df10e8ec47db07c031db317a25bef962b4e5de1", - "reference": "7df10e8ec47db07c031db317a25bef962b4e5de1", - "shasum": "" - }, - "require": { - "ext-json": "*", - "php": ">=7.2", - "psr/log": "^1.1 || ^2.0 || ^3.0", - "symfony/deprecation-contracts": "^2 || ^3", - "symfony/finder": ">=2.2", - "symfony/yaml": ">=3.3" - }, - "require-dev": { - "composer/package-versions-deprecated": "^1.11", - "doctrine/annotations": "^1.7 || ^2.0", - "friendsofphp/php-cs-fixer": "^2.17 || 3.62.0", - "phpstan/phpstan": "^1.6", - "phpunit/phpunit": ">=8", - "vimeo/psalm": "^4.23" - }, - "suggest": { - "doctrine/annotations": "^1.7 || ^2.0" - }, - "bin": [ - "bin/openapi" - ], - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "4.x-dev" - } - }, - "autoload": { - "psr-4": { - "OpenApi\\": "src" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "Apache-2.0" - ], - "authors": [ - { - "name": "Robert Allen", - "email": "zircote@gmail.com" - }, - { - "name": "Bob Fanger", - "email": "bfanger@gmail.com", - "homepage": "https://bfanger.nl" - }, - { - "name": "Martin Rademacher", - "email": "mano@radebatz.net", - "homepage": "https://radebatz.net" - } - ], - "description": "swagger-php - Generate interactive documentation for your RESTful API using phpdoc annotations", - "homepage": "https://github.com/zircote/swagger-php/", - "keywords": [ - "api", - "json", - "rest", - "service discovery" - ], - "support": { - "issues": "https://github.com/zircote/swagger-php/issues", - "source": "https://github.com/zircote/swagger-php/tree/4.11.1" - }, - "time": "2024-10-15T19:20:02+00:00" } ], "packages-dev": [ diff --git a/config/app.php b/config/app.php index 1854482..22383b5 100644 --- a/config/app.php +++ b/config/app.php @@ -161,7 +161,6 @@ Illuminate\Translation\TranslationServiceProvider::class, Illuminate\Validation\ValidationServiceProvider::class, Illuminate\View\ViewServiceProvider::class, - L5Swagger\L5SwaggerServiceProvider::class, /* * Package Service Providers... @@ -176,7 +175,6 @@ App\Providers\EventServiceProvider::class, App\Providers\TelescopeServiceProvider::class, App\Providers\RouteServiceProvider::class, - App\Providers\ResponseMacroServiceProvider::class, App\Providers\ScrambleServiceProvider::class, ], diff --git a/config/l5-swagger.php b/config/l5-swagger.php deleted file mode 100644 index fe4aeea..0000000 --- a/config/l5-swagger.php +++ /dev/null @@ -1,310 +0,0 @@ - 'default', - 'documentations' => [ - 'default' => [ - 'api' => [ - 'title' => 'L5 Swagger UI', - ], - - 'routes' => [ - /* - * Route for accessing api documentation interface - */ - 'api' => 'api/documentation', - ], - 'paths' => [ - /* - * Edit to include full URL in ui for assets - */ - 'use_absolute_path' => env('L5_SWAGGER_USE_ABSOLUTE_PATH', true), - - /* - * File name of the generated json documentation file - */ - 'docs_json' => 'api-docs.json', - - /* - * File name of the generated YAML documentation file - */ - 'docs_yaml' => 'api-docs.yaml', - - /* - * Set this to `json` or `yaml` to determine which documentation file to use in UI - */ - 'format_to_use_for_docs' => env('L5_FORMAT_TO_USE_FOR_DOCS', 'json'), - - /* - * Absolute paths to directory containing the swagger annotations are stored. - */ - 'annotations' => [ - base_path('app'), - ], - - ], - ], - ], - 'defaults' => [ - 'routes' => [ - /* - * Route for accessing parsed swagger annotations. - */ - 'docs' => 'docs', - - /* - * Route for Oauth2 authentication callback. - */ - 'oauth2_callback' => 'api/oauth2-callback', - - /* - * Middleware allows to prevent unexpected access to API documentation - */ - 'middleware' => [ - 'api' => [], - 'asset' => [], - 'docs' => [], - 'oauth2_callback' => [], - ], - - /* - * Route Group options - */ - 'group_options' => [], - ], - - 'paths' => [ - /* - * Absolute path to location where parsed annotations will be stored - */ - 'docs' => storage_path('api-docs'), - - /* - * Absolute path to directory where to export views - */ - 'views' => base_path('resources/views/vendor/l5-swagger'), - - /* - * Edit to set the api's base path - */ - 'base' => env('L5_SWAGGER_BASE_PATH', null), - - /* - * Edit to set path where swagger ui assets should be stored - */ - 'swagger_ui_assets_path' => env('L5_SWAGGER_UI_ASSETS_PATH', 'vendor/swagger-api/swagger-ui/dist/'), - - /* - * Absolute path to directories that should be exclude from scanning - * @deprecated Please use `scanOptions.exclude` - * `scanOptions.exclude` overwrites this - */ - 'excludes' => [], - ], - - 'scanOptions' => [ - /** - * analyser: defaults to \OpenApi\StaticAnalyser . - * - * @see \OpenApi\scan - */ - 'analyser' => null, - - /** - * analysis: defaults to a new \OpenApi\Analysis . - * - * @see \OpenApi\scan - */ - 'analysis' => null, - - /** - * Custom query path processors classes. - * - * @link https://github.com/zircote/swagger-php/tree/master/Examples/schema-query-parameter-processor - * @see \OpenApi\scan - */ - 'processors' => [ - // new \App\SwaggerProcessors\SchemaQueryParameter(), - ], - - /** - * pattern: string $pattern File pattern(s) to scan (default: *.php) . - * - * @see \OpenApi\scan - */ - 'pattern' => null, - - /* - * Absolute path to directories that should be exclude from scanning - * @note This option overwrites `paths.excludes` - * @see \OpenApi\scan - */ - 'exclude' => [], - - /* - * Allows to generate specs either for OpenAPI 3.0.0 or OpenAPI 3.1.0. - * By default the spec will be in version 3.0.0 - */ - 'open_api_spec_version' => env('L5_SWAGGER_OPEN_API_SPEC_VERSION', \L5Swagger\Generator::OPEN_API_DEFAULT_SPEC_VERSION), - ], - - /* - * API security definitions. Will be generated into documentation file. - */ - 'securityDefinitions' => [ - 'securitySchemes' => [ - /* - * Examples of Security schemes - */ - /* - 'api_key_security_example' => [ // Unique name of security - 'type' => 'apiKey', // The type of the security scheme. Valid values are "basic", "apiKey" or "oauth2". - 'description' => 'A short description for security scheme', - 'name' => 'api_key', // The name of the header or query parameter to be used. - 'in' => 'header', // The location of the API key. Valid values are "query" or "header". - ], - 'oauth2_security_example' => [ // Unique name of security - 'type' => 'oauth2', // The type of the security scheme. Valid values are "basic", "apiKey" or "oauth2". - 'description' => 'A short description for oauth2 security scheme.', - 'flow' => 'implicit', // The flow used by the OAuth2 security scheme. Valid values are "implicit", "password", "application" or "accessCode". - 'authorizationUrl' => 'http://example.com/auth', // The authorization URL to be used for (implicit/accessCode) - //'tokenUrl' => 'http://example.com/auth' // The authorization URL to be used for (password/application/accessCode) - 'scopes' => [ - 'read:projects' => 'read your projects', - 'write:projects' => 'modify projects in your account', - ] - ], - */ - 'passport' => [ - 'description' => 'Laravel passport oauth2 security.', - 'type' => 'oauth2', - 'in' => 'header', - 'scheme' => 'https', - 'flows' => [ - "clientCredentials" => [ - "tokenUrl" => config('app.url') . '/oauth/token', - "scopes" => [] - ] - ], - ], - /* Open API 3.0 support - 'passport' => [ // Unique name of security - 'type' => 'oauth2', // The type of the security scheme. Valid values are "basic", "apiKey" or "oauth2". - 'description' => 'Laravel passport oauth2 security.', - 'in' => 'header', - 'scheme' => 'https', - 'flows' => [ - "password" => [ - "authorizationUrl" => config('app.url') . '/oauth/authorize', - "tokenUrl" => config('app.url') . '/oauth/token', - "refreshUrl" => config('app.url') . '/token/refresh', - "scopes" => [] - ], - ], - ],/* - 'sanctum' => [ // Unique name of security - 'type' => 'apiKey', // Valid values are "basic", "apiKey" or "oauth2". - 'description' => 'Enter token in format (Bearer )', - 'name' => 'Authorization', // The name of the header or query parameter to be used. - 'in' => 'header', // The location of the API key. Valid values are "query" or "header". - ],*/ - ], - 'security' => [ - /* - * Examples of Securities - */ - [ - /* - 'oauth2_security_example' => [ - 'read', - 'write' - ], - - 'passport' => [] - */], - ], - ], - - /* - * Set this to `true` in development mode so that docs would be regenerated on each request - * Set this to `false` to disable swagger generation on production - */ - 'generate_always' => env('L5_SWAGGER_GENERATE_ALWAYS', false), - - /* - * Set this to `true` to generate a copy of documentation in yaml format - */ - 'generate_yaml_copy' => env('L5_SWAGGER_GENERATE_YAML_COPY', false), - - /* - * Edit to trust the proxy's ip address - needed for AWS Load Balancer - * string[] - */ - 'proxy' => false, - - /* - * Configs plugin allows to fetch external configs instead of passing them to SwaggerUIBundle. - * See more at: https://github.com/swagger-api/swagger-ui#configs-plugin - */ - 'additional_config_url' => null, - - /* - * Apply a sort to the operation list of each API. It can be 'alpha' (sort by paths alphanumerically), - * 'method' (sort by HTTP method). - * Default is the order returned by the server unchanged. - */ - 'operations_sort' => env('L5_SWAGGER_OPERATIONS_SORT', 'method'), - - /* - * Pass the validatorUrl parameter to SwaggerUi init on the JS side. - * A null value here disables validation. - */ - 'validator_url' => null, - - /* - * Swagger UI configuration parameters - */ - 'ui' => [ - 'display' => [ - /* - * Controls the default expansion setting for the operations and tags. It can be : - * 'list' (expands only the tags), - * 'full' (expands the tags and operations), - * 'none' (expands nothing). - */ - 'doc_expansion' => env('L5_SWAGGER_UI_DOC_EXPANSION', 'none'), - - /** - * If set, enables filtering. The top bar will show an edit box that - * you can use to filter the tagged operations that are shown. Can be - * Boolean to enable or disable, or a string, in which case filtering - * will be enabled using that string as the filter expression. Filtering - * is case-sensitive matching the filter expression anywhere inside - * the tag. - */ - 'filter' => env('L5_SWAGGER_UI_FILTERS', true), // true | false - ], - - 'authorization' => [ - /* - * If set to true, it persists authorization data, and it would not be lost on browser close/refresh - */ - 'persist_authorization' => env('L5_SWAGGER_UI_PERSIST_AUTHORIZATION', false), - - 'oauth2' => [ - /* - * If set to true, adds PKCE to AuthorizationCodeGrant flow - */ - 'use_pkce_with_authorization_code_grant' => false, - ], - ], - ], - /* - * Constants which can be used in annotations - */ - 'constants' => [ - 'L5_SWAGGER_CONST_HOST' => env('APP_URL', 'http://localhost:8085'), - 'L5_SWAGGER_CONST_TOKEN_URL' => env('L5_SWAGGER_CONST_TOKEN_URL', config('app.url') . '/oauth/token'), - ], - ], -]; diff --git a/tests/Feature/ScrambleTest.php b/tests/Feature/ScrambleTest.php new file mode 100644 index 0000000..73e8997 --- /dev/null +++ b/tests/Feature/ScrambleTest.php @@ -0,0 +1,14 @@ +withMiddleware()->get('docs/api'); + $response->assertStatus(200); + } +} diff --git a/tests/Feature/SwaggerTest.php b/tests/Feature/SwaggerTest.php deleted file mode 100644 index c766c62..0000000 --- a/tests/Feature/SwaggerTest.php +++ /dev/null @@ -1,26 +0,0 @@ -withMiddleware()->get('/docs?api-docs.json'); - $response->assertStatus(200); - } - - public function testSwaggerRoute() - { - $response = $this->withMiddleware()->get('/api/documentation'); - $response->assertStatus(200); - } -} From c6dd4166c172f141192d33936ea76d9bcc7bafdf Mon Sep 17 00:00:00 2001 From: Valentina Pasquino Date: Fri, 10 Jul 2026 18:10:05 +0200 Subject: [PATCH 06/12] BZR-74 improve oauth --- app/Providers/ScrambleServiceProvider.php | 96 +++++++++++------------ config/scramble.php | 28 ++----- 2 files changed, 51 insertions(+), 73 deletions(-) diff --git a/app/Providers/ScrambleServiceProvider.php b/app/Providers/ScrambleServiceProvider.php index a9146e4..84608c5 100644 --- a/app/Providers/ScrambleServiceProvider.php +++ b/app/Providers/ScrambleServiceProvider.php @@ -4,70 +4,62 @@ use Dedoc\Scramble\Scramble; use Dedoc\Scramble\Support\Generator\OpenApi; +use Dedoc\Scramble\Support\Generator\Schema; use Dedoc\Scramble\Support\Generator\SecurityRequirement; use Dedoc\Scramble\Support\Generator\SecurityScheme; use Dedoc\Scramble\Support\Generator\SecuritySchemes\OAuthFlow; +use Dedoc\Scramble\Support\Generator\Types\StringType; +use Dedoc\Scramble\Support\Generator\Operation; +use Dedoc\Scramble\Support\Generator\Parameter; +use Dedoc\Scramble\Support\Generator\RequestBodyObject; +use Dedoc\Scramble\Support\RouteInfo; use Illuminate\Support\ServiceProvider; -use Illuminate\Support\Facades\Route; class ScrambleServiceProvider extends ServiceProvider { - public function boot(): void + public function boot() { - Scramble::afterOpenApiGenerated(function (OpenApi $openApi) { - $this->addSecuritySchemes($openApi); - $routeMiddlewareMap = []; - foreach (Route::getRoutes() as $route) { - if (!str_starts_with($route->uri(), 'api/')) { - continue; - } - $action = $route->getAction(); - if (isset($action['controller'])) { - $parts = explode('@', $action['controller']); - if (count($parts) === 2) { - [$controller, $method] = $parts; - $controllerShortName = class_basename($controller); - $operationId = strtolower(str_replace('Controller', '', $controllerShortName)) . '.' . $method; - $middlewares = $route->gatherMiddleware(); - $routeMiddlewareMap[$operationId] = $middlewares; - } - } - } + Scramble::configure() - foreach ($openApi->paths as $path) { - foreach ($path->operations as $operation) { - $operationId = $operation->operationId ?? ''; - $middlewares = $routeMiddlewareMap[$operationId] ?? []; + ->withDocumentTransformers(function (OpenApi $openApi) { - if (in_array('client', $middlewares)) { - $operation->security = [new SecurityRequirement(['passport' => []])]; - } elseif (in_array('basicAuth', $middlewares)) { - $operation->security = [new SecurityRequirement(['basicAuth' => []])]; - } - } - } - }); - } + // INFO: define security schemas + $openApi->secure(SecurityScheme::http('basic')->as("basicAuth")); + $openApi->secure(SecurityScheme::oauth2() + ->as("passport") + ->flow('authorizationCode', function (OAuthFlow $flow) { + $flow + ->authorizationUrl(config('app.url') . '/oauth/authorize') + ->tokenUrl(config('app.url') . '/oauth/token') + ->addScope('*', 'all'); + })); + }) - private function addSecuritySchemes(OpenApi $openApi): void - { - // Add OAuth2 (Laravel Passport) security scheme - $clientCredentialsFlow = new OAuthFlow(); - $clientCredentialsFlow->tokenUrl(config('app.url') . '/oauth/token'); + ->withOperationTransformers(function (Operation $operation, RouteInfo $routeInfo) { - $oauth2Scheme = SecurityScheme::oauth2() - ->as('passport') - ->setDescription('OAuth2 client credentials flow for admin API access') - ->flows(function ($flows) use ($clientCredentialsFlow) { - $flows->clientCredentials($clientCredentialsFlow); - }); - - $openApi->components->addSecurityScheme('passport', $oauth2Scheme); - - $basicAuthScheme = SecurityScheme::http('basic') - ->as('basicAuth') - ->setDescription('Basic HTTP authentication for publishers sending messages'); + // INFO: assign security schema based on middleware + $routeMiddlewares = collect($routeInfo->route->gatherMiddleware()); + if ($routeMiddlewares->contains("basicAuth")) { + $operation->addSecurity(new SecurityRequirement(["basicAuth" => []])); + } elseif ($routeMiddlewares->contains("client")) { + $operation->addSecurity(new SecurityRequirement(["passport" => []])); + } else { + $operation->security = []; + } - $openApi->components->addSecurityScheme('basicAuth', $basicAuthScheme); + // INFO: improve oauth/token route doc + if ($operation->path == "oauth/token" && $operation->method == "post") { + $operation->addRequestBodyObject(RequestBodyObject::make()->setContent( + 'application/json', + Schema::createFromParameters([ + (new Parameter('grant_type', 'query'))->setSchema(Schema::fromType(new StringType))->example("client_credentials"), + (new Parameter('client_id', 'query'))->setSchema(Schema::fromType(new StringType))->example("1"), + (new Parameter('client_secret', 'query'))->setSchema(Schema::fromType(new StringType))->example("secretOAuth2Example"), + (new Parameter('scope', 'query'))->setSchema(Schema::fromType(new StringType))->example(""), + ]) + )); + } + }) + ; } } diff --git a/config/scramble.php b/config/scramble.php index 2338bb3..aa4c2f8 100644 --- a/config/scramble.php +++ b/config/scramble.php @@ -19,7 +19,10 @@ * Override with `servers`, or use Scramble::registerApi() for separate bases. */ 'api_path' => [ - 'include' => 'api', + 'include' => [ + 'api', + 'oauth/token' + ], 'exclude' => [ 'api/documentation', // l5-swagger UI 'api/oauth2-callback', // l5-swagger OAuth callback @@ -28,6 +31,7 @@ 'api/basic', // Test endpoint 'api/oauth', // Test endpoint 'api/none', // Test endpoint + 'oauth/token/refresh' ], ], @@ -103,9 +107,7 @@ * ], * ``` */ - 'servers' => [ - 'Local' => env('APP_URL', 'http://localhost:8085') . '/api', - ], + 'servers' => [], /** * Determines how Scramble stores the descriptions of enum cases. @@ -153,23 +155,7 @@ /* * Security schemes for the API. Defines the authentication methods available. */ - 'security_schemes' => [ - 'passport' => [ - 'type' => 'oauth2', - 'description' => 'OAuth2 client credentials flow for admin API access', - 'flows' => [ - 'clientCredentials' => [ - 'tokenUrl' => env('APP_URL', 'http://localhost:8085') . '/oauth/token', - 'scopes' => [], - ], - ], - ], - 'basicAuth' => [ - 'type' => 'http', - 'scheme' => 'basic', - 'description' => 'Basic HTTP authentication for publishers sending messages', - ], - ], + 'security_schemes' => [], /* * Automatically document API security (OpenAPI `security` / `securitySchemes`) based on route From 09f7cf4e2b9683e73ce1f5642c62f10b181688d0 Mon Sep 17 00:00:00 2001 From: FrancescoSanti Date: Tue, 14 Jul 2026 14:23:33 +0200 Subject: [PATCH 07/12] BZR-74: add test for json route --- tests/Feature/ScrambleTest.php | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/tests/Feature/ScrambleTest.php b/tests/Feature/ScrambleTest.php index 73e8997..4e9acab 100644 --- a/tests/Feature/ScrambleTest.php +++ b/tests/Feature/ScrambleTest.php @@ -11,4 +11,10 @@ public function testScrambleRoute() $response = $this->withMiddleware()->get('docs/api'); $response->assertStatus(200); } + + public function testScrambleJsonRoute(): void + { + $response = $this->withMiddleware()->get('docs/api'); + $response->assertStatus(200); + } } From 6f98ba5bb69b5610f8a6ef041e3fa6493882fe8a Mon Sep 17 00:00:00 2001 From: FrancescoSanti Date: Tue, 14 Jul 2026 15:37:52 +0200 Subject: [PATCH 08/12] BZR-74: add delete button --- .../views/vendor/scramble/docs.blade.php | 73 +++++++++++++++++-- 1 file changed, 66 insertions(+), 7 deletions(-) diff --git a/resources/views/vendor/scramble/docs.blade.php b/resources/views/vendor/scramble/docs.blade.php index 6aaee67..2805fb7 100644 --- a/resources/views/vendor/scramble/docs.blade.php +++ b/resources/views/vendor/scramble/docs.blade.php @@ -37,6 +37,17 @@ } }; + const hasHeader = (headers, headerKey) => { + if (headers instanceof Headers) { + return headers.has(headerKey); + } else if (Array.isArray(headers)) { + return headers.some(([key]) => key.toLowerCase() === headerKey.toLowerCase()); + } else if (headers) { + return Object.keys(headers).some((key) => key.toLowerCase() === headerKey.toLowerCase()); + } + return false; + }; + const { headers = new Headers() } = options || {}; // Add CSRF token @@ -45,10 +56,11 @@ updateFetchHeaders(headers, CSRF_TOKEN_HEADER_KEY, decodeURIComponent(csrfToken)); } - // Add OAuth2 Bearer token if available - if (window.oauth2Token) { + // Add OAuth2 Bearer token if available, but don't clobber an Authorization + // header already set for another security scheme (e.g. Basic Auth or an API key) + if (window.oauth2Token && !hasHeader(headers, 'Authorization')) { updateFetchHeaders(headers, 'Authorization', `Bearer ${window.oauth2Token}`); - } + } return originalFetch(url, { ...options, @@ -114,13 +126,16 @@ @@ -322,7 +337,7 @@ function updateStoplightAuthDisplay() { const originalText = btn.textContent; const originalBg = btn.style.background; - btn.textContent = 'Token Applied!'; + btn.textContent = 'Applied!'; btn.style.background = '#059669'; // Update Stoplight Elements display (which now reads from textarea) @@ -373,6 +388,50 @@ function showCopyFeedback() { }, 2000); } + document.getElementById('delete-token-btn').addEventListener('click', () => { + // Clear the in-memory token used by the fetch interceptor + window.oauth2Token = null; + + // Clear the textarea and hide the token display + document.getElementById('token-value').value = ''; + tokenDisplayHide(); + + // Clear any Stoplight Elements authorization inputs that were populated + const selectors = [ + 'input[placeholder*="Bearer"]', + 'input[placeholder*="Authorization"]', + 'input[placeholder*="token"]', + 'input[name*="Authorization"]', + 'input[name*="authorization"]', + '[class*="auth"] input[type="text"]', + '[class*="Auth"] input[type="text"]', + ]; + selectors.forEach((selector) => { + document.querySelectorAll(selector).forEach((input) => { + input.value = ''; + input.dispatchEvent(new Event('input', { bubbles: true })); + input.dispatchEvent(new Event('change', { bubbles: true })); + }); + }); + + showDeleteFeedback(); + }); + + function tokenDisplayHide() { + document.getElementById('token-display').style.display = 'none'; + } + + function showDeleteFeedback() { + const btn = document.getElementById('delete-token-btn'); + const originalText = btn.textContent; + btn.textContent = 'Deleted!'; + btn.style.background = '#991b1b'; + setTimeout(() => { + btn.textContent = originalText; + btn.style.background = '#dc2626'; + }, 2000); + } + // Sync textarea token-value changes to window.oauth2Token document.getElementById('token-value').addEventListener('input', (e) => { const newToken = e.target.value.trim(); From cfdefa43691e50e9ad422e9a33ba456d5edb808d Mon Sep 17 00:00:00 2001 From: FrancescoSanti Date: Tue, 14 Jul 2026 15:45:27 +0200 Subject: [PATCH 09/12] BZR-74: align buttons behaviour after click --- .../views/vendor/scramble/docs.blade.php | 36 ++++++++++++++----- 1 file changed, 28 insertions(+), 8 deletions(-) diff --git a/resources/views/vendor/scramble/docs.blade.php b/resources/views/vendor/scramble/docs.blade.php index 2805fb7..a79fca8 100644 --- a/resources/views/vendor/scramble/docs.blade.php +++ b/resources/views/vendor/scramble/docs.blade.php @@ -380,21 +380,30 @@ function updateStoplightAuthDisplay() { function showCopyFeedback() { const btn = document.getElementById('copy-token-btn'); const originalText = btn.textContent; + const originalBg = btn.style.background; + btn.textContent = 'Copied!'; btn.style.background = '#059669'; + setTimeout(() => { - btn.textContent = originalText; - btn.style.background = '#10b981'; - }, 2000); + // Close the panel after showing feedback + oauthPanel.style.display = 'none'; + toggleBtn.style.display = 'flex'; + + // Reset button after closing + setTimeout(() => { + btn.textContent = originalText; + btn.style.background = originalBg; + }, 100); + }, 1000); } document.getElementById('delete-token-btn').addEventListener('click', () => { // Clear the in-memory token used by the fetch interceptor window.oauth2Token = null; - // Clear the textarea and hide the token display + // Clear the textarea document.getElementById('token-value').value = ''; - tokenDisplayHide(); // Clear any Stoplight Elements authorization inputs that were populated const selectors = [ @@ -424,12 +433,23 @@ function tokenDisplayHide() { function showDeleteFeedback() { const btn = document.getElementById('delete-token-btn'); const originalText = btn.textContent; + const originalBg = btn.style.background; + btn.textContent = 'Deleted!'; btn.style.background = '#991b1b'; + setTimeout(() => { - btn.textContent = originalText; - btn.style.background = '#dc2626'; - }, 2000); + // Close the panel after showing feedback + oauthPanel.style.display = 'none'; + toggleBtn.style.display = 'flex'; + tokenDisplayHide(); + + // Reset button after closing + setTimeout(() => { + btn.textContent = originalText; + btn.style.background = originalBg; + }, 100); + }, 1000); } // Sync textarea token-value changes to window.oauth2Token From ef75063eec2c24b3f4e57a2df7d84296b03e2561 Mon Sep 17 00:00:00 2001 From: FrancescoSanti Date: Fri, 17 Jul 2026 14:19:55 +0200 Subject: [PATCH 10/12] BZR-74: fix duplicated test --- tests/Feature/ScrambleTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/Feature/ScrambleTest.php b/tests/Feature/ScrambleTest.php index 4e9acab..fab1b6c 100644 --- a/tests/Feature/ScrambleTest.php +++ b/tests/Feature/ScrambleTest.php @@ -14,7 +14,7 @@ public function testScrambleRoute() public function testScrambleJsonRoute(): void { - $response = $this->withMiddleware()->get('docs/api'); + $response = $this->withMiddleware()->get('docs/api.json'); $response->assertStatus(200); } } From 9dc936b581f4575ce88b147cf77fa1fdaa2d4be6 Mon Sep 17 00:00:00 2001 From: FrancescoSanti Date: Fri, 17 Jul 2026 14:23:50 +0200 Subject: [PATCH 11/12] BZR-74: light theme --- config/scramble.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/scramble.php b/config/scramble.php index aa4c2f8..56714dd 100644 --- a/config/scramble.php +++ b/config/scramble.php @@ -70,7 +70,7 @@ */ 'elements' => [ 'view' => 'scramble::docs', - 'theme' => 'dark', + 'theme' => 'light', 'hideTryIt' => false, 'hideSchemas' => false, 'logo' => '', From 15a58aa984b90997002cbd6eb27618f87444844a Mon Sep 17 00:00:00 2001 From: Michela Date: Fri, 17 Jul 2026 14:28:43 +0200 Subject: [PATCH 12/12] BZR-74: change passport secutiry scheme to clientCredentials --- app/Providers/ScrambleServiceProvider.php | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/app/Providers/ScrambleServiceProvider.php b/app/Providers/ScrambleServiceProvider.php index 84608c5..d1b6c80 100644 --- a/app/Providers/ScrambleServiceProvider.php +++ b/app/Providers/ScrambleServiceProvider.php @@ -27,9 +27,8 @@ public function boot() $openApi->secure(SecurityScheme::http('basic')->as("basicAuth")); $openApi->secure(SecurityScheme::oauth2() ->as("passport") - ->flow('authorizationCode', function (OAuthFlow $flow) { + ->flow('clientCredentials', function (OAuthFlow $flow) { $flow - ->authorizationUrl(config('app.url') . '/oauth/authorize') ->tokenUrl(config('app.url') . '/oauth/token') ->addScope('*', 'all'); }));