├── resources ├── dist │ └── .gitkeep ├── js │ └── index.js ├── views │ └── .gitkeep ├── css │ └── index.css └── lang │ ├── en │ └── api-service.php │ ├── it │ └── api-service.php │ └── de │ └── api-service.php ├── .husky ├── pre-push └── pre-commit ├── src ├── Contracts │ ├── HasAllowedSorts.php │ ├── HasAllowedFields.php │ ├── HasAllowedFilters.php │ └── HasAllowedIncludes.php ├── Exceptions │ └── InvalidTenancyConfiguration.php ├── Models │ └── Token.php ├── Facades │ └── ApiService.php ├── Traits │ ├── HasApiTransformer.php │ ├── HttpResponse.php │ └── HasHandlerTenantScope.php ├── Concerns │ └── HasTenancy.php ├── Transformers │ └── DefaultTransformer.php ├── Resources │ ├── TokenResource │ │ └── Pages │ │ │ ├── EditToken.php │ │ │ ├── ListTokens.php │ │ │ └── CreateToken.php │ └── TokenResource.php ├── AuthServiceProvider.php ├── Http │ ├── Requests │ │ └── LoginRequest.php │ ├── Controllers │ │ └── AuthController.php │ └── Handlers.php ├── ApiService.php ├── Policies │ └── TokenPolicy.php ├── Commands │ ├── MakeApiTransformerCommand.php │ ├── MakeApiRequest.php │ ├── MakeApiServiceCommand.php │ └── MakeApiHandlerCommand.php ├── ApiServicePlugin.php └── ApiServiceServiceProvider.php ├── postcss.config.cjs ├── stubs ├── CustomApiService.stub ├── ApiTransformer.stub ├── ResourceApiService.stub ├── Request.stub ├── CustomHandler.stub ├── DetailHandler.stub ├── DeleteHandler.stub ├── CreateHandler.stub ├── PaginationHandler.stub └── UpdateHandler.stub ├── config └── api-service.php ├── LICENSE.md ├── bin └── build.js ├── composer.json ├── routes └── api.php ├── CONTRIBUTING.md ├── CODE_OF_CONDUCT.md ├── README.md ├── TROUBLESHOOTING.md └── CHANGELOG.md /resources/dist/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /resources/js/index.js: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /resources/views/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.husky/pre-push: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # Running Test 4 | vendor/bin/pest --ci -------------------------------------------------------------------------------- /resources/css/index.css: -------------------------------------------------------------------------------- 1 | @import '../../vendor/filament/filament/resources/css/theme.css'; 2 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # Fix Styling 4 | vendor/bin/pint 5 | 6 | git add . 7 | -------------------------------------------------------------------------------- /src/Contracts/HasAllowedSorts.php: -------------------------------------------------------------------------------- 1 | resource->toArray(); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/Resources/TokenResource/Pages/EditToken.php: -------------------------------------------------------------------------------- 1 | TokenPolicy::class, 13 | ]; 14 | 15 | /** 16 | * Register any authentication / authorization services. 17 | */ 18 | public function boot(): void {} 19 | } 20 | -------------------------------------------------------------------------------- /stubs/ApiTransformer.stub: -------------------------------------------------------------------------------- 1 | resource->toArray(); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/Traits/HttpResponse.php: -------------------------------------------------------------------------------- 1 | json([ 12 | 'message' => $message, 13 | 'data' => $data, 14 | ]); 15 | } 16 | 17 | public static function sendNotFoundResponse($message = 'resource not found'): JsonResponse 18 | { 19 | return response()->json([ 20 | 'message' => $message, 21 | ], 404); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /stubs/ResourceApiService.stub: -------------------------------------------------------------------------------- 1 | |string> 21 | */ 22 | public function rules(): array 23 | { 24 | return {{ validationRules }}; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/Http/Requests/LoginRequest.php: -------------------------------------------------------------------------------- 1 | |string> 22 | */ 23 | public function rules(): array 24 | { 25 | return config('api-service.login-rules', [ 26 | 'email' => 'required|email', 27 | 'password' => 'required', 28 | ]); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /stubs/CustomHandler.stub: -------------------------------------------------------------------------------- 1 | fill($request->all()); 27 | 28 | $model->save(); 29 | 30 | return static::sendSuccessResponse($model, "Successfully Create Resource"); 31 | } 32 | } -------------------------------------------------------------------------------- /config/api-service.php: -------------------------------------------------------------------------------- 1 | [ 5 | 'token' => [ 6 | 'cluster' => null, 7 | 'group' => 'User', 8 | 'sort' => -1, 9 | 'icon' => 'heroicon-o-key', 10 | 'should_register_navigation' => false, 11 | ], 12 | ], 13 | 'models' => [ 14 | 'token' => [ 15 | 'enable_policy' => true, 16 | ], 17 | ], 18 | 'route' => [ 19 | 'panel_prefix' => true, 20 | 'use_resource_middlewares' => false, 21 | ], 22 | 'tenancy' => [ 23 | 'enabled' => false, 24 | 'awareness' => false, 25 | ], 26 | 'login-rules' => [ 27 | 'email' => 'required|email', 28 | 'password' => 'required', 29 | ], 30 | 'login-middleware' => [ 31 | // Add any additional middleware you want to apply to the login route 32 | ], 33 | 'logout-middleware' => [ 34 | 'auth:sanctum', 35 | // Add any additional middleware you want to apply to the logout route 36 | ], 37 | 'use-spatie-permission-middleware' => true, 38 | ]; 39 | -------------------------------------------------------------------------------- /stubs/DetailHandler.stub: -------------------------------------------------------------------------------- 1 | route('id'); 28 | 29 | $query = static::getEloquentQuery(); 30 | 31 | $query = QueryBuilder::for( 32 | $query->where(static::getKeyName(), $id) 33 | ) 34 | ->first(); 35 | 36 | if (!$query) return static::sendNotFoundResponse(); 37 | 38 | return new {{ transformer }}($query); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /stubs/DeleteHandler.stub: -------------------------------------------------------------------------------- 1 | route('id'); 31 | 32 | $model = static::getModel()::find($id); 33 | 34 | if (!$model) return static::sendNotFoundResponse(); 35 | 36 | $model->delete(); 37 | 38 | return static::sendSuccessResponse($model, "Successfully Delete Resource"); 39 | } 40 | } -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) rupadana 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /stubs/CreateHandler.stub: -------------------------------------------------------------------------------- 1 | fill($request->all()); 34 | 35 | $model->save(); 36 | 37 | return static::sendSuccessResponse($model, "Successfully Create Resource"); 38 | } 39 | } -------------------------------------------------------------------------------- /stubs/PaginationHandler.stub: -------------------------------------------------------------------------------- 1 | allowedFields($this->getAllowedFields() ?? []) 28 | ->allowedSorts($this->getAllowedSorts() ?? []) 29 | ->allowedFilters($this->getAllowedFilters() ?? []) 30 | ->allowedIncludes($this->getAllowedIncludes() ?? []) 31 | ->paginate(request()->query('per_page')) 32 | ->appends(request()->query()); 33 | 34 | return {{ transformer }}::collection($query); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /stubs/UpdateHandler.stub: -------------------------------------------------------------------------------- 1 | route('id'); 33 | 34 | $model = static::getModel()::find($id); 35 | 36 | if (!$model) return static::sendNotFoundResponse(); 37 | 38 | $model->fill($request->all()); 39 | 40 | $model->save(); 41 | 42 | return static::sendSuccessResponse($model, "Successfully Update Resource"); 43 | } 44 | } -------------------------------------------------------------------------------- /src/Http/Controllers/AuthController.php: -------------------------------------------------------------------------------- 1 | validated())) { 20 | return response()->json( 21 | [ 22 | 'success' => false, 23 | 'message' => 'The provided credentials are incorrect.', 24 | ], 25 | 401 26 | ); 27 | } 28 | 29 | $user = Auth::getLastAttempted(); 30 | 31 | return response()->json( 32 | [ 33 | 'success' => true, 34 | 'message' => 'Login success.', 35 | 'token' => $user->createToken($request->header('User-Agent'), ['*'])->plainTextToken, 36 | ], 37 | 201 38 | ); 39 | } 40 | 41 | /** 42 | * Logout 43 | * 44 | * @return JsonResponse 45 | */ 46 | public function logout() 47 | { 48 | Auth::user()->tokens()->delete(); 49 | 50 | return response()->json( 51 | [ 52 | 'success' => true, 53 | 'message' => 'Logout success.', 54 | ], 55 | 200 56 | ); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /bin/build.js: -------------------------------------------------------------------------------- 1 | import esbuild from 'esbuild' 2 | 3 | const isDev = process.argv.includes('--dev') 4 | 5 | async function compile(options) { 6 | const context = await esbuild.context(options) 7 | 8 | if (isDev) { 9 | await context.watch() 10 | } else { 11 | await context.rebuild() 12 | await context.dispose() 13 | } 14 | } 15 | 16 | const defaultOptions = { 17 | define: { 18 | 'process.env.NODE_ENV': isDev ? `'development'` : `'production'`, 19 | }, 20 | bundle: true, 21 | mainFields: ['module', 'main'], 22 | platform: 'neutral', 23 | sourcemap: isDev ? 'inline' : false, 24 | sourcesContent: isDev, 25 | treeShaking: true, 26 | target: ['es2020'], 27 | minify: !isDev, 28 | plugins: [{ 29 | name: 'watchPlugin', 30 | setup: function (build) { 31 | build.onStart(() => { 32 | console.log(`Build started at ${new Date(Date.now()).toLocaleTimeString()}: ${build.initialOptions.outfile}`) 33 | }) 34 | 35 | build.onEnd((result) => { 36 | if (result.errors.length > 0) { 37 | console.log(`Build failed at ${new Date(Date.now()).toLocaleTimeString()}: ${build.initialOptions.outfile}`, result.errors) 38 | } else { 39 | console.log(`Build finished at ${new Date(Date.now()).toLocaleTimeString()}: ${build.initialOptions.outfile}`) 40 | } 41 | }) 42 | } 43 | }], 44 | } 45 | 46 | compile({ 47 | ...defaultOptions, 48 | entryPoints: ['./resources/js/index.js'], 49 | outfile: './resources/dist/api-service.js', 50 | }) 51 | -------------------------------------------------------------------------------- /src/Resources/TokenResource/Pages/CreateToken.php: -------------------------------------------------------------------------------- 1 | user()->id; 21 | } 22 | 23 | $user = User::find($data['tokenable_id']); 24 | 25 | $this->newToken = $user->createToken($data['name'], $data['ability']); 26 | 27 | return $user; 28 | } 29 | 30 | protected function getCreatedNotification(): ?Notification 31 | { 32 | return Notification::make() 33 | ->title(__('api-service::api-service.notification.token.created')) 34 | ->body($this->newToken->plainTextToken) 35 | ->persistent() 36 | ->actions([ 37 | Action::make('close') 38 | ->label(__('api-service::api-service.action.close')) 39 | ->close(), 40 | ]) 41 | ->success(); 42 | } 43 | 44 | protected function sendCreatedNotificationAndRedirect(bool $shouldCreateAnotherInsteadOfRedirecting = true): void 45 | { 46 | if ($shouldCreateAnotherInsteadOfRedirecting) { 47 | // Ensure that the form record is anonymized so that relationships aren't loaded. 48 | $this->form->model($this->getRecord()::class); 49 | $this->record = null; 50 | 51 | $this->fillForm(); 52 | 53 | return; 54 | } 55 | 56 | $this->redirect($this->getRedirectUrl()); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/ApiService.php: -------------------------------------------------------------------------------- 1 | replace('/', '.') 42 | ->append('.'); 43 | 44 | $resourceRouteMiddlewares = static::useResourceMiddlewares() ? static::getResource()::getRouteMiddleware($panel) : []; 45 | 46 | Route::name($name) 47 | ->middleware($resourceRouteMiddlewares) 48 | ->prefix(static::$groupRouteName ?? $slug) 49 | ->group(function (Router $route) { 50 | foreach (static::handlers() as $key => $handler) { 51 | app($handler)->route($route); 52 | } 53 | }); 54 | } 55 | 56 | public static function handlers(): array 57 | { 58 | return []; 59 | } 60 | 61 | public static function isRoutePrefixedByPanel(): bool 62 | { 63 | return config('api-service.route.panel_prefix', true); 64 | } 65 | 66 | public static function useResourceMiddlewares(): bool 67 | { 68 | return config('api-service.route.use_resource_middlewares', false); 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /resources/lang/en/api-service.php: -------------------------------------------------------------------------------- 1 | 'Token', 13 | 'models' => 'Tokens', 14 | 15 | /* 16 | |-------------------------------------------------------------------------- 17 | | Table Columns 18 | |-------------------------------------------------------------------------- 19 | */ 20 | 21 | 'column.name' => 'Name', 22 | 'column.user' => 'User', 23 | 'column.abilities' => 'Abilities', 24 | 'column.created_at' => 'Created at', 25 | 26 | /* 27 | |-------------------------------------------------------------------------- 28 | | Form Fields 29 | |-------------------------------------------------------------------------- 30 | */ 31 | 32 | 'field.name' => 'Name', 33 | 'field.user' => 'User', 34 | 'field.ability' => 'Ability', 35 | 36 | /* 37 | |-------------------------------------------------------------------------- 38 | | Form Actions 39 | |-------------------------------------------------------------------------- 40 | */ 41 | 42 | 'action.select_all' => 'Select all', 43 | 'action.unselect_all' => 'Unselect all', 44 | 'action.close' => 'Close', 45 | 46 | /* 47 | |-------------------------------------------------------------------------- 48 | | Section & Tabs 49 | |-------------------------------------------------------------------------- 50 | */ 51 | 52 | 'section.general' => 'General:', 53 | 'section.abilities' => 'Abilities:', 54 | 'section.abilities.description' => 'Select abilities of the token', 55 | 56 | /* 57 | |-------------------------------------------------------------------------- 58 | | Notifications 59 | |-------------------------------------------------------------------------- 60 | */ 61 | 62 | 'notification.token.created' => 'Token created, save it!', 63 | 64 | ]; 65 | -------------------------------------------------------------------------------- /resources/lang/it/api-service.php: -------------------------------------------------------------------------------- 1 | 'Token', 13 | 'models' => 'Tokens', 14 | 15 | /* 16 | |-------------------------------------------------------------------------- 17 | | Table Columns 18 | |-------------------------------------------------------------------------- 19 | */ 20 | 21 | 'column.name' => 'Nome', 22 | 'column.user' => 'Utente', 23 | 'column.abilities' => 'Permessi', 24 | 'column.created_at' => 'Creato il', 25 | 26 | /* 27 | |-------------------------------------------------------------------------- 28 | | Form Fields 29 | |-------------------------------------------------------------------------- 30 | */ 31 | 32 | 'field.name' => 'Nome', 33 | 'field.user' => 'Utente', 34 | 'field.ability' => 'Permessi', 35 | 36 | /* 37 | |-------------------------------------------------------------------------- 38 | | Form Actions 39 | |-------------------------------------------------------------------------- 40 | */ 41 | 42 | 'action.select_all' => 'Seleziona tutto', 43 | 'action.unselect_all' => 'Deseleziona tutto', 44 | 'action.close' => 'Chiudi', 45 | 46 | /* 47 | |-------------------------------------------------------------------------- 48 | | Section & Tabs 49 | |-------------------------------------------------------------------------- 50 | */ 51 | 52 | 'section.general' => 'Generale:', 53 | 'section.abilities' => 'Permessi:', 54 | 'section.abilities.description' => 'Seleziona permessi per il token', 55 | 56 | /* 57 | |-------------------------------------------------------------------------- 58 | | Notifications 59 | |-------------------------------------------------------------------------- 60 | */ 61 | 62 | 'notification.token.created' => 'Token creato, salvalo e tienilo al sicuro!', 63 | 64 | ]; 65 | -------------------------------------------------------------------------------- /resources/lang/de/api-service.php: -------------------------------------------------------------------------------- 1 | 'Token', 13 | 'models' => 'Tokens', 14 | 15 | /* 16 | |-------------------------------------------------------------------------- 17 | | Table Columns 18 | |-------------------------------------------------------------------------- 19 | */ 20 | 21 | 'column.name' => 'Name', 22 | 'column.user' => 'Benutzer', 23 | 'column.abilities' => 'Fähigkeiten', 24 | 'column.created_at' => 'Erstellt am', 25 | 26 | /* 27 | |-------------------------------------------------------------------------- 28 | | Form Fields 29 | |-------------------------------------------------------------------------- 30 | */ 31 | 32 | 'field.name' => 'Name', 33 | 'field.user' => 'Benutzer', 34 | 'field.ability' => 'Fähigkeit', 35 | 36 | /* 37 | |-------------------------------------------------------------------------- 38 | | Form Actions 39 | |-------------------------------------------------------------------------- 40 | */ 41 | 42 | 'action.select_all' => 'Alle auswählen', 43 | 'action.unselect_all' => 'Alle abwählen', 44 | 'action.close' => 'Schließen', 45 | 46 | /* 47 | |-------------------------------------------------------------------------- 48 | | Section & Tabs 49 | |-------------------------------------------------------------------------- 50 | */ 51 | 52 | 'section.general' => 'Allgemein:', 53 | 'section.abilities' => 'Fähigkeiten:', 54 | 'section.abilities.description' => 'Wählen Sie die Fähigkeiten des Tokens aus', 55 | 56 | /* 57 | |-------------------------------------------------------------------------- 58 | | Notifications 59 | |-------------------------------------------------------------------------- 60 | */ 61 | 62 | 'notification.token.created' => 'Token erstellt, speichern Sie ihn!', 63 | 64 | ]; 65 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "rupadana/filament-api-service", 3 | "description": "A simple api service for supporting filamentphp", 4 | "keywords": [ 5 | "rupadana", 6 | "laravel", 7 | "api-service", 8 | "api", 9 | "filament", 10 | "filament api" 11 | ], 12 | "homepage": "https://github.com/rupadana/api-service", 13 | "support": { 14 | "issues": "https://github.com/rupadana/api-service/issues", 15 | "source": "https://github.com/rupadana/api-service" 16 | }, 17 | "license": "MIT", 18 | "authors": [ 19 | { 20 | "name": "Rupadana", 21 | "email": "rupadanawayan@gmail.com", 22 | "role": "Developer" 23 | } 24 | ], 25 | "require": { 26 | "php": "^8.2", 27 | "dedoc/scramble": "^0.13", 28 | "filament/filament": "^4.0", 29 | "illuminate/contracts": "^11.0|^12.0", 30 | "laravel/framework": "^11.0|^12.0", 31 | "laravel/sanctum": "^3.2|^4.0", 32 | "spatie/laravel-package-tools": "^1.19", 33 | "spatie/laravel-permission": "^6.0", 34 | "spatie/laravel-query-builder": "^5.3|^6.3" 35 | }, 36 | "require-dev": { 37 | "laravel/pint": "^1.0", 38 | "nunomaduro/collision": "^7.9|^8.0", 39 | "orchestra/testbench": "^9.0|^10.0", 40 | "pestphp/pest": "^2.0|^3.7", 41 | "pestphp/pest-plugin-arch": "^2.0|^3.0", 42 | "pestphp/pest-plugin-laravel": "^2.0|^3.1", 43 | "pestphp/pest-plugin-livewire": "^2.1|^3.0", 44 | "phpunit/phpunit": "^10.0.17|^10.5|^11.5" 45 | }, 46 | "autoload": { 47 | "psr-4": { 48 | "Rupadana\\ApiService\\": "src/" 49 | } 50 | }, 51 | "autoload-dev": { 52 | "psr-4": { 53 | "Rupadana\\ApiService\\Tests\\": "tests/" 54 | } 55 | }, 56 | "scripts": { 57 | "post-autoload-dump": "@php ./vendor/bin/testbench package:discover --ansi", 58 | "post-install-cmd": [ 59 | "yarn install", 60 | "npx husky init" 61 | ], 62 | "test": "vendor/bin/pest", 63 | "test-coverage": "vendor/bin/pest --coverage" 64 | }, 65 | "config": { 66 | "sort-packages": true, 67 | "allow-plugins": { 68 | "pestphp/pest-plugin": true, 69 | "phpstan/extension-installer": true 70 | } 71 | }, 72 | "extra": { 73 | "laravel": { 74 | "providers": [ 75 | "Rupadana\\ApiService\\ApiServiceServiceProvider", 76 | "Rupadana\\ApiService\\AuthServiceProvider" 77 | ], 78 | "aliases": { 79 | "ApiService": "Rupadana\\ApiService\\Facades\\ApiService" 80 | } 81 | } 82 | }, 83 | "minimum-stability": "stable", 84 | "prefer-stable": true 85 | } 86 | -------------------------------------------------------------------------------- /routes/api.php: -------------------------------------------------------------------------------- 1 | name('api.') 12 | ->group(function (Router $router) { 13 | $router->post('/auth/login', [AuthController::class, 'login'])->middleware(config('api-service.login-middleware', [])); 14 | $router->post('/auth/logout', [AuthController::class, 'logout'])->middleware(config('api-service.logout-middleware', ['auth:sanctum'])); 15 | 16 | if (ApiService::tenancyAwareness() && (! ApiService::isRoutePrefixedByPanel() || ! ApiService::isTenancyEnabled())) { 17 | throw new InvalidTenancyConfiguration("Tenancy awareness is enabled!. Please set 'api-service.route.panel_prefix=true' and 'api-service.tenancy.enabled=true'"); 18 | } 19 | 20 | $panels = Filament::getPanels(); 21 | 22 | foreach ($panels as $key => $panel) { 23 | try { 24 | 25 | $hasTenancy = $panel->hasTenancy(); 26 | $tenantRoutePrefix = $panel->getTenantRoutePrefix(); 27 | $tenantSlugAttribute = $panel->getTenantSlugAttribute(); 28 | $apiServicePlugin = $panel->getPlugin('api-service'); 29 | $middlewares = $apiServicePlugin->getMiddlewares(); 30 | $panelRoutePrefix = ApiService::isRoutePrefixedByPanel() ? '{panel}' : ''; 31 | $panelNamePrefix = $panelRoutePrefix ? $panel->getId() . '.' : ''; 32 | 33 | if ( 34 | $hasTenancy && 35 | ApiService::isTenancyEnabled() && 36 | ApiService::tenancyAwareness() 37 | ) { 38 | Route::prefix($panelRoutePrefix . '/' . (($tenantRoutePrefix) ? "{$tenantRoutePrefix}/" : '') . '{tenant' . (($tenantSlugAttribute) ? ":{$tenantSlugAttribute}" : '') . '}') 39 | ->name($panelNamePrefix) 40 | ->middleware($middlewares) 41 | ->group(function () use ($panel, $apiServicePlugin) { 42 | $apiServicePlugin->route($panel); 43 | }); 44 | } 45 | 46 | if (! ApiService::tenancyAwareness()) { 47 | Route::prefix($panelRoutePrefix) 48 | ->name($panelNamePrefix) 49 | ->middleware($middlewares) 50 | ->group(function () use ($panel) { 51 | $apiServicePlugin = $panel->getPlugin('api-service'); 52 | $apiServicePlugin->route($panel); 53 | }); 54 | } 55 | } catch (Exception $e) { 56 | // Log the exception for debugging purposes 57 | if (config('app.debug')) { 58 | logger()->error("Error registering API routes for panel '{$panel->getId()}': " . $e->getMessage(), [ 59 | 'exception' => $e, 60 | 'panel' => $panel->getId(), 61 | ]); 62 | } 63 | } 64 | } 65 | }); 66 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | Contributions are **welcome** and will be fully **credited**. 4 | 5 | Please read and understand the contribution guide before creating an issue or pull request. 6 | 7 | ## Development setup 8 | 9 | to contribute to this packages, you may want to test it in a real Filament project: 10 | 11 | - Fork this repository to your GitHub account. 12 | - Create a Filament app locally. 13 | - Clone your fork in your Filament app's root directory. 14 | - In the `/filament-api-service` directory, create a branch for your fix, e.g. `fix/error-message`. 15 | 16 | Install the packages in your app's `composer.json`: 17 | 18 | ```json 19 | "require": { 20 | "rupadana/filament-api-service": "dev-fix/error-message as main-dev", 21 | }, 22 | "repositories": [ 23 | { 24 | "type": "path", 25 | "url": "filament-api-service" 26 | } 27 | ] 28 | ``` 29 | 30 | Now, run `composer update`. 31 | 32 | 33 | ## Etiquette 34 | 35 | This project is open source, and as such, the maintainers give their free time to build and maintain the source code 36 | held within. They make the code freely available in the hope that it will be of use to other developers. It would be 37 | extremely unfair for them to suffer abuse or anger for their hard work. 38 | 39 | Please be considerate towards maintainers when raising issues or presenting pull requests. Let's show the 40 | world that developers are civilized and selfless people. 41 | 42 | It's the duty of the maintainer to ensure that all submissions to the project are of sufficient 43 | quality to benefit the project. Many developers have different skillsets, strengths, and weaknesses. Respect the maintainer's decision, and do not be upset or abusive if your submission is not used. 44 | 45 | ## Viability 46 | 47 | When requesting or submitting new features, first consider whether it might be useful to others. Open 48 | source projects are used by many developers, who may have entirely different needs to your own. Think about 49 | whether or not your feature is likely to be used by other users of the project. 50 | 51 | ## Procedure 52 | 53 | Before filing an issue: 54 | 55 | - Attempt to replicate the problem, to ensure that it wasn't a coincidental incident. 56 | - Check to make sure your feature suggestion isn't already present within the project. 57 | - Check the pull requests tab to ensure that the bug doesn't have a fix in progress. 58 | - Check the pull requests tab to ensure that the feature isn't already in progress. 59 | 60 | Before submitting a pull request: 61 | 62 | - Check the codebase to ensure that your feature doesn't already exist. 63 | - Check the pull requests to ensure that another person hasn't already submitted the feature or fix. 64 | 65 | ## Requirements 66 | 67 | If the project maintainer has any additional requirements, you will find them listed here. 68 | 69 | - **[PSR-2 Coding Standard](https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-2-coding-style-guide.md)** - The easiest way to apply the conventions is to install [PHP Code Sniffer](https://pear.php.net/package/PHP_CodeSniffer). 70 | 71 | - **Add tests!** - Your patch won't be accepted if it doesn't have tests. 72 | 73 | - **Document any change in behaviour** - Make sure the `README.md` and any other relevant documentation are kept up-to-date. 74 | 75 | - **Consider our release cycle** - We try to follow [SemVer v2.0.0](https://semver.org/). Randomly breaking public APIs is not an option. 76 | 77 | - **One pull request per feature** - If you want to do more than one thing, send multiple pull requests. 78 | 79 | - **Send coherent history** - Make sure each individual commit in your pull request is meaningful. If you had to make multiple intermediate commits while developing, please [squash them](https://www.git-scm.com/book/en/v2/Git-Tools-Rewriting-History#Changing-Multiple-Commit-Messages) before submitting. 80 | 81 | **Happy coding**! -------------------------------------------------------------------------------- /src/Policies/TokenPolicy.php: -------------------------------------------------------------------------------- 1 | isPolicyEnabled()) { 24 | return true; 25 | } 26 | 27 | return $user->can('view_any_token'); 28 | } 29 | 30 | /** 31 | * Determine whether the user can view the model. 32 | */ 33 | public function view(User $user, Token $token): bool 34 | { 35 | if (! $this->isPolicyEnabled()) { 36 | return true; 37 | } 38 | 39 | return $user->can('view_token') && $token->tokenable_id === $user->id; 40 | } 41 | 42 | /** 43 | * Determine whether the user can create models. 44 | */ 45 | public function create(User $user): bool 46 | { 47 | if (! $this->isPolicyEnabled()) { 48 | return true; 49 | } 50 | 51 | return $user->can('create_token'); 52 | } 53 | 54 | /** 55 | * Determine whether the user can update the model. 56 | */ 57 | public function update(User $user, Token $token): bool 58 | { 59 | if (! $this->isPolicyEnabled()) { 60 | return true; 61 | } 62 | 63 | return $user->can('update_token') && $token->tokenable_id === $user->id; 64 | } 65 | 66 | /** 67 | * Determine whether the user can delete the model. 68 | */ 69 | public function delete(User $user, Token $token): bool 70 | { 71 | 72 | if (! $this->isPolicyEnabled()) { 73 | return true; 74 | } 75 | 76 | return $user->can('delete_token') && $token->tokenable_id === $user->id; 77 | } 78 | 79 | /** 80 | * Determine whether the user can bulk delete. 81 | */ 82 | public function deleteAny(User $user): bool 83 | { 84 | if (! $this->isPolicyEnabled()) { 85 | return true; 86 | } 87 | 88 | return $user->can('delete_any_token'); 89 | } 90 | 91 | /** 92 | * Determine whether the user can permanently delete. 93 | */ 94 | public function forceDelete(User $user, Token $token): bool 95 | { 96 | if (! $this->isPolicyEnabled()) { 97 | return true; 98 | } 99 | 100 | return $user->can('force_delete_token') && $token->tokenable_id === $user->id; 101 | } 102 | 103 | /** 104 | * Determine whether the user can permanently bulk delete. 105 | */ 106 | public function forceDeleteAny(User $user): bool 107 | { 108 | if (! $this->isPolicyEnabled()) { 109 | return true; 110 | } 111 | 112 | return $user->can('force_delete_any_token'); 113 | } 114 | 115 | /** 116 | * Determine whether the user can restore. 117 | */ 118 | public function restore(User $user, Token $token): bool 119 | { 120 | if (! $this->isPolicyEnabled()) { 121 | return true; 122 | } 123 | 124 | return $user->can('restore_token') && $token->tokenable_id === $user->id; 125 | } 126 | 127 | /** 128 | * Determine whether the user can bulk restore. 129 | */ 130 | public function restoreAny(User $user): bool 131 | { 132 | if (! $this->isPolicyEnabled()) { 133 | return true; 134 | } 135 | 136 | return $user->can('restore_any_token'); 137 | } 138 | 139 | /** 140 | * Determine whether the user can replicate. 141 | */ 142 | public function replicate(User $user, Token $token): bool 143 | { 144 | if (! $this->isPolicyEnabled()) { 145 | return true; 146 | } 147 | 148 | return $user->can('replicate_token') && $token->tokenable_id === $user->id; 149 | } 150 | 151 | /** 152 | * Determine whether the user can reorder. 153 | */ 154 | public function reorder(User $user): bool 155 | { 156 | if (! $this->isPolicyEnabled()) { 157 | return true; 158 | } 159 | 160 | return $user->can('reorder_token'); 161 | } 162 | } 163 | -------------------------------------------------------------------------------- /src/Commands/MakeApiTransformerCommand.php: -------------------------------------------------------------------------------- 1 | argument('resource') ?? text( 23 | label: 'What is the Resource name?', 24 | placeholder: 'Blog', 25 | required: true, 26 | )) 27 | ->studly() 28 | ->beforeLast('Resource') 29 | ->trim('/') 30 | ->trim('\\') 31 | ->trim(' ') 32 | ->studly() 33 | ->replace('/', '\\'); 34 | 35 | if (blank($model)) { 36 | $model = 'Resource'; 37 | } 38 | 39 | $modelClass = (string) str($model)->afterLast('\\'); 40 | 41 | $modelNamespace = str($model)->contains('\\') ? 42 | (string) str($model)->beforeLast('\\') : 43 | ''; 44 | $pluralModelClass = (string) str($modelClass)->pluralStudly(); 45 | 46 | $panel = $this->option('panel'); 47 | 48 | if ($panel) { 49 | $panel = Filament::getPanel($panel); 50 | } 51 | 52 | if (! $panel) { 53 | $panels = Filament::getPanels(); 54 | 55 | /** @var Panel $panel */ 56 | $panel = (count($panels) > 1) ? $panels[select( 57 | label: 'Which panel would you like to create this in?', 58 | options: array_map( 59 | fn (Panel $panel): string => $panel->getId(), 60 | $panels, 61 | ), 62 | default: Filament::getDefaultPanel()->getId() 63 | )] : Arr::first($panels); 64 | } 65 | 66 | $resourceDirectories = $panel->getResourceDirectories(); 67 | $resourceNamespaces = $panel->getResourceNamespaces(); 68 | 69 | $namespace = (count($resourceNamespaces) > 1) ? 70 | select( 71 | label: 'Which namespace would you like to create this in?', 72 | options: $resourceNamespaces 73 | ) : (Arr::first($resourceNamespaces) ?? 'App\\Filament\\Resources'); 74 | $path = (count($resourceDirectories) > 1) ? 75 | $resourceDirectories[array_search($namespace, $resourceNamespaces)] : (Arr::first($resourceDirectories) ?? app_path('Filament/Resources/')); 76 | 77 | $resource = "{$model}Resource"; 78 | $resourceClass = "{$modelClass}Resource"; 79 | $apiTransformerClass = "{$model}Transformer"; 80 | $resourceNamespace = $modelNamespace; 81 | $namespace .= $resourceNamespace !== '' ? "\\{$resourceNamespace}" : ''; 82 | 83 | $baseResourcePath = 84 | (string) str("{$pluralModelClass}") 85 | ->prepend('/') 86 | ->prepend($path) 87 | ->replace('\\', '/') 88 | ->replace('//', '/'); 89 | 90 | $resourceApiTransformerDirectory = "{$baseResourcePath}/Api/Transformers/$apiTransformerClass.php"; 91 | 92 | $modelClass = app("{$namespace}\\{$pluralModelClass}\\{$resourceClass}")->getModel(); 93 | 94 | $this->copyStubToApp('ApiTransformer', $resourceApiTransformerDirectory, [ 95 | 'namespace' => "{$namespace}\\{$pluralModelClass}\\Api\\Transformers", 96 | 'resource' => "{$namespace}\\{$pluralModelClass}\\{$resourceClass}", 97 | 'resourceClass' => $resourceClass, 98 | 'resourcePageClass' => $resourceApiTransformerDirectory, 99 | 'apiTransformerClass' => $apiTransformerClass, 100 | 'model' => $model, 101 | 'modelClass' => $modelClass, 102 | ]); 103 | 104 | $this->components->info("Successfully created API Transformer for {$resource}!"); 105 | $this->components->info("Add this method to {$namespace}\\{$resourceClass}.php"); 106 | $this->components->info("public static function getApiTransformer() { 107 | return $apiTransformerClass::class; 108 | }"); 109 | 110 | return static::SUCCESS; 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /src/ApiServicePlugin.php: -------------------------------------------------------------------------------- 1 | 17 | */ 18 | protected array $middleware = []; 19 | 20 | public function getId(): string 21 | { 22 | return 'api-service'; 23 | } 24 | 25 | public function register(Panel $panel): void 26 | { 27 | $panel->resources([ 28 | TokenResource::class, 29 | ]); 30 | } 31 | 32 | public function boot(Panel $panel): void {} 33 | 34 | public static function getAbilities(Panel $panel): array 35 | { 36 | $resources = $panel->getResources(); 37 | 38 | $abilities = []; 39 | foreach ($resources as $key => $resource) { 40 | try { 41 | $apiServiceClass = static::resolveApiServiceClass($resource); 42 | 43 | if (! $apiServiceClass) { 44 | continue; 45 | } 46 | 47 | $handlers = app($apiServiceClass)->handlers(); 48 | 49 | if (count($handlers) > 0) { 50 | $abilities[$resource] = []; 51 | foreach ($handlers as $key => $handler) { 52 | $abilities[$resource][$handler] = app($handler)->getAbility(); 53 | } 54 | } 55 | } catch (Exception $e) { 56 | } 57 | } 58 | 59 | return $abilities; 60 | } 61 | 62 | public function route(Panel $panel): void 63 | { 64 | $resources = $panel->getResources(); 65 | 66 | foreach ($resources as $key => $resource) { 67 | try { 68 | $apiServiceClass = static::resolveApiServiceClass($resource); 69 | 70 | if (! $apiServiceClass) { 71 | continue; 72 | } 73 | 74 | app($apiServiceClass)->registerRoutes($panel); 75 | } catch (Exception $e) { 76 | // Log error in debug mode 77 | if (config('app.debug')) { 78 | logger()->error("Error registering API routes for resource '{$resource}': " . $e->getMessage(), [ 79 | 'exception' => $e, 80 | 'resource' => $resource, 81 | ]); 82 | } 83 | } 84 | } 85 | } 86 | 87 | public static function make(): static 88 | { 89 | return app(static::class); 90 | } 91 | 92 | public static function get(): static 93 | { 94 | /** @var static $plugin */ 95 | $plugin = filament(app(static::class)->getId()); 96 | 97 | return $plugin; 98 | } 99 | 100 | /** 101 | * @param array $middleware 102 | */ 103 | public function middleware(array $middleware): static 104 | { 105 | $this->middleware = [ 106 | ...$this->middleware, 107 | ...$middleware, 108 | ]; 109 | 110 | return $this; 111 | } 112 | 113 | public function getMiddlewares(): array 114 | { 115 | return $this->middleware; 116 | } 117 | 118 | /** 119 | * Resolve the API service class for a given resource 120 | * Caches parsed resource names to avoid repeated string operations 121 | */ 122 | protected static function resolveApiServiceClass(string $resource): ?string 123 | { 124 | static $cache = []; 125 | 126 | if (isset($cache[$resource])) { 127 | return $cache[$resource]; 128 | } 129 | 130 | $resourceName = str($resource)->beforeLast('Resource')->explode('\\')->last(); 131 | $pluralResourceName = str($resourceName)->pluralStudly(); 132 | 133 | // Try with plural form first (as created by make:filament-api-service command) 134 | $apiServiceClass = str($resource)->remove($resourceName . 'Resource') . $pluralResourceName . '\\Api\\' . $resourceName . 'ApiService'; 135 | 136 | // If the plural form doesn't exist, try without plural (legacy support) 137 | if (! class_exists($apiServiceClass)) { 138 | $apiServiceClass = str($resource)->remove($resourceName . 'Resource') . 'Api\\' . $resourceName . 'ApiService'; 139 | } 140 | 141 | // Only cache if the class exists 142 | if (class_exists($apiServiceClass)) { 143 | $cache[$resource] = $apiServiceClass; 144 | 145 | return $apiServiceClass; 146 | } 147 | 148 | $cache[$resource] = null; 149 | 150 | return null; 151 | } 152 | } 153 | -------------------------------------------------------------------------------- /src/Traits/HasHandlerTenantScope.php: -------------------------------------------------------------------------------- 1 | getKeyName(); 27 | } 28 | 29 | public static function getTenantOwnershipRelationship(Model $record): Relation 30 | { 31 | $relationshipName = static::getTenantOwnershipRelationshipName(); 32 | 33 | if (! $record->isRelation($relationshipName)) { 34 | 35 | $resourceClass = static::class; 36 | $recordClass = $record::class; 37 | 38 | throw new Exception("The model [{$recordClass}] does not have a relationship named [{$relationshipName}]. You can change the relationship being used by passing it to the [ownershipRelationship] argument of the [tenant()] method in configuration. You can change the relationship being used per-resource by setting it as the [\$tenantOwnershipRelationshipName] static property on the [{$resourceClass}] resource class."); 39 | } 40 | 41 | return $record->{$relationshipName}(); 42 | } 43 | 44 | protected static function modifyTenantQuery(Builder $query, ?Model $tenant = null): Builder 45 | { 46 | // Early return if not on API routes or tenancy not enabled 47 | if (! request()->routeIs('api.*') || ! Filament::hasTenancy()) { 48 | return $query; 49 | } 50 | 51 | // Early return if tenancy not enabled or not scoped to tenant 52 | if (! ApiService::isTenancyEnabled() || ! static::isScopedToTenant()) { 53 | return $query; 54 | } 55 | 56 | // Early return if user not authenticated 57 | if (! auth()->check()) { 58 | return $query; 59 | } 60 | 61 | $tenantOwnershipRelationship = static::getTenantOwnershipRelationship($query->getModel()); 62 | $tenantOwnershipRelationshipName = static::getTenantOwnershipRelationshipName(); 63 | $tenantModel = app(Filament::getTenantModel()); 64 | 65 | if (ApiService::tenancyAwareness()) { 66 | $tenantId = $tenant?->getKey() ?? request()->route()->parameter('tenant'); 67 | 68 | if (! $tenantId) { 69 | return $query; 70 | } 71 | 72 | $tenant = $tenantModel::where(Filament::getCurrentOrDefaultPanel()->getTenantSlugAttribute() ?? $tenantModel->getRouteKeyName(), $tenantId)->first(); 73 | 74 | if (! $tenant) { 75 | return $query; 76 | } 77 | 78 | $query = match (true) { 79 | $tenantOwnershipRelationship instanceof MorphTo => $query->whereMorphedTo( 80 | $tenantOwnershipRelationshipName, 81 | $tenant, 82 | ), 83 | $tenantOwnershipRelationship instanceof BelongsTo => $query->whereBelongsTo( 84 | $tenant, 85 | $tenantOwnershipRelationshipName, 86 | ), 87 | default => $query->whereHas( 88 | $tenantOwnershipRelationshipName, 89 | fn (Builder $query) => $query->whereKey($tenant->getKey()), 90 | ), 91 | }; 92 | } else { 93 | $userTenants = request()->user()->{Str::plural($tenantOwnershipRelationshipName)}; 94 | 95 | $query = match (true) { 96 | $tenantOwnershipRelationship instanceof MorphTo => $query 97 | ->where($tenantModel->getRelationWithoutConstraints($tenantOwnershipRelationshipName)->getMorphType(), $tenantModel->getMorphClass()) 98 | ->whereIn($tenantModel->getRelationWithoutConstraints($tenantOwnershipRelationshipName)->getForeignKeyName(), $userTenants->pluck($tenantModel->getKeyName())->toArray()), 99 | $tenantOwnershipRelationship instanceof BelongsTo => $query->whereBelongsTo($userTenants), 100 | default => $query->whereHas( 101 | $tenantOwnershipRelationshipName, 102 | fn (Builder $query) => $query->whereIn($query->getModel()->getQualifiedKeyName(), $userTenants->pluck($tenantModel->getKeyName())), 103 | ), 104 | }; 105 | } 106 | 107 | return $query; 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /src/ApiServiceServiceProvider.php: -------------------------------------------------------------------------------- 1 | name(static::$name) 38 | ->hasCommands($this->getCommands()) 39 | ->hasInstallCommand(function (InstallCommand $command) { 40 | $command 41 | ->publishConfigFile() 42 | ->askToStarRepoOnGitHub('rupadana/filament-api-service'); 43 | }) 44 | ->hasRoute('api'); 45 | 46 | $configFileName = $package->shortName(); 47 | 48 | if (file_exists($package->basePath("/../config/{$configFileName}.php"))) { 49 | $package->hasConfigFile(); 50 | } 51 | 52 | if (file_exists($package->basePath('/../database/migrations'))) { 53 | $package->hasMigrations($this->getMigrations()); 54 | } 55 | 56 | if (file_exists($package->basePath('/../resources/lang'))) { 57 | $package->hasTranslations(); 58 | } 59 | 60 | if (file_exists($package->basePath('/../resources/views'))) { 61 | $package->hasViews(static::$viewNamespace); 62 | } 63 | } 64 | 65 | public function packageRegistered(): void {} 66 | 67 | public function packageBooted(): void 68 | { 69 | // Asset Registration 70 | FilamentAsset::register( 71 | $this->getAssets(), 72 | $this->getAssetPackageName() 73 | ); 74 | 75 | FilamentAsset::registerScriptData( 76 | $this->getScriptData(), 77 | $this->getAssetPackageName() 78 | ); 79 | 80 | // Icon Registration 81 | FilamentIcon::register($this->getIcons()); 82 | 83 | $router = app('router'); 84 | $router->aliasMiddleware('abilities', CheckAbilities::class); 85 | $router->aliasMiddleware('ability', CheckForAnyAbility::class); 86 | $router->aliasMiddleware('role', RoleMiddleware::class); 87 | $router->aliasMiddleware('permission', PermissionMiddleware::class); 88 | $router->aliasMiddleware('role_or_permission', RoleOrPermissionMiddleware::class); 89 | 90 | // Configure Scramble Authentication 91 | Scramble::afterOpenApiGenerated(function (OpenApi $openApi) { 92 | $openApi->secure( 93 | SecurityScheme::http('bearer') 94 | ); 95 | }); 96 | 97 | // Publish Stubs 98 | if ($this->app->runningInConsole()) { 99 | foreach (app(Filesystem::class)->files(__DIR__ . '/../stubs/') as $file) { 100 | $this->publishes([ 101 | $file->getRealPath() => base_path("stubs/filament/{$file->getFilename()}"), 102 | ], static::$name . '-stubs'); 103 | } 104 | } 105 | } 106 | 107 | protected function getAssetPackageName(): ?string 108 | { 109 | return 'rupadana/api-service'; 110 | } 111 | 112 | /** 113 | * @return array 114 | */ 115 | protected function getAssets(): array 116 | { 117 | return []; 118 | } 119 | 120 | /** 121 | * @return array 122 | */ 123 | protected function getCommands(): array 124 | { 125 | return [ 126 | MakeApiHandlerCommand::class, 127 | MakeApiServiceCommand::class, 128 | MakeApiTransformerCommand::class, 129 | MakeApiRequest::class, 130 | ]; 131 | } 132 | 133 | /** 134 | * @return array 135 | */ 136 | protected function getIcons(): array 137 | { 138 | return []; 139 | } 140 | 141 | /** 142 | * @return array 143 | */ 144 | protected function getRoutes(): array 145 | { 146 | return []; 147 | } 148 | 149 | /** 150 | * @return array 151 | */ 152 | protected function getScriptData(): array 153 | { 154 | return []; 155 | } 156 | 157 | /** 158 | * @return array 159 | */ 160 | protected function getMigrations(): array 161 | { 162 | return [ 163 | 'create_api-service_table', 164 | ]; 165 | } 166 | } 167 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | We as members, contributors, and leaders pledge to make participation in our 6 | community a harassment-free experience for everyone, regardless of age, body 7 | size, visible or invisible disability, ethnicity, sex characteristics, gender 8 | identity and expression, level of experience, education, socio-economic status, 9 | nationality, personal appearance, race, religion, or sexual identity 10 | and orientation. 11 | 12 | We pledge to act and interact in ways that contribute to an open, welcoming, 13 | diverse, inclusive, and healthy community. 14 | 15 | ## Our Standards 16 | 17 | Examples of behavior that contributes to a positive environment for our 18 | community include: 19 | 20 | * Demonstrating empathy and kindness toward other people 21 | * Being respectful of differing opinions, viewpoints, and experiences 22 | * Giving and gracefully accepting constructive feedback 23 | * Accepting responsibility and apologizing to those affected by our mistakes, 24 | and learning from the experience 25 | * Focusing on what is best not just for us as individuals, but for the 26 | overall community 27 | 28 | Examples of unacceptable behavior include: 29 | 30 | * The use of sexualized language or imagery, and sexual attention or 31 | advances of any kind 32 | * Trolling, insulting or derogatory comments, and personal or political attacks 33 | * Public or private harassment 34 | * Publishing others' private information, such as a physical or email 35 | address, without their explicit permission 36 | * Other conduct which could reasonably be considered inappropriate in a 37 | professional setting 38 | 39 | ## Enforcement Responsibilities 40 | 41 | Community leaders are responsible for clarifying and enforcing our standards of 42 | acceptable behavior and will take appropriate and fair corrective action in 43 | response to any behavior that they deem inappropriate, threatening, offensive, 44 | or harmful. 45 | 46 | Community leaders have the right and responsibility to remove, edit, or reject 47 | comments, commits, code, wiki edits, issues, and other contributions that are 48 | not aligned to this Code of Conduct, and will communicate reasons for moderation 49 | decisions when appropriate. 50 | 51 | ## Scope 52 | 53 | This Code of Conduct applies within all community spaces, and also applies when 54 | an individual is officially representing the community in public spaces. 55 | Examples of representing our community include using an official e-mail address, 56 | posting via an official social media account, or acting as an appointed 57 | representative at an online or offline event. 58 | 59 | ## Enforcement 60 | 61 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 62 | reported to the community leaders responsible for enforcement at 63 | rupadana@codecrafters.id. 64 | All complaints will be reviewed and investigated promptly and fairly. 65 | 66 | All community leaders are obligated to respect the privacy and security of the 67 | reporter of any incident. 68 | 69 | ## Enforcement Guidelines 70 | 71 | Community leaders will follow these Community Impact Guidelines in determining 72 | the consequences for any action they deem in violation of this Code of Conduct: 73 | 74 | ### 1. Correction 75 | 76 | **Community Impact**: Use of inappropriate language or other behavior deemed 77 | unprofessional or unwelcome in the community. 78 | 79 | **Consequence**: A private, written warning from community leaders, providing 80 | clarity around the nature of the violation and an explanation of why the 81 | behavior was inappropriate. A public apology may be requested. 82 | 83 | ### 2. Warning 84 | 85 | **Community Impact**: A violation through a single incident or series 86 | of actions. 87 | 88 | **Consequence**: A warning with consequences for continued behavior. No 89 | interaction with the people involved, including unsolicited interaction with 90 | those enforcing the Code of Conduct, for a specified period of time. This 91 | includes avoiding interactions in community spaces as well as external channels 92 | like social media. Violating these terms may lead to a temporary or 93 | permanent ban. 94 | 95 | ### 3. Temporary Ban 96 | 97 | **Community Impact**: A serious violation of community standards, including 98 | sustained inappropriate behavior. 99 | 100 | **Consequence**: A temporary ban from any sort of interaction or public 101 | communication with the community for a specified period of time. No public or 102 | private interaction with the people involved, including unsolicited interaction 103 | with those enforcing the Code of Conduct, is allowed during this period. 104 | Violating these terms may lead to a permanent ban. 105 | 106 | ### 4. Permanent Ban 107 | 108 | **Community Impact**: Demonstrating a pattern of violation of community 109 | standards, including sustained inappropriate behavior, harassment of an 110 | individual, or aggression toward or disparagement of classes of individuals. 111 | 112 | **Consequence**: A permanent ban from any sort of public interaction within 113 | the community. 114 | 115 | ## Attribution 116 | 117 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 118 | version 2.0, available at 119 | https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. 120 | 121 | Community Impact Guidelines were inspired by [Mozilla's code of conduct 122 | enforcement ladder](https://github.com/mozilla/diversity). 123 | 124 | [homepage]: https://www.contributor-covenant.org 125 | 126 | For answers to common questions about this code of conduct, see the FAQ at 127 | https://www.contributor-covenant.org/faq. Translations are available at 128 | https://www.contributor-covenant.org/translations. 129 | -------------------------------------------------------------------------------- /src/Commands/MakeApiRequest.php: -------------------------------------------------------------------------------- 1 | argument('name') ?? text( 25 | label: 'What is the Request name?', 26 | placeholder: 'CreateRequest', 27 | required: true, 28 | )) 29 | ->studly() 30 | ->beforeLast('Request') 31 | ->trim('/') 32 | ->trim('\\') 33 | ->trim(' ') 34 | ->studly() 35 | ->replace('/', '\\'); 36 | 37 | $model = (string) str($this->argument('resource') ?? text( 38 | label: 'What is the Resource name?', 39 | placeholder: 'Blog', 40 | required: true, 41 | )) 42 | ->studly() 43 | ->beforeLast('Resource') 44 | ->trim('/') 45 | ->trim('\\') 46 | ->trim(' ') 47 | ->studly() 48 | ->replace('/', '\\'); 49 | 50 | if (blank($model)) { 51 | $model = 'Resource'; 52 | } 53 | 54 | $modelClass = (string) str($model)->afterLast('\\'); 55 | 56 | $modelNamespace = str($model)->contains('\\') ? 57 | (string) str($model)->beforeLast('\\') : 58 | ''; 59 | $pluralModelClass = (string) str($modelClass)->pluralStudly(); 60 | 61 | $panel = $this->option('panel'); 62 | 63 | if ($panel) { 64 | $panel = Filament::getPanel($panel); 65 | } 66 | 67 | if (! $panel) { 68 | $panels = Filament::getPanels(); 69 | 70 | /** @var Panel $panel */ 71 | $panel = (count($panels) > 1) ? $panels[select( 72 | label: 'Which panel would you like to create this in?', 73 | options: array_map( 74 | fn (Panel $panel): string => $panel->getId(), 75 | $panels, 76 | ), 77 | default: Filament::getDefaultPanel()->getId() 78 | )] : Arr::first($panels); 79 | } 80 | 81 | $resourceDirectories = $panel->getResourceDirectories(); 82 | $resourceNamespaces = $panel->getResourceNamespaces(); 83 | 84 | $namespace = (count($resourceNamespaces) > 1) ? 85 | select( 86 | label: 'Which namespace would you like to create this in?', 87 | options: $resourceNamespaces 88 | ) : (Arr::first($resourceNamespaces) ?? 'App\\Filament\\Resources'); 89 | $path = (count($resourceDirectories) > 1) ? 90 | $resourceDirectories[array_search($namespace, $resourceNamespaces)] : (Arr::first($resourceDirectories) ?? app_path('Filament/Resources/')); 91 | 92 | $nameClass = "{$name}{$model}Request"; 93 | $resource = "{$model}Resource"; 94 | $resourceClass = "{$modelClass}Resource"; 95 | $resourceNamespace = $modelNamespace; 96 | $namespace .= $resourceNamespace !== '' ? "\\{$resourceNamespace}" : ''; 97 | 98 | $baseResourcePath = 99 | (string) str("{$pluralModelClass}") 100 | ->prepend('/') 101 | ->prepend($path) 102 | ->replace('\\', '/') 103 | ->replace('//', '/'); 104 | 105 | $requestDirectory = "{$baseResourcePath}/Api/Requests/$nameClass.php"; 106 | 107 | $modelNamespace = app("{$namespace}\\{$pluralModelClass}\\{$resourceClass}")->getModel(); 108 | 109 | $this->copyStubToApp('Request', $requestDirectory, [ 110 | 'namespace' => "{$namespace}\\{$pluralModelClass}\\Api\\Requests", 111 | 'nameClass' => $nameClass, 112 | 'validationRules' => $this->getValidationRules(new $modelNamespace), 113 | ]); 114 | 115 | $this->components->info("Successfully created API {$nameClass} for {$resource}!"); 116 | 117 | return static::SUCCESS; 118 | } 119 | 120 | public function getValidationRules(Model $model) 121 | { 122 | $tableName = $model->getTable(); 123 | 124 | $columns = DB::getSchemaBuilder()->getColumnListing($tableName); 125 | 126 | $validationRules = collect($columns) 127 | ->filter(function ($column) { 128 | // Ignore colunas 'created_at' and 'updated_at' 129 | return ! in_array($column, ['id', 'created_at', 'updated_at']); 130 | }) 131 | ->map(function ($column) use ($model) { 132 | $type = DB::getSchemaBuilder()->getColumnType($model->getTable(), $column); 133 | 134 | // Data type mapping for Laravel Validation 135 | $rule = 'required'; // Add 'required' rule as default 136 | 137 | $rule .= match ($type) { 138 | 'integer' => '|integer', 139 | 'string', 'text' => '|string', 140 | 'date' => '|date', 141 | 'decimal', 'float', 'double' => '|numeric', 142 | default => '', 143 | }; 144 | 145 | return "\t\t\t'{$column}' => '{$rule}'"; 146 | }) 147 | ->implode(",\n"); 148 | 149 | return "[\n" . $validationRules . "\n\t\t]"; 150 | } 151 | } 152 | -------------------------------------------------------------------------------- /src/Resources/TokenResource.php: -------------------------------------------------------------------------------- 1 | components([ 42 | Section::make(__('api-service::api-service.section.general')) 43 | ->schema([ 44 | TextInput::make('name') 45 | ->label(__('api-service::api-service.field.name')) 46 | ->required(), 47 | Select::make('tokenable_id') 48 | ->options(User::all()->pluck('name', 'id')) 49 | ->label(__('api-service::api-service.field.user')) 50 | ->hidden(function () { 51 | $user = auth()->user(); 52 | 53 | $policy = config('api-service.models.token.enable_policy', true); 54 | 55 | if ($policy === false) { 56 | return false; 57 | } 58 | 59 | return ! $user->hasRole('super_admin'); 60 | }) 61 | ->required(), 62 | ]), 63 | Section::make(__('api-service::api-service.section.abilities')) 64 | ->description(__('api-service::api-service.section.abilities.description')) 65 | ->schema(static::getAbilitiesSchema()) 66 | ->columns(2), 67 | ]) 68 | ->columns(1); 69 | } 70 | public static function getAbilitiesSchema(): array 71 | { 72 | $schema = []; 73 | 74 | $abilities = ApiServicePlugin::getAbilities(Filament::getCurrentOrDefaultPanel()); 75 | 76 | foreach ($abilities as $resource => $handler) { 77 | $extractedAbilities = []; 78 | foreach ($handler as $handlerClass => $ability) { 79 | foreach ($ability as $a) { 80 | $extractedAbilities[$a] = __($a); 81 | } 82 | } 83 | 84 | $schema[] = Section::make(str($resource)->beforeLast('Resource')->explode('\\')->last()) 85 | ->description($resource) 86 | ->schema([ 87 | CheckboxList::make('ability') 88 | ->label(__('api-service::api-service.field.ability')) 89 | ->options($extractedAbilities) 90 | ->selectAllAction(fn (Action $action) => $action->label(__('api-service::api-service.action.select_all'))) 91 | ->deselectAllAction(fn (Action $action) => $action->label(__('api-service::api-service.action.unselect_all'))) 92 | ->bulkToggleable(), 93 | ]) 94 | ->collapsible(); 95 | } 96 | 97 | return $schema; 98 | } 99 | 100 | public static function table(Table $table): Table 101 | { 102 | return $table 103 | ->columns([ 104 | TextColumn::make('name') 105 | ->label(__('api-service::api-service.column.name')), 106 | TextColumn::make('tokenable.name') 107 | ->label(__('api-service::api-service.column.user')), 108 | TextColumn::make('abilities') 109 | ->label(__('api-service::api-service.column.abilities')) 110 | ->badge() 111 | ->words(1), 112 | TextColumn::make('created_at') 113 | ->label(__('api-service::api-service.column.created_at')) 114 | ->toggleable(isToggledHiddenByDefault: true), 115 | ]) 116 | ->filters([ 117 | 118 | ]) 119 | ->recordActions([ 120 | DeleteAction::make(), 121 | ]) 122 | ->toolbarActions([ 123 | BulkActionGroup::make([ 124 | DeleteBulkAction::make(), 125 | ]), 126 | ]) 127 | ->modifyQueryUsing(function (Builder $query) { 128 | $authenticatedUser = auth()->user(); 129 | 130 | if (method_exists($authenticatedUser, 'hasRole') && $authenticatedUser->hasRole('super_admin')) { 131 | return $query; 132 | } 133 | 134 | return $query->where('tokenable_id', $authenticatedUser->id); 135 | }); 136 | } 137 | 138 | public static function getRelations(): array 139 | { 140 | return [ 141 | 142 | ]; 143 | } 144 | 145 | public static function getPages(): array 146 | { 147 | return [ 148 | 'index' => ListTokens::route('/'), 149 | 'create' => CreateToken::route('/create'), 150 | ]; 151 | } 152 | 153 | public static function getCluster(): ?string 154 | { 155 | return config('api-service.navigation.token.cluster', null); 156 | } 157 | 158 | public static function getNavigationGroup(): ?string 159 | { 160 | return __(config('api-service.navigation.token.group') ?? config('api-service.navigation.group.token')); 161 | } 162 | 163 | public static function getNavigationSort(): ?int 164 | { 165 | return config('api-service.navigation.token.sort', -1); 166 | } 167 | 168 | public static function getNavigationIcon(): ?string 169 | { 170 | return config('api-service.navigation.token.icon', 'heroicon-o-key'); 171 | } 172 | 173 | public static function getModelLabel(): string 174 | { 175 | return __('api-service::api-service.model'); 176 | } 177 | 178 | public static function getPluralLabel(): string 179 | { 180 | return __('api-service::api-service.models'); 181 | } 182 | } 183 | -------------------------------------------------------------------------------- /src/Commands/MakeApiServiceCommand.php: -------------------------------------------------------------------------------- 1 | argument('resource') ?? text( 23 | label: 'What is the Resource name?', 24 | placeholder: 'Blog', 25 | required: true, 26 | )) 27 | ->studly() 28 | ->beforeLast('Resource') 29 | ->trim('/') 30 | ->trim('\\') 31 | ->trim(' ') 32 | ->studly() 33 | ->replace('/', '\\'); 34 | 35 | if (blank($model)) { 36 | $model = 'Resource'; 37 | } 38 | 39 | $modelClass = (string) str($model)->afterLast('\\'); 40 | 41 | $modelNamespace = str($model)->contains('\\') ? 42 | (string) str($model)->beforeLast('\\') : 43 | ''; 44 | $pluralModelClass = (string) str($modelClass)->pluralStudly(); 45 | 46 | $panel = $this->option('panel'); 47 | 48 | if ($panel) { 49 | $panel = Filament::getPanel($panel); 50 | } 51 | 52 | if (! $panel) { 53 | $panels = Filament::getPanels(); 54 | 55 | /** @var Panel $panel */ 56 | $panel = (count($panels) > 1) ? $panels[select( 57 | label: 'Which panel would you like to create this in?', 58 | options: array_map( 59 | fn (Panel $panel): string => $panel->getId(), 60 | $panels, 61 | ), 62 | default: Filament::getDefaultPanel()->getId() 63 | )] : Arr::first($panels); 64 | } 65 | 66 | $resourceDirectories = $panel->getResourceDirectories(); 67 | $resourceNamespaces = $panel->getResourceNamespaces(); 68 | 69 | $namespace = (count($resourceNamespaces) > 1) ? 70 | select( 71 | label: 'Which namespace would you like to create this in?', 72 | options: $resourceNamespaces 73 | ) : (Arr::first($resourceNamespaces) ?? 'App\\Filament\\Resources'); 74 | $path = (count($resourceDirectories) > 1) ? 75 | $resourceDirectories[array_search($namespace, $resourceNamespaces)] : (Arr::first($resourceDirectories) ?? app_path('Filament/Resources/')); 76 | 77 | $resource = "{$model}Resource"; 78 | $resourceClass = "{$modelClass}Resource"; 79 | $apiServiceClass = "{$model}ApiService"; 80 | $transformer = "{$model}Transformer"; 81 | $resourceNamespace = $modelNamespace; 82 | $namespace .= $resourceNamespace !== '' ? "\\{$resourceNamespace}" : ''; 83 | 84 | $createHandlerClass = 'CreateHandler'; 85 | $updateHandlerClass = 'UpdateHandler'; 86 | $detailHandlerClass = 'DetailHandler'; 87 | $paginationHandlerClass = 'PaginationHandler'; 88 | $deleteHandlerClass = 'DeleteHandler'; 89 | 90 | $baseResourcePath = 91 | (string) str("{$pluralModelClass}") 92 | ->prepend('/') 93 | ->prepend($path) 94 | ->replace('\\', '/') 95 | ->replace('//', '/'); 96 | 97 | $transformerClass = "{$namespace}\\{$pluralModelClass}\\Api\\Transformers\\{$transformer}"; 98 | $handlersNamespace = "{$namespace}\\{$pluralModelClass}\\Api\\Handlers"; 99 | 100 | $resourceApiDirectory = "{$baseResourcePath}/Api/$apiServiceClass.php"; 101 | $createHandlerDirectory = "{$baseResourcePath}/Api/Handlers/$createHandlerClass.php"; 102 | $updateHandlerDirectory = "{$baseResourcePath}/Api/Handlers/$updateHandlerClass.php"; 103 | $detailHandlerDirectory = "{$baseResourcePath}/Api/Handlers/$detailHandlerClass.php"; 104 | $paginationHandlerDirectory = "{$baseResourcePath}/Api/Handlers/$paginationHandlerClass.php"; 105 | $deleteHandlerDirectory = "{$baseResourcePath}/Api/Handlers/$deleteHandlerClass.php"; 106 | 107 | $this->call('make:filament-api-transformer', [ 108 | 'resource' => $model, 109 | '--panel' => $panel->getId(), 110 | ]); 111 | collect(['Create', 'Update']) 112 | ->each(function ($name) use ($model, $panel) { 113 | $this->call('make:filament-api-request', [ 114 | 'name' => $name, 115 | 'resource' => $model, 116 | '--panel' => $panel->getId(), 117 | ]); 118 | }); 119 | 120 | $this->copyStubToApp('ResourceApiService', $resourceApiDirectory, [ 121 | 'namespace' => "{$namespace}\\{$pluralModelClass}\\Api", 122 | 'resource' => "{$namespace}\\{$pluralModelClass}\\{$resourceClass}", 123 | 'resourceClass' => $resourceClass, 124 | 'resourcePageClass' => $resourceApiDirectory, 125 | 'apiServiceClass' => $apiServiceClass, 126 | ]); 127 | 128 | $this->copyStubToApp('DeleteHandler', $deleteHandlerDirectory, [ 129 | 'resource' => "{$namespace}\\{$pluralModelClass}\\{$resourceClass}", 130 | 'resourcePath' => "{$namespace}\\{$pluralModelClass}", 131 | 'resourceClass' => $resourceClass, 132 | 'handlersNamespace' => $handlersNamespace, 133 | 'model' => $model, 134 | ]); 135 | 136 | $this->copyStubToApp('DetailHandler', $detailHandlerDirectory, [ 137 | 'resource' => "{$namespace}\\{$pluralModelClass}\\{$resourceClass}", 138 | 'resourcePath' => "{$namespace}\\{$pluralModelClass}", 139 | 'resourceClass' => $resourceClass, 140 | 'handlersNamespace' => $handlersNamespace, 141 | 'transformer' => $transformer, 142 | 'transformerClass' => $transformerClass, 143 | 'model' => $model, 144 | ]); 145 | 146 | $this->copyStubToApp('CreateHandler', $createHandlerDirectory, [ 147 | 'resource' => "{$namespace}\\{$pluralModelClass}\\{$resourceClass}", 148 | 'resourcePath' => "{$namespace}\\{$pluralModelClass}", 149 | 'resourceClass' => $resourceClass, 150 | 'handlersNamespace' => $handlersNamespace, 151 | 'model' => $model, 152 | ]); 153 | 154 | $this->copyStubToApp('UpdateHandler', $updateHandlerDirectory, [ 155 | 'resource' => "{$namespace}\\{$pluralModelClass}\\{$resourceClass}", 156 | 'resourcePath' => "{$namespace}\\{$pluralModelClass}", 157 | 'resourceClass' => $resourceClass, 158 | 'handlersNamespace' => $handlersNamespace, 159 | 'model' => $model, 160 | ]); 161 | 162 | $this->copyStubToApp('PaginationHandler', $paginationHandlerDirectory, [ 163 | 'resource' => "{$namespace}\\{$pluralModelClass}\\{$resourceClass}", 164 | 'resourcePath' => "{$namespace}\\{$pluralModelClass}", 165 | 'resourceClass' => $resourceClass, 166 | 'handlersNamespace' => $handlersNamespace, 167 | 'transformer' => $transformer, 168 | 'transformerClass' => $transformerClass, 169 | 'model' => $model, 170 | 171 | ]); 172 | 173 | $this->components->info("Successfully created API for {$resource}!"); 174 | $this->components->info("It automatically registered to '/api' route group"); 175 | 176 | return static::SUCCESS; 177 | } 178 | } 179 | -------------------------------------------------------------------------------- /src/Commands/MakeApiHandlerCommand.php: -------------------------------------------------------------------------------- 1 | argument('resource') ?? text( 24 | label: 'What is the Resource name?', 25 | placeholder: 'Blog', 26 | required: true, 27 | )) 28 | ->studly() 29 | ->beforeLast('Resource') 30 | ->trim('/') 31 | ->trim('\\') 32 | ->trim(' ') 33 | ->studly() 34 | ->replace('/', '\\'); 35 | 36 | if (blank($model)) { 37 | $model = 'Resource'; 38 | } 39 | 40 | $handler = (string) str( 41 | $this->argument('handler') ?? text( 42 | label: 'What is the Handler name?', 43 | placeholder: 'CreateHandler', 44 | required: true 45 | ) 46 | ) 47 | ->studly() 48 | ->beforeLast('Handler') 49 | ->trim('/') 50 | ->trim('\\') 51 | ->trim(' ') 52 | ->studly() 53 | ->replace('/', '\\'); 54 | 55 | if (blank($handler)) { 56 | $handler = 'Handler'; 57 | } 58 | 59 | $modelClass = (string) str($model)->afterLast('\\'); 60 | 61 | $modelNamespace = str($model)->contains('\\') ? 62 | (string) str($model)->beforeLast('\\') : 63 | ''; 64 | $pluralModelClass = (string) str($modelClass)->pluralStudly(); 65 | 66 | $panel = $this->option('panel'); 67 | 68 | if ($panel) { 69 | $panel = Filament::getPanel($panel); 70 | } 71 | 72 | if (! $panel) { 73 | $panels = Filament::getPanels(); 74 | 75 | /** @var Panel $panel */ 76 | $panel = (count($panels) > 1) ? $panels[select( 77 | label: 'Which panel would you like to create this in?', 78 | options: array_map( 79 | fn (Panel $panel): string => $panel->getId(), 80 | $panels, 81 | ), 82 | default: Filament::getDefaultPanel()->getId() 83 | )] : Arr::first($panels); 84 | } 85 | 86 | $resourceDirectories = $panel->getResourceDirectories(); 87 | $resourceNamespaces = $panel->getResourceNamespaces(); 88 | 89 | $namespace = (count($resourceNamespaces) > 1) ? 90 | select( 91 | label: 'Which namespace would you like to create this in?', 92 | options: $resourceNamespaces 93 | ) : (Arr::first($resourceNamespaces) ?? 'App\\Filament\\Resources'); 94 | $path = (count($resourceDirectories) > 1) ? 95 | $resourceDirectories[array_search($namespace, $resourceNamespaces)] : (Arr::first($resourceDirectories) ?? app_path('Filament/Resources/')); 96 | 97 | $resource = "{$model}Resource"; 98 | $resourceClass = "{$modelClass}Resource"; 99 | $handlerClass = "{$handler}Handler"; 100 | $resourceNamespace = $modelNamespace; 101 | $namespace .= $resourceNamespace !== '' ? "\\{$resourceNamespace}" : ''; 102 | 103 | $baseResourcePath = 104 | (string) str("{$pluralModelClass}\\{$resource}") 105 | ->prepend('/') 106 | ->prepend($path) 107 | ->replace('\\', '/') 108 | ->replace('//', '/'); 109 | 110 | $handlersNamespace = "{$namespace}\\{$pluralModelClass}\\{$resourceClass}\\Api\\Handlers"; 111 | 112 | $handlerDirectory = "{$baseResourcePath}/Api/Handlers/$handlerClass.php"; 113 | 114 | $stubName = $this->getStubForHandler($handlerClass); 115 | $this->copyStubToApp($stubName, $handlerDirectory, [ 116 | 'resource' => "{$namespace}\\{$pluralModelClass}\\{$resourceClass}", 117 | 'resourceClass' => $resourceClass, 118 | 'handlersNamespace' => $handlersNamespace, 119 | 'handlerClass' => $handlerClass, 120 | ]); 121 | 122 | $this->createOrUpdateApiServiceFile($baseResourcePath, $namespace, $resourceClass, $modelClass, $handlerClass); 123 | 124 | $this->components->info("Successfully created API Handler for {$resource}!"); 125 | $this->components->info("Handler {$handlerClass} has been registered in the APIService."); 126 | 127 | return static::SUCCESS; 128 | } 129 | 130 | private function getStubForHandler(string $handlerClass): string 131 | { 132 | $handlerMap = [ 133 | 'CreateHandler' => 'CreateHandler', 134 | 'UpdateHandler' => 'UpdateHandler', 135 | 'DeleteHandler' => 'DeleteHandler', 136 | 'DetailHandler' => 'DetailHandler', 137 | 'PaginationHandler' => 'PaginationHandler', 138 | ]; 139 | 140 | return $handlerMap[$handlerClass] ?? 'CustomHandler'; 141 | } 142 | 143 | private function createOrUpdateApiServiceFile(string $baseResourcePath, string $namespace, string $resourceClass, string $modelClass, string $handlerClass): void 144 | { 145 | $apiServicePath = "{$baseResourcePath}/Api/{$modelClass}ApiService.php"; 146 | $apiServiceNamespace = "{$namespace}\\{$resourceClass}\\Api"; 147 | 148 | if (! File::exists($apiServicePath)) { 149 | $this->copyStubToApp('CustomApiService', $apiServicePath, [ 150 | 'namespace' => $apiServiceNamespace, 151 | 'resource' => "{$namespace}\\{$resourceClass}", 152 | 'resourceClass' => $resourceClass, 153 | 'apiServiceClass' => "{$modelClass}ApiService", 154 | 'handlers' => "Handlers\\{$handlerClass}::class", 155 | ]); 156 | } else { 157 | $content = File::get($apiServicePath); 158 | $updatedContent = $this->updateHandlersInContent($content, $handlerClass); 159 | File::put($apiServicePath, $updatedContent); 160 | } 161 | } 162 | 163 | private function updateHandlersInContent(string $content, string $newHandler): string 164 | { 165 | $pattern = '/public\s+static\s+function\s+handlers\(\)\s*:\s*array\s*\{[^}]*\}/s'; 166 | 167 | if (preg_match($pattern, $content, $matches)) { 168 | $handlersBlock = $matches[0]; 169 | $handlersList = $this->extractHandlersList($handlersBlock); 170 | 171 | if (! in_array("Handlers\\{$newHandler}::class", $handlersList)) { 172 | $handlersList[] = "Handlers\\{$newHandler}::class"; 173 | } 174 | 175 | $newHandlersBlock = "public static function handlers() : array\n {\n return [\n " . 176 | implode(",\n ", $handlersList) . 177 | "\n ];\n }"; 178 | 179 | return str_replace($handlersBlock, $newHandlersBlock, $content); 180 | } 181 | 182 | return $content; 183 | } 184 | 185 | private function extractHandlersList(string $handlersBlock): array 186 | { 187 | preg_match('/return\s*\[(.*?)\]/s', $handlersBlock, $matches); 188 | $handlersListString = $matches[1] ?? ''; 189 | $handlersList = array_map('trim', explode(',', $handlersListString)); 190 | 191 | return array_filter($handlersList); 192 | } 193 | } 194 | -------------------------------------------------------------------------------- /src/Http/Handlers.php: -------------------------------------------------------------------------------- 1 | routeIs('api.*') && ApiService::isRoutePrefixedByPanel()) { 48 | $this->panel(Filament::getPanel(request()->route()->parameter('panel'))); 49 | Filament::setCurrentPanel($this->getPanel()); 50 | } 51 | } 52 | 53 | public static function getMethod() 54 | { 55 | return static::$method; 56 | } 57 | 58 | public static function route(Router $router) 59 | { 60 | $method = static::getMethod(); 61 | 62 | $router 63 | ->{$method}(static::$uri, [static::class, 'handler']) 64 | ->name(static::getKebabClassName()) 65 | ->middleware(static::getRouteMiddleware()); 66 | } 67 | 68 | public static function isPublic(): bool 69 | { 70 | return static::$public; 71 | } 72 | 73 | public static function getRouteMiddleware(): array 74 | { 75 | if (static::isPublic()) { 76 | return []; 77 | } 78 | 79 | return [ 80 | 'auth:sanctum', 81 | static::getMiddlewareAliasName() . ':' . static::stringifyAbility(), 82 | ]; 83 | } 84 | 85 | protected static function getMiddlewareAliasName() 86 | { 87 | return config('api-service.use-spatie-permission-middleware', false) ? 'permission' : 'ability'; 88 | } 89 | 90 | public static function getKebabClassName() 91 | { 92 | $className = str(str(static::class)->beforeLast('Handler')->explode('\\')->last())->kebab()->value(); 93 | 94 | if (config('api-service.use-spatie-permission-middleware', false)) { 95 | return match ($className) { 96 | 'detail' => 'view', 97 | 'pagination' => 'view_any', 98 | default => $className 99 | }; 100 | } 101 | 102 | return $className; 103 | } 104 | 105 | public static function stringifyAbility() 106 | { 107 | return implode(',', static::getAbility()); 108 | } 109 | 110 | public static function getAbility(): array 111 | { 112 | if (config('api-service.use-spatie-permission-middleware', false)) { 113 | return [ 114 | static::$permission, 115 | ]; 116 | } 117 | 118 | $handlerClass = static::class; 119 | 120 | if (! isset(static::$modelClassCache[$handlerClass])) { 121 | static::$modelClassCache[$handlerClass] = str(str(static::getModel())->explode('\\')->last())->kebab()->value(); 122 | } 123 | 124 | return [ 125 | static::$modelClassCache[$handlerClass] . ':' . static::getKebabClassName(), 126 | ]; 127 | } 128 | 129 | public static function getModel() 130 | { 131 | return static::$resource::getModel(); 132 | } 133 | 134 | public static function getApiTransformer(): ?string 135 | { 136 | if (! method_exists(static::$resource, 'getApiTransformer')) { 137 | return DefaultTransformer::class; 138 | } 139 | 140 | return static::$resource::getApiTransformer(); 141 | } 142 | 143 | public static function getKeyName(): ?string 144 | { 145 | return static::$keyName; 146 | } 147 | 148 | public static function getTenantOwnershipRelationship(Model $record): Relation 149 | { 150 | return static::$resource::getTenantOwnershipRelationship($record); 151 | } 152 | 153 | public static function getTenantOwnershipRelationshipName(): ?string 154 | { 155 | return static::$resource::getTenantOwnershipRelationshipName(); 156 | } 157 | 158 | public static function isScopedToTenant(): bool 159 | { 160 | return static::$resource::isScopedToTenant(); 161 | } 162 | 163 | public function panel(Panel $panel) 164 | { 165 | $this->panel = $panel; 166 | 167 | return $this; 168 | } 169 | 170 | public function getAllowedFields(): array 171 | { 172 | $model = static::getModel(); 173 | 174 | if (static::modelImplements(HasAllowedFields::class)) { 175 | return $model::getAllowedFields(); 176 | } 177 | 178 | if (property_exists($model, 'allowedFields') && is_array($model::$allowedFields)) { 179 | return $model::$allowedFields; 180 | } 181 | 182 | return []; 183 | } 184 | 185 | public function getAllowedIncludes(): array 186 | { 187 | $model = static::getModel(); 188 | 189 | if (static::modelImplements(HasAllowedIncludes::class)) { 190 | return $model::getAllowedIncludes(); 191 | } 192 | 193 | if (property_exists($model, 'allowedIncludes') && is_array($model::$allowedIncludes)) { 194 | return $model::$allowedIncludes; 195 | } 196 | 197 | return []; 198 | } 199 | 200 | public function getAllowedSorts(): array 201 | { 202 | $model = static::getModel(); 203 | 204 | if (static::modelImplements(HasAllowedSorts::class)) { 205 | return $model::getAllowedSorts(); 206 | } 207 | 208 | if (property_exists($model, 'allowedFields') && is_array($model::$allowedFields)) { 209 | return $model::$allowedFields; 210 | } 211 | 212 | return []; 213 | } 214 | 215 | public function getAllowedFilters(): array 216 | { 217 | $model = static::getModel(); 218 | 219 | if (static::modelImplements(HasAllowedFilters::class)) { 220 | return $model::getAllowedFilters(); 221 | } 222 | 223 | if (property_exists($model, 'allowedFilters') && is_array($model::$allowedFilters)) { 224 | return $model::$allowedFilters; 225 | } 226 | 227 | return []; 228 | } 229 | 230 | public function getPanel(): Panel 231 | { 232 | return $this->panel; 233 | } 234 | 235 | protected static function getEloquentQuery() 236 | { 237 | $query = app(static::getModel())->query(); 238 | 239 | if (static::isScopedToTenant() && ApiService::tenancyAwareness() && Filament::getCurrentOrDefaultPanel()) { 240 | $query = static::modifyTenantQuery($query); 241 | } 242 | 243 | return $query; 244 | } 245 | 246 | /** 247 | * Check if the model implements a specific interface 248 | * Caches the result for performance 249 | */ 250 | protected static function modelImplements(string $interface): bool 251 | { 252 | $modelClass = static::getModel(); 253 | 254 | if (! isset(static::$modelImplementsCache[$modelClass])) { 255 | $implements = class_implements($modelClass) ?: []; 256 | // Flip array for O(1) lookup instead of O(n) 257 | static::$modelImplementsCache[$modelClass] = array_flip($implements); 258 | } 259 | 260 | return isset(static::$modelImplementsCache[$modelClass][$interface]); 261 | } 262 | } 263 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Filament Api Service 2 | 3 | [![Total Downloads](https://img.shields.io/packagist/dt/rupadana/filament-api-service.svg?style=flat-square)](https://packagist.org/packages/rupadana/filament-api-service) 4 | ![Run Test](https://github.com/rupadana/filament-api-service/actions/workflows/run-tests.yml/badge.svg?branch=main) 5 | 6 | Filament API Service is a powerful yet simple package that enables automatic API generation for FilamentPHP resources. It eliminates the need for manual route registration and provides built-in support for authentication, authorization, filtering, sorting, and more. 7 | 8 | ## Features 9 | - **Automatic API Generation** – Instantly create RESTful API endpoints for Filament resources. 10 | - **Authentication & Authorization** – Includes built-in authentication endpoints and integrates with Spatie Laravel Permission & Filament Shield. 11 | - **Role-Based Access Control** – Token-based authentication with customizable policies. 12 | - **Query Filters & Sorting** – Supports Spatie's laravel-query-builder for dynamic filtering, sorting, and field selection. 13 | - **Transformers** – Customize API responses using dedicated transformers. 14 | - **Multi-Tenancy Support** – Enable tenant-aware API responses with minimal configuration. 15 | - **Middleware Customization** – Add or override middlewares at the panel or resource level. 16 | - **Public & Secure APIs** – Define public or protected endpoints with a simple configuration. 17 | - **API Documentation** – Automatically generate API documentation with Scramble. 18 | 19 | ## Installation 20 | 21 | Install the package via Composer: 22 | 23 | ```bash 24 | composer require rupadana/filament-api-service 25 | ``` 26 | 27 | Register it in your Filament Provider: 28 | 29 | ```php 30 | use Rupadana\ApiService\ApiServicePlugin; 31 | 32 | $panel->plugins([ 33 | ApiServicePlugin::make() 34 | ]) 35 | ``` 36 | 37 | For further configuration, run: 38 | 39 | ```bash 40 | php artisan vendor:publish --tag=api-service-config 41 | ``` 42 | 43 | ```php 44 | 45 | return [ 46 | 'navigation' => [ 47 | 'token' => [ 48 | 'cluster' => null, 49 | 'group' => 'User', 50 | 'sort' => -1, 51 | 'icon' => 'heroicon-o-key', 52 | 'should_register_navigation' => false, 53 | ], 54 | ], 55 | 'models' => [ 56 | 'token' => [ 57 | 'enable_policy' => true, 58 | ], 59 | ], 60 | 'route' => [ 61 | 'panel_prefix' => true, 62 | 'use_resource_middlewares' => false, 63 | ], 64 | 'tenancy' => [ 65 | 'enabled' => false, 66 | 'awareness' => false, 67 | ], 68 | 'login-rules' => [ 69 | 'email' => 'required|email', 70 | 'password' => 'required', 71 | ], 72 | 'login-middleware' => [ 73 | // Add any additional middleware you want to apply to the login route 74 | ], 75 | 'logout-middleware' => [ 76 | 'auth:sanctum', 77 | // Add any additional middleware you want to apply to the logout route 78 | ], 79 | 'use-spatie-permission-middleware' => true, 80 | ]; 81 | ``` 82 | 83 | ## Usage 84 | 85 | Create an API service for a Filament resource: 86 | 87 | ```bash 88 | php artisan make:filament-api-service BlogResource 89 | ``` 90 | 91 | Since version 3.0, routes automatically registered. it will grouped as '/api/`admin`'. `admin` is panelId. to disable panelId prefix, please set `route.panel_prefix` to `false` 92 | 93 | Once generated, the following API endpoints are automatically available: 94 | 95 | | Method | Endpoint | Description | 96 | | ------ | -------------------- | --------------------------- | 97 | | GET | /api/`admin`/blogs | Return LengthAwarePaginator | 98 | | GET | /api/`admin`/blogs/1 | Return single resource | 99 | | PUT | /api/`admin`/blogs/1 | Update resource | 100 | | POST | /api/`admin`/blogs | Create resource | 101 | | DELETE | /api/`admin`/blogs/1 | Delete resource | 102 | 103 | To disable the admin panel prefix, update the config: 104 | 105 | ```php 106 | 'route' => [ 107 | 'panel_prefix' => false 108 | ], 109 | ``` 110 | 111 | ### Token Resource 112 | 113 | By default, Token resource only show on `super_admin` role. you can modify give permission to other permission too. 114 | 115 | Token Resource is protected by TokenPolicy. You can disable it by publishing the config and change this line. 116 | 117 | ```php 118 | 'models' => [ 119 | 'token' => [ 120 | 'enable_policy' => false // default: true 121 | ] 122 | ], 123 | ``` 124 | 125 | > [!IMPORTANT] 126 | > If you use Laravel 11, don't forget to run ``` php artisan install:api ``` to publish the personal_access_tokens migration after that run ``` php artisan migrate ``` to migrate the migration, but as default if you run the ``` php artisan install:api ``` it will ask you to migrate your migration. 127 | 128 | ### Filtering & Allowed Field 129 | 130 | We used `"spatie/laravel-query-builder": "^5.3"` to handle query selecting, sorting and filtering. Check out [the spatie/laravel-query-builder documentation](https://spatie.be/docs/laravel-query-builder/v5/introduction) for more information. 131 | 132 | In order to allow modifying the query for your model you can implement the `HasAllowedFields`, `HasAllowedSorts` and `HasAllowedFilters` Contracts in your model. 133 | 134 | ```php 135 | class User extends Model implements HasAllowedFields, HasAllowedSorts, HasAllowedFilters { 136 | // Which fields can be selected from the database through the query string 137 | public static function getAllowedFields(): array 138 | { 139 | // Your implementation here 140 | } 141 | 142 | // Which fields can be used to sort the results through the query string 143 | public static function getAllowedSorts(): array 144 | { 145 | // Your implementation here 146 | } 147 | 148 | // Which fields can be used to filter the results through the query string 149 | public static function getAllowedFilters(): array 150 | { 151 | // Your implementation here 152 | } 153 | } 154 | ``` 155 | 156 | ### Create a Handler 157 | 158 | To create a handler you can use this command. We have 5 Handler, CreateHandler, UpdateHandler, DeleteHandler, DetailHandler, PaginationHandler, If you want a custom handler then write what handler you want. 159 | 160 | ```bash 161 | php artisan make:filament-api-handler BlogResource 162 | ``` 163 | 164 | or 165 | 166 | ```bash 167 | php artisan make:filament-api-handler Blog 168 | ``` 169 | 170 | #### Customize Handlers 171 | If you want to customize the generated handlers, you can export them using the following command. 172 | 173 | ```bash 174 | php artisan vendor:publish --tag=api-service-stubs 175 | ``` 176 | 177 | ### Transform API Response 178 | 179 | ```bash 180 | php artisan make:filament-api-transformer Blog 181 | ``` 182 | 183 | it will be create BlogTransformer in `App\Filament\Resources\BlogResource\Api\Transformers` 184 | 185 | ```php 186 | resource->toArray(); 203 | 204 | // or 205 | 206 | return md5(json_encode($this->resource->toArray())); 207 | } 208 | } 209 | ``` 210 | 211 | next step you need to edit & add it to your Resource 212 | 213 | ```php 214 | use App\Filament\Resources\BlogResource\Api\Transformers\BlogTransformer; 215 | 216 | class BlogResource extends Resource 217 | { 218 | ... 219 | public static function getApiTransformer() 220 | { 221 | return BlogTransformer::class; 222 | } 223 | ... 224 | } 225 | ``` 226 | 227 | ### Group Name & Prefix 228 | 229 | You can edit prefix & group route name as you want, default this plugin use model singular label; 230 | 231 | ```php 232 | class BlogApiService extends ApiService 233 | { 234 | ... 235 | protected static string | null $groupRouteName = 'myblog'; 236 | ... 237 | } 238 | ``` 239 | 240 | ### Middlewares 241 | 242 | You can add or override middlewares at two specific places. Via the Filament Panel Provider and/or via the Resources $routeMiddleware. 243 | 244 | If you set `route.use_resource_middlewares` to true, the package will register the middlewares for that specific resource as defined in: 245 | 246 | ```php 247 | class BlogResource extends Resource 248 | { 249 | ... 250 | protected static string | array $routeMiddleware = []; // <-- your specific resource middlewares 251 | ... 252 | } 253 | ``` 254 | 255 | Then your API resource endpoint will go through these middlewares first. 256 | 257 | Another method of adding/overriding middlewares is via the initialization of the plugin in your Panel Provider by adding the `middleware()` method like so: 258 | 259 | ```php 260 | use Rupadana\ApiService\ApiServicePlugin; 261 | 262 | $panel->plugins([ 263 | ApiServicePlugin::make() 264 | ->middleware([ 265 | // ... add your middlewares 266 | ]) 267 | ]) 268 | ``` 269 | 270 | ### Tenancy 271 | 272 | When you want to enable Tenancy on this package you can enable this by setting the config `tenancy.enabled` to `true`. This makes sure that your api responses only retreive the data which that user has access to. So if you have configured 5 tenants and an user has access to 2 tenants. Then, enabling this feature will return only the data of those 2 tenants. 273 | 274 | If you have enabled tenancy on this package but on a specific Resource you have defined `protected static bool $isScopedToTenant = false;`, then the API will honour this for that specific resource and will return all records. 275 | 276 | If you want to make api routes tenant aware. you can set `tenancy.awareness` to `true` in your published api-service.php. This way this package will register extra API routes which will return only the specific tenant data in the API response. 277 | 278 | Now your API endpoints will have URI prefix of `{tenant}` in the API routes when `tenancy.awareness` is `true`. 279 | 280 | It will look like this: 281 | 282 | ```bash 283 | POST api/admin/{tenant}/blog 284 | GET|HEAD api/admin/{tenant}/blog 285 | PUT api/admin/{tenant}/blog/{id} 286 | DELETE api/admin/{tenant}/blog/{id} 287 | GET|HEAD api/admin/{tenant}/blog/{id} 288 | ``` 289 | 290 | Overriding tenancy ownership relationship name by adding this property to the Handlers `protected static ?string $tenantOwnershipRelationshipName = null;` 291 | 292 | ### How to secure it? 293 | 294 | Since version 3.4, this plugin includes built-in authentication routes: 295 | 296 | | Method | Endpoint | Description | 297 | | ------ | -------------------- | --------------------------- | 298 | | POST | /api/auth/login | Login | 299 | | POST | /api/auth/logout | Logout | 300 | 301 | We also use the permission middleware from [spatie/laravel-permission](https://spatie.be/docs/laravel-permission/v6/basic-usage/middleware). making it easier to integrate with [filament-shield](https://github.com/bezhanSalleh/filament-shield) 302 | 303 | If you prefer to use the old version of the middleware, please set 'use-spatie-permission-middleware' => false. 304 | 305 | ### Public API 306 | 307 | Set API to public by overriding this property on your API Handler. Assume we have a `PaginationHandler` 308 | 309 | ```php 310 | class PaginationHandler extends Handlers { 311 | public static bool $public = true; 312 | } 313 | ``` 314 | 315 | ### API Documentation 316 | 317 | For our documentation, we utilize [Scramble](https://scramble.dedoc.co), a powerful tool for generating and managing API documentation. All available routes and their detailed descriptions can be accessed at `/docs/api`. This ensures that developers have a centralized and well-organized resource to understand and integrate with the API effectively. 318 | 319 | ## Troubleshooting 320 | 321 | For common issues and their solutions, please refer to the [Troubleshooting Guide](TROUBLESHOOTING.md). 322 | 323 | ## License 324 | 325 | The MIT License (MIT). 326 | 327 | ## Supported By 328 | 329 | 330 | -------------------------------------------------------------------------------- /TROUBLESHOOTING.md: -------------------------------------------------------------------------------- 1 | # Troubleshooting Guide 2 | 3 | This document provides solutions to common issues and questions about Filament API Service. 4 | 5 | ## Table of Contents 6 | - [Plugin Configuration](#plugin-configuration) 7 | - [FilamentShield Integration](#filamentshield-integration) 8 | - [Tenant Access Control](#tenant-access-control) 9 | - [API Access from Desktop Applications](#api-access-from-desktop-applications) 10 | - [API Documentation with Scramble](#api-documentation-with-scramble) 11 | - [Token Resource Navigation](#token-resource-navigation) 12 | - [Resources in Clusters](#resources-in-clusters) 13 | - [Publishing Package Resources](#publishing-package-resources) 14 | 15 | ## Plugin Configuration 16 | 17 | ### Error After Integration with Filament 4 / Laravel 12 (Issue #121) 18 | 19 | **Problem:** After installing the package with Filament 4 and Laravel 12, artisan commands fail with errors. 20 | 21 | **Investigation Steps:** 22 | 23 | 1. **Verify Laravel and Filament versions:** 24 | ```bash 25 | composer show | grep laravel/framework 26 | composer show | grep filament/filament 27 | ``` 28 | 29 | The package supports: 30 | - Laravel 11.0+ or 12.0+ 31 | - Filament 4.0+ 32 | - PHP 8.2+ 33 | 34 | 2. **Check for conflicting dependencies:** 35 | ```bash 36 | composer why-not rupadana/filament-api-service 37 | ``` 38 | 39 | 3. **Ensure all migrations are run:** 40 | ```bash 41 | php artisan install:api # For Laravel 11+ 42 | php artisan migrate 43 | ``` 44 | 45 | 4. **Clear all caches:** 46 | ```bash 47 | php artisan config:clear 48 | php artisan cache:clear 49 | php artisan route:clear 50 | php artisan view:clear 51 | ``` 52 | 53 | 5. **Check plugin registration:** 54 | Make sure the plugin is properly registered in your panel provider: 55 | 56 | ```php 57 | use Rupadana\ApiService\ApiServicePlugin; 58 | 59 | public function panel(Panel $panel): Panel 60 | { 61 | return $panel 62 | // ... other configuration 63 | ->plugins([ 64 | ApiServicePlugin::make(), 65 | ]); 66 | } 67 | ``` 68 | 69 | 6. **Enable debug mode to see detailed errors:** 70 | ```env 71 | APP_DEBUG=true 72 | ``` 73 | 74 | If the issue persists, please provide: 75 | - The exact error message 76 | - Your `composer.json` dependencies 77 | - Your panel provider configuration 78 | - Output of `php artisan about` 79 | 80 | ### Plugin Load Order (Issue #108) 81 | 82 | **Problem:** When using FilamentShield or other plugins, you may encounter errors if ApiServicePlugin is registered before other required plugins. 83 | 84 | **Solution:** Make sure ApiServicePlugin is registered **after** FilamentShieldPlugin: 85 | 86 | ```php 87 | use BezhanSalleh\FilamentShield\FilamentShieldPlugin; 88 | use Rupadana\ApiService\ApiServicePlugin; 89 | 90 | $panel->plugins([ 91 | FilamentShieldPlugin::make(), 92 | ApiServicePlugin::make(), // Load ApiServicePlugin AFTER FilamentShieldPlugin 93 | ]) 94 | ``` 95 | 96 | If you encounter errors, enable debug mode in your `.env` file to see detailed error messages: 97 | 98 | ```env 99 | APP_DEBUG=true 100 | ``` 101 | 102 | The package now logs errors when debug mode is enabled, making it easier to diagnose configuration issues. 103 | 104 | ## FilamentShield Integration (Issue #112) 105 | 106 | **Problem:** Getting "User does not have the right permissions" error when using filament-shield integration. 107 | 108 | **Solution:** The package uses Spatie's permission middleware by default. Follow these steps: 109 | 110 | 1. **Setup FilamentShield:** 111 | ```bash 112 | php artisan shield:setup --fresh 113 | php artisan shield:install 114 | ``` 115 | 116 | 2. **Generate permissions for all resources:** 117 | ```bash 118 | php artisan shield:generate --all 119 | ``` 120 | 121 | 3. **Verify permissions exist:** 122 | The command should create permissions for your API handlers (e.g., `view_blog`, `create_blog`, `update_blog`, `delete_blog`). 123 | 124 | 4. **Assign permissions to roles:** 125 | Use Filament Shield's UI to assign the generated permissions to your roles. 126 | 127 | 5. **Alternative - Disable permission middleware:** 128 | If you don't want to use the permission middleware, set this in your config: 129 | 130 | ```php 131 | // config/api-service.php 132 | return [ 133 | 'use-spatie-permission-middleware' => false, 134 | ]; 135 | ``` 136 | 137 | ## Tenant Access Control (Issue #94) 138 | 139 | **Problem:** Users can access all tenants via API, not just their assigned tenants. 140 | 141 | **Solution:** The package respects Filament's tenancy configuration, but you need to ensure: 142 | 143 | 1. **Enable tenancy in the config:** 144 | ```php 145 | // config/api-service.php 146 | return [ 147 | 'tenancy' => [ 148 | 'enabled' => true, 149 | 'awareness' => false, // Set to true only if you want tenant-aware routes 150 | ], 151 | ]; 152 | ``` 153 | 154 | 2. **Implement proper authorization:** 155 | Make sure your resource policies check tenant membership: 156 | 157 | ```php 158 | // app/Policies/BlogPolicy.php 159 | public function view(User $user, Blog $blog): bool 160 | { 161 | // Check if user belongs to the blog's tenant 162 | return $user->tenants->contains($blog->tenant_id); 163 | } 164 | ``` 165 | 166 | 3. **Tenant-aware routes (optional):** 167 | If you want routes to include tenant slugs (e.g., `/api/admin/{tenant}/blogs`), set: 168 | 169 | ```php 170 | // config/api-service.php 171 | return [ 172 | 'route' => [ 173 | 'panel_prefix' => true, // Required for tenant awareness 174 | ], 175 | 'tenancy' => [ 176 | 'enabled' => true, 177 | 'awareness' => true, // Enable tenant-aware routes 178 | ], 179 | ]; 180 | ``` 181 | 182 | ## API Access from Desktop Applications (Issue #105) 183 | 184 | **Problem:** Desktop application can access API locally but gets redirected to login when deployed online. 185 | 186 | **Solution:** This is typically a middleware or authentication issue: 187 | 188 | 1. **Check middleware configuration:** 189 | Make sure your API routes use `sanctum` middleware, not `auth` or `web`: 190 | 191 | ```php 192 | // The package already uses the correct middleware by default 193 | // But verify your config/api-service.php: 194 | return [ 195 | 'logout-middleware' => [ 196 | 'auth:sanctum', // Correct 197 | ], 198 | ]; 199 | ``` 200 | 201 | 2. **Verify CORS configuration:** 202 | If your desktop app is accessing the API from a different domain, configure CORS in `config/cors.php`: 203 | 204 | ```php 205 | return [ 206 | 'paths' => ['api/*', 'sanctum/csrf-cookie'], 207 | 'allowed_origins' => ['*'], // Or specify your desktop app's domain 208 | 'allowed_methods' => ['*'], 209 | 'allowed_headers' => ['*'], 210 | 'exposed_headers' => [], 211 | 'max_age' => 0, 212 | 'supports_credentials' => true, 213 | ]; 214 | ``` 215 | 216 | 3. **Use token authentication:** 217 | Desktop applications should use token-based authentication: 218 | 219 | ```php 220 | // Desktop app should send token in Authorization header 221 | Authorization: Bearer {your-token} 222 | ``` 223 | 224 | 4. **Check session driver:** 225 | Make sure you're not using session-based authentication for API routes. Sanctum should use token authentication for API requests. 226 | 227 | ## API Documentation with Scramble (Issue #101) 228 | 229 | **Problem:** Scramble API docs don't show filter/sort options or proper transformer responses. 230 | 231 | **Current Limitation:** This is a known limitation of how Scramble generates documentation from the package's dynamic routes. 232 | 233 | **Workaround:** 234 | 235 | 1. **Document filters and sorts manually:** 236 | Add PHPDoc annotations to your handlers: 237 | 238 | ```php 239 | /** 240 | * Get paginated list of products 241 | * 242 | * @queryParam filter[name] Filter by product name 243 | * @queryParam sort Sort by field (e.g., name, created_at) 244 | * @queryParam fields[products] Select specific fields (e.g., id,name,price) 245 | */ 246 | public function handler(): JsonResponse 247 | { 248 | // Your handler logic 249 | } 250 | ``` 251 | 252 | 2. **Transformers:** 253 | Scramble may not automatically detect custom transformers. The API response will follow your transformer structure at runtime, but the documentation might show the raw model structure. 254 | 255 | ## Token Resource Navigation (Issue #88) 256 | 257 | **Problem:** Unable to hide the Token resource from navigation. 258 | 259 | **Solution:** The config setting works correctly. To hide the Token resource: 260 | 261 | ```php 262 | // config/api-service.php 263 | return [ 264 | 'navigation' => [ 265 | 'token' => [ 266 | 'should_register_navigation' => false, // This hides the navigation 267 | ], 268 | ], 269 | ]; 270 | ``` 271 | 272 | To **show** the Token resource in navigation: 273 | 274 | ```php 275 | return [ 276 | 'navigation' => [ 277 | 'token' => [ 278 | 'should_register_navigation' => true, // This shows the navigation 279 | ], 280 | ], 281 | ]; 282 | ``` 283 | 284 | Make sure to clear your config cache after making changes: 285 | 286 | ```bash 287 | php artisan config:clear 288 | ``` 289 | 290 | ## Resources in Clusters (Issues #118, #119, #120) 291 | 292 | **Problem:** Cannot create API services for resources in Filament clusters. 293 | 294 | **Current Status:** The package now supports auto-discovery of API services in clusters (as of version 4.0.1). However, creating new API services for clustered resources requires manual steps. 295 | 296 | **Workaround for creating API services:** 297 | 298 | 1. **Create the resource first using Filament:** 299 | ```bash 300 | php artisan make:filament-resource Product --cluster=Catalog 301 | ``` 302 | 303 | 2. **Manually create API service structure:** 304 | For a resource at `App\Filament\Clusters\Catalog\Resources\ProductResource`, create: 305 | 306 | ``` 307 | app/Filament/Clusters/Catalog/Resources/ 308 | └── Products/ 309 | ├── ProductResource.php 310 | └── Api/ 311 | ├── ProductApiService.php 312 | ├── Transformers/ 313 | │ └── ProductTransformer.php 314 | └── Handlers/ 315 | ├── CreateHandler.php 316 | ├── UpdateHandler.php 317 | ├── DeleteHandler.php 318 | ├── DetailHandler.php 319 | └── PaginationHandler.php 320 | ``` 321 | 322 | 3. **Copy and adapt from a working API service:** 323 | The easiest approach is to copy from `tests/Fixtures/Resources/Product/Api/` and adapt the namespaces. 324 | 325 | **Note:** Auto-discovery works automatically once the API service structure is in place. 326 | 327 | ## Publishing Package Resources (Issue #90) 328 | 329 | **Problem:** Cannot publish the package's Filament resources (like TokenResource). 330 | 331 | **Explanation:** The package's Filament resources (like `TokenResource`) are designed to be configured, not published. This is intentional to ensure the package works out of the box. 332 | 333 | **Customization Options:** 334 | 335 | 1. **Configure the Token resource:** 336 | ```php 337 | // config/api-service.php 338 | return [ 339 | 'navigation' => [ 340 | 'token' => [ 341 | 'cluster' => null, 342 | 'group' => 'User', 343 | 'sort' => -1, 344 | 'icon' => 'heroicon-o-key', 345 | 'should_register_navigation' => false, 346 | ], 347 | ], 348 | 'models' => [ 349 | 'token' => [ 350 | 'enable_policy' => true, 351 | ], 352 | ], 353 | ]; 354 | ``` 355 | 356 | 2. **Extend the Token resource:** 357 | Create your own resource that extends the package's TokenResource: 358 | 359 | ```php 360 | namespace App\Filament\Resources; 361 | 362 | use Rupadana\ApiService\Resources\TokenResource as BaseTokenResource; 363 | 364 | class CustomTokenResource extends BaseTokenResource 365 | { 366 | // Override methods as needed 367 | public static function getNavigationIcon(): ?string 368 | { 369 | return 'heroicon-o-lock-closed'; 370 | } 371 | } 372 | ``` 373 | 374 | Then register your custom resource instead: 375 | 376 | ```php 377 | // In your panel provider 378 | use App\Filament\Resources\CustomTokenResource; 379 | 380 | $panel->resources([ 381 | CustomTokenResource::class, 382 | ]); 383 | ``` 384 | 385 | ## Feature Requests 386 | 387 | ### API Versioning (Issue #68) 388 | 389 | **Status:** Feature request for future consideration. 390 | 391 | **Current Workaround:** 392 | - Use custom transformers to handle different response formats 393 | - Implement versioning at the application level using route prefixes 394 | - Create separate panels for different API versions 395 | 396 | If you need this feature, please upvote the issue and describe your use case to help prioritize development. 397 | 398 | ## Getting Help 399 | 400 | If you encounter an issue not covered in this guide: 401 | 402 | 1. Check the [main documentation](README.md) 403 | 2. Search [existing issues](https://github.com/rupadana/filament-api-service/issues) 404 | 3. Create a new issue with: 405 | - Detailed description of the problem 406 | - Steps to reproduce 407 | - Your environment (PHP version, Laravel version, Filament version) 408 | - Relevant code snippets 409 | - Error messages or logs 410 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to `api-service` will be documented in this file. 4 | 5 | ## 4.0.3 - 2025-11-06 6 | 7 | ### What's Changed 8 | 9 | * Optimize API request handling with caching and early returns by @Copilot in https://github.com/rupadana/filament-api-service/pull/127 10 | 11 | **Full Changelog**: https://github.com/rupadana/filament-api-service/compare/4.0.2...4.0.3 12 | 13 | ## 4.0.2 - 2025-11-06 14 | 15 | ### What's Changed 16 | 17 | * Fix invisible prompts, improve cluster support, and add comprehensive troubleshooting guide by @Copilot in https://github.com/rupadana/filament-api-service/pull/126 18 | * Upgrade dedoc/scramble dependency to version 0.13 by @marcogermani87 in https://github.com/rupadana/filament-api-service/pull/128 19 | 20 | ### New Contributors 21 | 22 | * @Copilot made their first contribution in https://github.com/rupadana/filament-api-service/pull/126 23 | 24 | **Full Changelog**: https://github.com/rupadana/filament-api-service/compare/4.0.1...4.0.2 25 | 26 | ## 3.4.10 - 2025-10-07 27 | 28 | **Full Changelog**: https://github.com/rupadana/filament-api-service/compare/3.4.9...3.4.10 29 | 30 | ## 4.0.1 - 2025-09-30 31 | 32 | ### What's Changed 33 | 34 | * Update TokenResource.php to allow NavigationGroup translation by @santihobby in https://github.com/rupadana/filament-api-service/pull/123 35 | * Change resource path location by @rupadana in https://github.com/rupadana/filament-api-service/pull/124 36 | 37 | ### New Contributors 38 | 39 | * @santihobby made their first contribution in https://github.com/rupadana/filament-api-service/pull/123 40 | 41 | **Full Changelog**: https://github.com/rupadana/filament-api-service/compare/4.0.0...4.0.1 42 | 43 | ## 4.0.0 - 2025-08-14 44 | 45 | Special Thanks to [marcogermani87](https://github.com/marcogermani87) for migrate the plugin to support on Filament v4 🔥 46 | 47 | ### What's Changed 48 | 49 | * Migration to Filament v4 by @marcogermani87 in https://github.com/rupadana/filament-api-service/pull/117 50 | 51 | **Full Changelog**: https://github.com/rupadana/filament-api-service/compare/3.4.9...4.0.0 52 | 53 | ## 3.4.9 - 2025-07-17 54 | 55 | ### What's Changed 56 | 57 | * Adding any additional middleware you want to apply to the logout and login route using config by @husam-tariq in https://github.com/rupadana/filament-api-service/pull/115 58 | 59 | ### New Contributors 60 | 61 | * @husam-tariq made their first contribution in https://github.com/rupadana/filament-api-service/pull/115 62 | 63 | **Full Changelog**: https://github.com/rupadana/filament-api-service/compare/3.4.8...3.4.9 64 | 65 | ## 3.4.8 - 2025-07-01 66 | 67 | ### What's Changed 68 | 69 | * Fix issue #113 by @hamrak in https://github.com/rupadana/filament-api-service/pull/114 70 | 71 | ### New Contributors 72 | 73 | * @hamrak made their first contribution in https://github.com/rupadana/filament-api-service/pull/114 74 | 75 | **Full Changelog**: https://github.com/rupadana/filament-api-service/compare/3.4.7...3.4.8 76 | 77 | ## 3.4.7 - 2025-07-01 78 | 79 | ### What's Changed 80 | 81 | * Bump dependabot/fetch-metadata from 2.3.0 to 2.4.0 by @dependabot in https://github.com/rupadana/filament-api-service/pull/106 82 | * [Feature] make the stubs publishable by @LizardEngineer in https://github.com/rupadana/filament-api-service/pull/109 83 | * feat: add the ability to show / hide token resource from config file by @aymanalhattami in https://github.com/rupadana/filament-api-service/pull/110 84 | 85 | ### New Contributors 86 | 87 | * @LizardEngineer made their first contribution in https://github.com/rupadana/filament-api-service/pull/109 88 | * @aymanalhattami made their first contribution in https://github.com/rupadana/filament-api-service/pull/110 89 | 90 | **Full Changelog**: https://github.com/rupadana/filament-api-service/compare/3.4.6...3.4.7 91 | 92 | ## 3.4.6 - 2025-05-07 93 | 94 | - Fix typo on AuthController #104 95 | **Full Changelog**: https://github.com/rupadana/filament-api-service/compare/3.4.5...3.4.6 96 | 97 | ## 3.4.5 - 2025-03-21 98 | 99 | ### What's Changed 100 | 101 | * Small update to work on other SQL databases by @b7s in https://github.com/rupadana/filament-api-service/pull/100 102 | 103 | ### New Contributors 104 | 105 | * @b7s made their first contribution in https://github.com/rupadana/filament-api-service/pull/100 106 | 107 | **Full Changelog**: https://github.com/rupadana/filament-api-service/compare/3.4.4...3.4.5 108 | 109 | ## 3.4.4 - 2025-03-03 110 | 111 | ### What's Changed 112 | 113 | * Add it translation by @marcogermani87 in https://github.com/rupadana/filament-api-service/pull/97 114 | 115 | ### New Contributors 116 | 117 | * @marcogermani87 made their first contribution in https://github.com/rupadana/filament-api-service/pull/97 118 | 119 | **Full Changelog**: https://github.com/rupadana/filament-api-service/compare/3.4.3...3.4.4 120 | 121 | ## 3.4.3 - 2025-02-28 122 | 123 | ### What's Changed 124 | 125 | * Changed to getRouteKeyName() instead of ->getKeyName() by @rupadana in https://github.com/rupadana/filament-api-service/pull/85 126 | * Bump tsickert/discord-webhook from 5.5.0 to 6.0.0 by @dependabot in https://github.com/rupadana/filament-api-service/pull/52 127 | * Bump dependabot/fetch-metadata from 2.2.0 to 2.3.0 by @dependabot in https://github.com/rupadana/filament-api-service/pull/87 128 | * Localizing "labels" for Model / Columns/ Fields / Actions / Sections … by @GeWoHaGeChSc in https://github.com/rupadana/filament-api-service/pull/96 129 | * add support for laravel 12 by @atmonshi in https://github.com/rupadana/filament-api-service/pull/95 130 | 131 | ### New Contributors 132 | 133 | * @GeWoHaGeChSc made their first contribution in https://github.com/rupadana/filament-api-service/pull/96 134 | 135 | **Full Changelog**: https://github.com/rupadana/filament-api-service/compare/3.4.2...3.4.3 136 | 137 | ## 3.4.2 - 2025-01-19 138 | 139 | ### What's Changed 140 | 141 | * fix: Bug on allowed fields, filters, sorts and includes by @rupadana in https://github.com/rupadana/filament-api-service/pull/84 142 | 143 | **Full Changelog**: https://github.com/rupadana/filament-api-service/compare/3.4.1...3.4.2 144 | 145 | ## 3.4.1 - 2025-01-19 146 | 147 | ### What's Changed 148 | 149 | * Fix Tenancy Bug by @rupadana in https://github.com/rupadana/filament-api-service/pull/83 150 | 151 | **Full Changelog**: https://github.com/rupadana/filament-api-service/compare/3.4.0...3.4.1 152 | 153 | ## 3.4.0 - 2025-01-18 154 | 155 | ### What's Changed 156 | 157 | * adding scramble for documentation and built-in authentication by @rupadana in https://github.com/rupadana/filament-api-service/pull/82 158 | 159 | **Full Changelog**: https://github.com/rupadana/filament-api-service/compare/3.3.5...3.4.0 160 | 161 | ## 3.3.5 - 2025-01-18 162 | 163 | ### What's Changed 164 | 165 | * make api abilities translateable by @iTzSofteis in https://github.com/rupadana/filament-api-service/pull/81 166 | 167 | ### New Contributors 168 | 169 | * @iTzSofteis made their first contribution in https://github.com/rupadana/filament-api-service/pull/81 170 | 171 | **Full Changelog**: https://github.com/rupadana/filament-api-service/compare/3.3.4...3.3.5 172 | 173 | ## 3.3.4 - 2024-11-28 174 | 175 | ### What's Changed 176 | 177 | * refactor: change fields method from instance to static access by @mohammadisa2 in https://github.com/rupadana/filament-api-service/pull/70 178 | * Update README.md by @christmex in https://github.com/rupadana/filament-api-service/pull/80 179 | 180 | ### New Contributors 181 | 182 | * @mohammadisa2 made their first contribution in https://github.com/rupadana/filament-api-service/pull/70 183 | 184 | **Full Changelog**: https://github.com/rupadana/filament-api-service/compare/3.3.3...3.3.4 185 | 186 | ## 3.3.3 - 2024-11-28 187 | 188 | ### What's Changed 189 | 190 | * change instanceOf to is_subclass_of by @christmex in https://github.com/rupadana/filament-api-service/pull/79 191 | 192 | **Full Changelog**: https://github.com/rupadana/filament-api-service/compare/3.3.2...3.3.3 193 | 194 | ## 3.3.2 - 2024-10-23 195 | 196 | ### What's Changed 197 | 198 | * Support spatie/laravel-query-builder to ^6.2 by @rupadana in https://github.com/rupadana/filament-api-service/pull/72 199 | * Refer to policy before checking user role by @Aniruddh-J in https://github.com/rupadana/filament-api-service/pull/75 200 | 201 | ### New Contributors 202 | 203 | * @Aniruddh-J made their first contribution in https://github.com/rupadana/filament-api-service/pull/75 204 | 205 | **Full Changelog**: https://github.com/rupadana/filament-api-service/compare/3.3.1...3.3.2 206 | 207 | ## 3.3.1 - 2024-08-28 208 | 209 | **Full Changelog**: https://github.com/rupadana/filament-api-service/compare/3.3.0...3.3.1 210 | 211 | ## 3.3.0 - 2024-08-13 212 | 213 | ### What's Changed 214 | 215 | * add notes to fix personal_access_token missing in laravel 11 by @christmex in https://github.com/rupadana/filament-api-service/pull/59 216 | * Bump dependabot/fetch-metadata from 2.1.0 to 2.2.0 by @dependabot in https://github.com/rupadana/filament-api-service/pull/64 217 | * Allow the usage of more complex filter methods by Spatie\QueryBuilder by @bfiessinger in https://github.com/rupadana/filament-api-service/pull/67 218 | 219 | ### New Contributors 220 | 221 | * @christmex made their first contribution in https://github.com/rupadana/filament-api-service/pull/59 222 | * @bfiessinger made their first contribution in https://github.com/rupadana/filament-api-service/pull/67 223 | 224 | **Full Changelog**: https://github.com/rupadana/filament-api-service/compare/3.2.4...3.3.0 225 | 226 | ## 3.2.4 - 2024-06-04 227 | 228 | ### What's Changed 229 | 230 | * Bump tsickert/discord-webhook from 5.4.0 to 5.5.0 by @dependabot in https://github.com/rupadana/filament-api-service/pull/50 231 | * Bump dependabot/fetch-metadata from 2.0.0 to 2.1.0 by @dependabot in https://github.com/rupadana/filament-api-service/pull/53 232 | * Add ability to use FilamentPHP Clusters feature by @carlosbarretoeng in https://github.com/rupadana/filament-api-service/pull/54 233 | 234 | ### New Contributors 235 | 236 | * @carlosbarretoeng made their first contribution in https://github.com/rupadana/filament-api-service/pull/54 237 | 238 | **Full Changelog**: https://github.com/rupadana/filament-api-service/compare/3.2.3...3.2.4 239 | 240 | ## 3.2.3 - 2024-03-28 241 | 242 | ### What's Changed 243 | 244 | * Fix failure to produce proper tenant query when using a polymorphic tenancy by @JonErickson in https://github.com/rupadana/filament-api-service/pull/49 245 | 246 | ### New Contributors 247 | 248 | * @JonErickson made their first contribution in https://github.com/rupadana/filament-api-service/pull/49 249 | 250 | **Full Changelog**: https://github.com/rupadana/filament-api-service/compare/3.2.2...3.2.3 251 | 252 | ## 3.2.2 - 2024-03-26 253 | 254 | ### What's Changed 255 | 256 | * Bump dependabot/fetch-metadata from 1.6.0 to 2.0.0 by @dependabot in https://github.com/rupadana/filament-api-service/pull/47 257 | * fix(bug): failed to get allowedFilters, etc. by @rupadana in https://github.com/rupadana/filament-api-service/pull/48 258 | 259 | **Full Changelog**: https://github.com/rupadana/filament-api-service/compare/3.2.1...3.2.2 260 | 261 | ## 3.2.1 - 2024-03-21 262 | 263 | ### What's Changed 264 | 265 | * fix: unusefull static::handlers() by @rupadana in https://github.com/rupadana/filament-api-service/pull/46 266 | 267 | **Full Changelog**: https://github.com/rupadana/filament-api-service/compare/3.2.0...3.2.1 268 | 269 | ## 3.2.0 - 2024-03-18 270 | 271 | ### What's Changed 272 | 273 | * Bump tsickert/discord-webhook from 5.3.0 to 5.4.0 by @dependabot in https://github.com/rupadana/filament-api-service/pull/43 274 | * add Laravel 11 support by @atmonshi in https://github.com/rupadana/filament-api-service/pull/21 275 | 276 | ### New Contributors 277 | 278 | * @atmonshi made their first contribution in https://github.com/rupadana/filament-api-service/pull/21 279 | 280 | **Full Changelog**: https://github.com/rupadana/filament-api-service/compare/3.1.4...3.2.0 281 | 282 | ## 3.1.4 - 2024-03-17 283 | 284 | ### What's Changed 285 | 286 | * Fix: missing Request on DetailHandler Stubs by @rupadana in https://github.com/rupadana/filament-api-service/pull/40 287 | 288 | **Full Changelog**: https://github.com/rupadana/filament-api-service/compare/3.1.3...3.1.4 289 | 290 | ## 3.1.3 - 2024-03-16 291 | 292 | **Full Changelog**: https://github.com/rupadana/filament-api-service/compare/3.1.2...3.1.3 293 | 294 | ### What's Changed 295 | 296 | * allow for setting middlewares via config and via Filament Resources by @eelco2k in https://github.com/rupadana/filament-api-service/pull/35 297 | * feat: Middleware API on ApiServicePlugin class by @rupadana in https://github.com/rupadana/filament-api-service/pull/39 298 | 299 | **Full Changelog**: https://github.com/rupadana/filament-api-service/compare/3.1.2...3.1.3 300 | 301 | ## 3.1.2 - 2024-03-10 302 | 303 | **Full Changelog**: https://github.com/rupadana/filament-api-service/compare/3.1.1...3.1.2 304 | 305 | ## 3.1.1 - 2024-03-10 306 | 307 | ### What's Changed 308 | 309 | * fix: adding panel name by @rupadana in https://github.com/rupadana/filament-api-service/pull/34 310 | 311 | **Full Changelog**: https://github.com/rupadana/filament-api-service/compare/3.1.0...3.1.1 312 | 313 | ## 3.1.0 - 2024-03-10 314 | 315 | ### What's Changed 316 | 317 | * Update README.md by @rupadana in https://github.com/rupadana/filament-api-service/pull/27 318 | * change: adding test for panel_prefix config by @rupadana in https://github.com/rupadana/filament-api-service/pull/28 319 | * added tenant aware api feature by @eelco2k in https://github.com/rupadana/filament-api-service/pull/23 320 | * fix: adding an InvalidTenancyConfiguration by @rupadana in https://github.com/rupadana/filament-api-service/pull/30 321 | * Feature tenant aware Refactored by @eelco2k in https://github.com/rupadana/filament-api-service/pull/32 322 | * Panel Prefix first by Path otherwise byId else empty by @eelco2k in https://github.com/rupadana/filament-api-service/pull/33 323 | * feat: Tenancy Support by @rupadana in https://github.com/rupadana/filament-api-service/pull/31 324 | 325 | ### New Contributors 326 | 327 | * @eelco2k made their first contribution in https://github.com/rupadana/filament-api-service/pull/23 328 | 329 | **Full Changelog**: https://github.com/rupadana/filament-api-service/compare/3.0.10...3.1.0 330 | 331 | ## 3.0.10 - 2024-03-06 332 | 333 | ### What's Changed 334 | 335 | * update Readme.md by @rupadana in https://github.com/rupadana/filament-api-service/pull/25 336 | * feat: add navigation sort & icon by @rupadana in https://github.com/rupadana/filament-api-service/pull/26 337 | 338 | **Full Changelog**: https://github.com/rupadana/filament-api-service/compare/3.0.9...3.0.10 339 | 340 | ## 3.0.9 - 2024-03-02 341 | 342 | ### What's Changed 343 | 344 | * feat: Allow non-admin users to generate API key to access their records by @rupadana in https://github.com/rupadana/filament-api-service/pull/22 345 | 346 | **Full Changelog**: https://github.com/rupadana/filament-api-service/compare/3.0.8...3.0.9 347 | 348 | ## 3.0.8 - 2024-02-09 349 | 350 | ### What's Changed 351 | 352 | * Public api by @rupadana in https://github.com/rupadana/filament-api-service/pull/20 353 | 354 | **Full Changelog**: https://github.com/rupadana/filament-api-service/compare/3.0.7...3.0.8 355 | 356 | ## 3.0.7 - 2024-02-08 357 | 358 | ### What's Changed 359 | 360 | * adding configuration to enable/disable policy by @rupadana in https://github.com/rupadana/filament-api-service/pull/19 361 | 362 | **Full Changelog**: https://github.com/rupadana/filament-api-service/compare/3.0.6...3.0.7 363 | 364 | ## 3.0.6 - 2024-01-29 365 | 366 | **Full Changelog**: https://github.com/rupadana/filament-api-service/compare/3.0.5...3.0.6 367 | 368 | ## 3.0.5 - 2024-01-29 369 | 370 | **Full Changelog**: https://github.com/rupadana/filament-api-service/compare/3.0.2...3.0.5 371 | 372 | ## 3.0.4 - 2024-01-27 373 | 374 | **Full Changelog**: https://github.com/rupadana/filament-api-service/compare/3.0.1...3.0.4 375 | 376 | ## 3.0.0 - 2024-01-11 377 | 378 | ### What's Changed 379 | 380 | * Bump stefanzweifel/git-auto-commit-action from 4 to 5 by @dependabot in https://github.com/rupadana/filament-api-service/pull/7 381 | * Bump aglipanci/laravel-pint-action from 2.3.0 to 2.3.1 by @dependabot in https://github.com/rupadana/filament-api-service/pull/9 382 | * Add test suite by @luttje in https://github.com/rupadana/filament-api-service/pull/10 383 | * Auto register routes by @rupadana in https://github.com/rupadana/filament-api-service/pull/14 384 | 385 | ### New Contributors 386 | 387 | * @luttje made their first contribution in https://github.com/rupadana/filament-api-service/pull/10 388 | 389 | **Full Changelog**: https://github.com/rupadana/filament-api-service/compare/v1.0.3...3.0.0 390 | 391 | ## Add AllowedSorts - 2023-10-07 392 | 393 | ### What's Changed 394 | 395 | - Add: Support for allowed sorts by @paulovnas in https://github.com/rupadana/filament-api-service/pull/6 396 | 397 | ### New Contributors 398 | 399 | - @paulovnas made their first contribution in https://github.com/rupadana/filament-api-service/pull/6 400 | 401 | **Full Changelog**: https://github.com/rupadana/filament-api-service/compare/v1.0.2...v1.0.3 402 | 403 | ## Add CreateHandler - 2023-10-03 404 | 405 | **Full Changelog**: https://github.com/rupadana/filament-api-service/compare/v1.0.1...v1.0.2 406 | 407 | ## Update bug - 2023-09-09 408 | 409 | ### What's Changed 410 | 411 | - Bump actions/checkout from 3 to 4 by @dependabot in https://github.com/rupadana/filament-api-service/pull/5 412 | 413 | ### New Contributors 414 | 415 | - @dependabot made their first contribution in https://github.com/rupadana/filament-api-service/pull/5 416 | 417 | **Full Changelog**: https://github.com/rupadana/filament-api-service/compare/v1.0.0...v1.0.1 418 | 419 | ## First Release - 2023-09-04 420 | 421 | ### What's Changed 422 | 423 | - update readme.md by @rupadana in https://github.com/rupadana/filament-api-service/pull/1 424 | - 1.0.0 by @rupadana in https://github.com/rupadana/filament-api-service/pull/2 425 | - Add API Transformer & Update Readme by @rupadana in https://github.com/rupadana/filament-api-service/pull/3 426 | - add default transformer by @rupadana in https://github.com/rupadana/filament-api-service/pull/4 427 | 428 | ### New Contributors 429 | 430 | - @rupadana made their first contribution in https://github.com/rupadana/filament-api-service/pull/1 431 | 432 | **Full Changelog**: https://github.com/rupadana/filament-api-service/commits/v1.0.0 433 | 434 | ## 1.0.0 - 202X-XX-XX 435 | 436 | - initial release 437 | --------------------------------------------------------------------------------