🧱 Middleware
A layer that runs before the controller: access checks, authentication, CSRF protection, rate limiting.
📚 What ships with it
| Alias | What it does |
|---|---|
auth | Allows an authenticated site user through; otherwise 401 |
guest | Anonymous only; an authenticated visitor is sent to / |
modauth | Checks the manager token in the modauth header; otherwise 403 |
csrf | Verifies the CSRF token on POST, PUT, PATCH, DELETE |
g-recaptcha-v3 | Verifies reCAPTCHA v3 on modifying requests |
api | Requires a bearer token and a path starting with /api/ |
throttle | Rate limiting |
Two more come from the framework and have no alias — write them by class name:
| Name | What it does |
|---|---|
ApiAuth | A REST API token; takes a scope: ApiAuth:cargo.read |
SudoOnly | A manager user with sudo, or a member of the administrator group |
⚙️ How a name becomes a class
An alias expands to a class name; the namespace is added at run time — PageBlocks\App\Http\Middleware\ first, then Boshnik\PageBlocks\Http\Middleware\. That is why the same bare SudoOnly finds the framework class while Authenticate finds yours.
Do not pass a fully qualified class name
// ✅ works
Route::get('profile', 'UserController@profile')->middleware('auth');
Route::get('profile', 'UserController@profile')->middleware('Authenticate');
// ❌ 403: the prefix is added to the FQCN as well
Route::get('profile', 'UserController@profile')->middleware(Authenticate::class);Pass an FQCN and you get PageBlocks\App\Http\Middleware\PageBlocks\App\Http\Middleware\Authenticate, which does not exist, so the request is denied.
A middleware that is not found denies the request
A typo in the name is an ERROR line in the MODX log and a 403, not a pass. Fail-closed is deliberate here: a guard that is absent must not look like a guard that let you through.
This is the exact opposite of throttle:name, where an undefined limiter allows the request. The difference is the cost of the mistake: a skipped access check exposes data, a skipped rate limit does not.
🧩 Aliases
They live in core/App/bootstrap/app.php — the site layer, untouched by a component upgrade:
<?php
return [
'middleware' => [
'modauth' => MODAuthenticate::class,
'csrf' => VerifyCsrfToken::class,
'g-recaptcha-v3' => VerifyGoogleRecaptchaV3::class,
'auth' => Authenticate::class,
'guest' => Guest::class,
'api' => API::class,
]
];This file must not contain use
There is deliberately no namespace declaration, so Authenticate::class yields the string 'Authenticate' — exactly the name the runtime will prefix.
Add use PageBlocks\App\Http\Middleware\Authenticate; and ::class starts yielding the full name. After prefixing, the class is not found, and every route using that alias starts answering 403.
🛠 Writing your own
A class in core/App/Http/Middleware/, extending Middleware:
<?php
namespace PageBlocks\App\Http\Middleware;
use Boshnik\PageBlocks\Http\Request;
class ExampleMiddleware extends Middleware
{
public function handle(Request $request)
{
return true;
}
}What handle() may return:
| Return | Result |
|---|---|
true | The route continues |
false | 403 |
abort($code) | Stops with that code |
response(...) | Your response is sent — redirect('/'), say |
Middleware with a parameter
A parameter goes after a colon and reaches setParams():
Route::get('api/v1/orders', 'OrderApi@index')->middleware('ApiAuth:orders.read');The split happens on the first colon. Fully qualified class names contain backslashes but never colons, so the parsing is unambiguous.
🔗 Attaching
To one route
Route::get('profile', 'UserController@profile')->middleware('auth');To a group
Route::prefix('account')
->middleware(['auth', 'csrf'])
->group(function () {
Route::get('/', 'AccountController@index');
Route::post('update', 'AccountController@update');
});To a whole CRUD set
Route::crud('mgr/pb/constructor/blocks', BlockController::class)
->middleware(['modauth', 'SudoOnly']);This attaches to all eleven routes in the batch — see Route::crud().
🚫 Excluding
Route::post('webhook', 'WebhookController@handle')->withoutMiddleware('csrf');Route::middleware(['auth', 'csrf'])
->withoutMiddleware('csrf')
->group(function () {
Route::post('settings', 'SettingsController@update');
});📌 What is attached for you
Every POST, PUT, PATCH and DELETE outside the mgr context automatically gets two:
csrf— unless excluded withwithoutMiddleware;g-recaptcha-v3— always.
It turns on by filling in keys, not by a checkbox
The installer registers pageblocks_recaptcha_service, pageblocks_recaptcha_public_key and pageblocks_recaptcha_secret_key empty. An empty secret key is what "off" means, so right after installing reCAPTCHA gets in nobody's way. Fill the keys in and it starts working.
You do not list them yourself. With pageblocks_recaptcha_secret_key empty, the reCAPTCHA check passes everything through, so on a site without keys it changes nothing.
The mgr context is excluded because the manager is guarded by its own token (modauth), and its requests do not carry the site's CSRF token.