Middleware stack
StartSession, ValidateCsrfToken, mad.auth, mad.permission.
Middleware é a camada que envolve cada request antes do controller e o response antes
de sair. Este app não tem app/Http/Kernel.php (estilo Laravel ≤10) — a
configuração é centralizada em bootstrap/app.php (estilo Laravel 11+), com
aliases extras registrados pelo MadServiceProvider e grupos adicionais
declarados por módulo em routes/modules/*.php.
Grupo web — o que toda rota de routes/web.php já ganha
O Laravel embrulha automaticamente todo o conteúdo de routes/web.php no
grupo web. Este app o configura (não substitui) em
bootstrap/app.php:
| # | Middleware | Faz o quê |
|---|---|---|
| 1 | EncryptCookies | Criptografa/decripta cookies (nativo Laravel). |
| 2 | AddQueuedCookiesToResponse | Aplica cookies enfileirados na resposta (nativo). |
| 3 | StartSession | Abre a sessão (driver database por padrão — ver SESSION_DRIVER). |
| 4 | ShareErrorsFromSession | Injeta $errors nas views a partir da sessão (nativo). |
| 5 | ValidateCsrfToken | CSRF nativo do Laravel — ver CSRF protection. |
| 6 | SubstituteBindings | Resolve route-model binding (nativo). |
| 7 | SetTenantConnection | Repointa o data-plane a partir de session('tenant_database'). No-op em single-DB. |
| 8 | SetUserLocale | Aplica o locale do usuário (session('user_language')) ao Laravel + ao MadLang interno. |
| 9 | LogRequest | Grava mad_log_request no terminate() (gated por general.request_log). |
// bootstrap/app.php
$middleware->validateCsrfTokens(except: [
'embed/v1/*',
'agent-console/v1/*',
]);
$middleware->web(append: [
SetTenantConnection::class, // tenant primeiro: antes de qualquer query de negócio
SetUserLocale::class,
LogRequest::class,
]);
Os três últimos (9–7 na tabela) são appends deste app, não vêm do Laravel —
rodam sempre depois de StartSession, porque dependem da
sessão já estar legível.
Aliases do framework — mad.auth / mad.permission / mad.api
O MadServiceProvider registra três aliases de middleware no boot, usáveis
em qualquer rota como string:
| Alias | Classe | O que verifica | Resposta na negação |
|---|---|---|---|
mad.auth |
Mad\Http\Middleware\MadAuthenticate |
Usuário logado (PermissionGate::isLogged()) OU classe alvo está em public_classes[]/é LoginForm/é o public_entry configurado. |
Wire/AJAX → JSON 401 com redirect; navegação normal → script de redirect 401 para a URL de login. |
mad.permission |
Mad\Http\Middleware\MadProgramPermission |
PermissionGate::canAccess($class, $method) — permissão de PROGRAMA/AÇÃO do usuário logado. |
HTML 403 ("Permission denied"). |
mad.apimad.api:{ability} |
App\Http\Middleware\MadApiTenantMiddleware |
Camada de API pública stateless: Authorization: Bearer mad_api_… → token → unit → tenant → banco, num passo só. Revalida por request se usuário/unidade/empresa continuam existindo e ativos, e checa as abilities declaradas na rota. |
JSON 401 (token ausente/inválido/expirado/revogado), 403 (escopo revogado, unidade fora do usuário, ability faltando), 429 (brute-force). |
// packages/mad-framework/src/mad/MadServiceProvider.php
$router->aliasMiddleware('mad.auth', \Mad\Http\Middleware\MadAuthenticate::class);
$router->aliasMiddleware('mad.permission', \Mad\Http\Middleware\MadProgramPermission::class);
// API pública stateless — a classe é app-level (usa os models IAM do app).
$router->aliasMiddleware('mad.api', \App\Http\Middleware\MadApiTenantMiddleware::class);
mad.api não vive no grupo web. Ele roda em
routes/api.php (registrado por withRouting(api: …) em
bootstrap/app.php), cujo grupo api tem só
SubstituteBindings: sem StartSession,
sem ValidateCsrfToken. O escopo não vem da sessão — vem do
token: o middleware seta TenantContext/UnitContext (filtro e
stamp automáticos nos models com BelongsToTenant/BelongsToUnit)
e, em multi-database, repointa o data-plane. Como SetTenantConnection, faz
flush do estado em dois pontos (início do handle() e no
terminate()) contra worker long-lived/Octane. Contrato completo em
REST API — Autenticação.
// routes/api.php — prefixo /api aplicado pelo withRouting(api:)
Route::middleware('mad.api')->apiResource('orders', OrderApiController::class);
// Abilities = authz fino POR ROTA. Vírgula exige TODAS.
// Token com abilities NULL = acesso total; token restrito sem a ability → 403.
Route::middleware('mad.api:orders.read')->get('/orders', ...);
Route::middleware('mad.api:orders.write')->post('/orders', ...);
Padrão de uso em todo módulo de conteúdo (routes/modules/*.php):
// routes/modules/documents.php
Route::middleware(['mad.auth', 'mad.permission'])->group(function () {
MadRoutes::resource('documents', 'DocumentList', 'DocumentForm'); // /app/documentos
MadRoutes::expose('folders', 'FolderForm');
MadRoutes::screen('ged_config', 'ConfigForm');
});
Endpoints JSON internos (não são telas, não passam pelo gate de programa) costumam usar
só mad.auth:
// routes/modules/admin.php — API REST interna (Mad\Rest\ApiResourceController)
Route::middleware('mad.auth')
->prefix('api')
->group(function () {
Route::apiResource('import-templates', \App\Http\Controllers\Sys\ImportTemplateApiController::class);
});
Pipeline isolado do instalador
As rotas de instalação (routes/install.php) NÃO ficam dentro do grupo
web de propósito: o StartSession padrão usa
SESSION_DRIVER=database, e antes do instalador rodar a tabela
sessions ainda não existe (clone fresco) — usar o grupo normal
fatalizaria antes mesmo de chegar no formulário. O instalador recebe sua
própria stack, registrada no hook then: de bootstrap/app.php:
then: function (): void {
Route::middleware([
\App\Http\Middleware\InstallGuard::class, // fail-closed: só roda se ainda não instalado
\App\Http\Middleware\ForceFileSession::class, // sessão em disco — não depende de tabela
\Illuminate\Cookie\Middleware\EncryptCookies::class,
\Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class,
\Illuminate\Session\Middleware\StartSession::class,
\Illuminate\Foundation\Http\Middleware\ValidateCsrfToken::class,
])->group(base_path('routes/install.php'));
}
Outros middlewares do app (App\Http\Middleware)
| Classe | Onde roda | Faz o quê |
|---|---|---|
SetTenantConnection | Append do grupo web | Repointa conexão de dados pelo tenant da sessão. terminate() reseta após a resposta (defesa contra worker long-lived). |
SetUserLocale | Append do grupo web | Locale do usuário (session('user_language')) → app()->setLocale() + MadLang. |
LogRequest | Append do grupo web | Grava log de requisição web no terminate(), gated por config. |
InstallGuard | Grupo isolado do instalador | Bloqueia acesso ao instalador se a app já estiver instalada. |
ForceFileSession | Grupo isolado do instalador | Força driver de sessão em arquivo (tabela sessions ainda não existe pré-migração). |
McpManifestAuthMiddleware | embed/v1/*, agent-console/v1/* | Resolve Authorization: Bearer (token MCP) → sessão/usuário do request. |
AgentConsoleAdminMiddleware | agent-console/v1/* (depois do McpManifestAuth) | Exige token da família Command Center + session('login') === 'admin'. Fail-closed: qualquer dúvida → 403. |
embed/v1/* e agent-console/v1/* são as únicas rotas excetuadas
do CSRF ($middleware->validateCsrfTokens(except: [...])) — são iframes
cross-origin e stateless; a autenticação ali é inteiramente o Bearer token, não cookie de
sessão. Ver CSRF protection para o porquê disso ser
seguro.
Criando um middleware novo
php artisan make:middleware RateLimitDownloadMiddleware
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
class RateLimitDownloadMiddleware
{
public function handle(Request $request, Closure $next)
{
// ANTES do controller
if (/* limite excedido */ false) {
return response('Too Many Requests', 429);
}
$response = $next($request);
// DEPOIS do controller, antes de sair
$response->headers->set('X-RateLimit-Remaining', '...');
return $response;
}
}
Depois, registre como alias (se quiser usar por nome curto em rotas) ou aplique direto pela classe:
// Opção A — alias, num service provider
$this->app['router']->aliasMiddleware('rate.download', \App\Http\Middleware\RateLimitDownloadMiddleware::class);
// Opção B — direto na rota, sem alias
Route::middleware([\App\Http\Middleware\RateLimitDownloadMiddleware::class])
->get('/app/_mad-download', \Mad\Http\Controllers\MadDownloadController::class);
// Opção C — append global no grupo web (bootstrap/app.php)
$middleware->web(append: [
\App\Http\Middleware\RateLimitDownloadMiddleware::class,
]);
session(...)) precisa rodar depois do StartSession — por
isso SetTenantConnection/SetUserLocale/LogRequest
são appends do grupo web, nunca prepends. O mesmo vale
para qualquer middleware custom que leia session('logged') ou afins.
Próximos passos
- Rotas públicas — como as rotas são declaradas e organizadas em módulos.
- CSRF protection — detalhe completo do
ValidateCsrfToken+MadCsrf. - Auth no portal público — como
mad.auth/mad.permissiondecidem quem é "logado" e "autorizado". - REST API — Autenticação — contrato completo do
mad.api: emissão de token, abilities e escopo de tenant.