Security of the Package Routes
Bulk actions, reordering and the CSV export post to (or download from) routes the package registers itself:
| Route | Name | Used by |
|---|---|---|
POST /table/bulk-action |
table.bulk-action |
Bulk Actions |
POST /table/reorder |
table.reorder |
Reordering |
GET /table/export |
table.export |
Export |
The component never builds these urls. The server signs them when it renders a table, so a url always names the Table class, its parameters and (for bulk actions) the action it was made for.
Recommended Setup
Publish the config and run the routes inside your web stack with a logged-in user:
// config/table-builder.php
'routes' => [
'middleware' => ['web', 'auth'],
'signature_ttl' => 720,
],
Then authorize every table that exposes one of these routes:
public function authorize(Request $request): bool
{
return $request->user()->can('viewAny', Room::class);
}
public function authorizeBulkAction(Request $request, int $key): bool
{
return $request->user()->can('delete', Room::class);
}
public function authorizeReorder(Request $request): bool
{
return $request->user()->can('update', $this->venue);
}
Without web the routes have no session, so $request->user() is null inside the hooks and
there is no CSRF check. The defaults keep the routes outside any group so existing applications keep
working after an upgrade; switch to the setup above when you can.
What the Signature Covers
Each url is made with URL::temporarySignedRoute() and checked by Laravel's ValidateSignature
(the export route uses its own check, see below). A missing, changed or expired signature answers
403 before any table code runs.
table- the Table class. Only subclasses ofAbstractTableare accepted; anything else is a404.params- the table's signed parameters.action- the bulk action key. A key the table does not have is a404.expires- the expiry, see below.
A bulk action url also signs the query string of the page it was rendered on. For "select all
results" the component posts the sort, search and filters it is showing as state in the body, which
takes the place of the table's own sort and filter keys from that query. The export url signs only table, params and
expires: the component appends the current sort, search and filters when the button is pressed, so
those may change after rendering. They select rows within the same table and are checked by the same
authorization as the page itself.
What the signature does not cover is who is calling: it proves the url was rendered by this application, not that the current user may use it. That is what the middleware and the authorization hooks are for.
Expiry
routes.signature_ttl is the lifetime of a signed url in minutes and defaults to 720 (12 hours).
Every Inertia visit renders fresh urls, so only a tab left open longer than that sees a 403.
Set it to null or 0 to sign urls without an expiry, as versions before 1.6 did.
Authorization Hooks
| Method | Called by | Default |
|---|---|---|
authorize(Request $request): bool |
the hooks below | true |
authorizeBulkAction(Request $request, int $key) |
the bulk action route | authorize() |
authorizeReorder(Request $request) |
the reorder route | authorize() |
authorizeExport(Request $request) |
the export route | authorize() |
Returning false answers 403 (an AuthorizationException). Rendering a table never calls these
hooks: guard the page itself in its controller. Gate the button too, so nobody sees an action they
cannot use: only register a bulk action, reorderable() or exportable() when the user may use it,
for example with $table->when($user->can('export', Booking::class), fn ($table) => $table->exportable()).
The routes answer 404 for a table that does not offer the feature.
Tables with Constructor Arguments
The routes create the Table class again from the url. By default that is app(TableClass::class),
which only works for tables without constructor arguments, or with arguments the container can
resolve. A table bound to a record tells the package what to sign and how to rebuild itself:
class RoomTable extends AbstractTable
{
public function __construct(private readonly Venue $venue) {}
public function for(): Builder
{
return $this->venue->rooms()->getQuery();
}
public function signedParameters(): array
{
return [
'venue' => $this->venue->id,
];
}
public static function fromSignedParameters(array $parameters, Request $request): static
{
return new static(Venue::query()->findOrFail($parameters['venue']));
}
}
signedParameters() returns scalars only. They travel base64-encoded in the signed params
parameter, so the venue cannot be swapped for another one. The rebuilt table runs the same for()
query, which scopes bulk actions, reordering and exports to that venue's rooms.
Prefer a route of your own instead? Pass url: to reorderable() or exportable() and handle the
request in your controller, see Reordering and
Export.
