View .md

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.

HelperMiddlewareExtra argumentUse for
registerApiRoutenonePublic endpoints, webhooks
registerAuthRoutelogged-in user (session or API key)Anything for a signed-in user
registerAdminRoutelogged-in user + permission check$permissionAdmin endpoints
registerServerRoutelogged-in user + server access$serverShortUuid, the name of the path parameter/api/user/servers/{uuidShort}/... endpoints
registerVmInstanceRoutelogged-in user + VM instance accessthe path parameter nameVDS endpoints
registerWingsRoute, registerQuilldRoutedaemon tokenEndpoints 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.

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.