🧱 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/ — see the warning below |
throttle | Rate limiting |
api is an example, not a guard
core/App/Http/Middleware/API.php ships with its token list hardcoded in the file (secret123, myapitoken). It is a template for your own check, and it lives in the site layer precisely so you can rewrite it. For a real token gate use ApiAuth and the tokens issued in the manager — see REST API.
Three 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 |
ProOnly | The paid edition only; the free one answers 402 Payment Required |
ProOnly is for you if you are writing a satellite
Inside the component itself it sits on every paid route. The free package does not carry the class at all — along with the paid routes it guarded — so hitting such an address there gives an ordinary 404 rather than a 402. See Free and PRO.
⚙️ 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_public_key and pageblocks_recaptcha_secret_key empty (pageblocks_recaptcha_service arrives filled in as Google reCAPTCHA, and it switches nothing on — it only names the service). 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.
Filling the keys in is not the whole job — the form needs a field
Once the secret key is set, the check demands a g-recaptcha-response value and rejects the request when it is absent. Nothing adds that field for you: put {recaptcha()} in the form, and give the form the pb-form attribute so pbForm can call grecaptcha.execute() and fill it in before sending.
So a form that submits the old-fashioned way, without pbForm, has no way to obtain a v3 token and will fail every POST. Fill the keys in and then walk your forms — this is the mistake that reads as "the site stopped accepting the contact form".
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.