# Plugin routes and permissions

> Add API routes to a FeatherPanel plugin: helpers for public, user, admin, server and VDS routes, rate limits, and role and subuser permission checks.

Source: https://featherpanel.com/docs/plugins/routes-and-permissions/

## Route files

Every PHP file in your plugin's `Routes/` folder is loaded together with the core routes. A route file returns a
function that receives the Symfony `RouteCollection`:

```php
<?php

use App\App;
use App\Helpers\ApiResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Routing\RouteCollection;
use App\Addons\myplugin\Controllers\StatusController;

return function (RouteCollection $routes): void {
    App::getInstance(true)->registerApiRoute(
        $routes,
        'myplugin-status',
        '/api/addons/myplugin/status',
        function (Request $request) {
            return (new StatusController())->index($request);
        }
    );
};
```

- The route name (second argument) must be unique across the panel. Prefix it with your identifier.
- Put plugin routes under `/api/addons/<identifier>/` so they never clash with core routes.
- An exception in a plugin route file is logged and the file is skipped; the rest of the panel keeps working.
- A route file whose name matches another addon's identifier is skipped.

## Which helper to use

All helpers are methods on `App::getInstance(true)`. They share the arguments `$routes, $name, $path, $controller`,
then helper-specific ones, then `array $methods = ['GET']`, `?Rate $rateLimit = null` and
`?string $rateLimitNamespace = null`.

| Helper | Middleware | Extra argument | Use for |
| --- | --- | --- | --- |
| `registerApiRoute` | none | | Public endpoints, webhooks |
| `registerAuthRoute` | logged-in user (session or API key) | | Anything for a signed-in user |
| `registerAdminRoute` | logged-in user + permission check | `$permission` | Admin endpoints |
| `registerServerRoute` | logged-in user + server access | `$serverShortUuid`, the name of the path parameter | `/api/user/servers/{uuidShort}/...` endpoints |
| `registerVmInstanceRoute` | logged-in user + VM instance access | the path parameter name | VDS endpoints |
| `registerWingsRoute`, `registerQuilldRoute` | daemon token | | Endpoints called by FeatherWings or FeatherQuilld |

Controllers get the request and the path parameters:

```php
App::getInstance(true)->registerServerRoute(
    $routes,
    'myplugin-server-info',
    '/api/user/servers/{uuidShort}/myplugin/info',
    function (Request $request, array $args) {
        $server = \App\Chat\Server::getServerByUuidShort($args['uuidShort'] ?? '');
        if (!$server) {
            return ApiResponse::error('Server not found', 'SERVER_NOT_FOUND', 404);
        }

        return ApiResponse::success(['name' => $server['name']]);
    },
    'uuidShort',
    ['GET']
);
```

The signed-in user is in `$request->attributes->get('user')`. Return `ApiResponse::success($data, $message, $status)`
or `ApiResponse::error($message, $code, $status, $data)` so your responses use the
[same envelope](https://featherpanel.com/docs/api/rate-limits-and-errors/) as the rest of the API.

## Admin permissions

`registerAdminRoute` takes a permission node. The request is refused with `NOT_AUTHORIZED` (403) unless one of the
user's roles has that node or `admin.root`.

```php
use App\Permissions;

App::getInstance(true)->registerAdminRoute(
    $routes,
    'myplugin-admin-settings',
    '/api/admin/myplugin/settings',
    fn (Request $request) => (new AdminController())->settings($request),
    Permissions::ADMIN_PLUGINS_MANAGE,
    ['GET', 'PATCH']
);
```

You can reuse a built-in node from the [permission list](https://featherpanel.com/docs/reference/permissions/#admin) or pass your own string,
such as `'myplugin.manage'`. Custom nodes are not shown in the role editor, which lists built-in nodes only; add them
to a role through the API (`PUT /api/admin/permissions` with `role_id` and `permission`).

To check a node inside a controller:

```php
use App\Helpers\PermissionHelper;

if (!PermissionHelper::hasPermission($user['uuid'], 'myplugin.manage')) {
    return ApiResponse::error('Not allowed', 'NOT_AUTHORIZED', 403);
}
```

## Server and subuser permissions

`registerServerRoute` lets in the owner, subusers and admins who can see the server. It does not check what a subuser
may do. Do that in the controller with `SubuserPermissionChecker`:

```php
use App\Helpers\SubuserPermissionChecker;
use App\SubuserPermissions;

$user = $request->attributes->get('user');
if (!SubuserPermissionChecker::hasPermission($user['id'], $server['id'], SubuserPermissions::FILE_READ)) {
    return ApiResponse::error('Insufficient permissions', 'FORBIDDEN', 403);
}
```

`hasAnyPermission()` and `hasAllPermissions()` take a list. The available nodes are listed under
[server subuser permissions](https://featherpanel.com/docs/reference/permissions/#server-subusers).

## Rate limits

Pass a default with `Rate`:

```php
use RateLimit\Rate;

App::getInstance(true)->registerApiRoute(
    $routes,
    'myplugin-webhook',
    '/api/addons/myplugin/webhook',
    fn (Request $request) => (new WebhookController())->handle($request),
    ['POST'],
    Rate::perMinute(30),
    'myplugin-webhook'
);
```

Admins can override any route's limit by its route name in **Admin > Rate Limits**. See
[Rate limits and errors](https://featherpanel.com/docs/api/rate-limits-and-errors/) for how limits are counted.

## Related

- [Frontend](https://featherpanel.com/docs/plugins/frontend/) for calling your routes from plugin pages
- [Migrations and storage](https://featherpanel.com/docs/plugins/database/)

## Frequently asked questions

### Can my plugin define its own permission node?

Yes. The permission argument of registerAdminRoute accepts any string, and a role can hold any node between 2 and 255 characters. The role editor only shows built-in nodes, so add a custom node to a role through PUT /api/admin/permissions. Roles with admin.root pass every check.

### Do I have to clear a cache after adding a route?

No. The route cache is keyed on the size and modification time of every route file, so a changed file rebuilds it. Set FEATHERPANEL_ROUTE_CACHE=0 to turn the cache off while developing.
