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.
On this page
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
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:
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 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.
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 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:
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:
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.
Rate limits
Pass a default with Rate:
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 for how limits are counted.
Related
- Frontend for calling your routes from plugin pages
- Migrations and storage
Frequently asked questions
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.
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.