Docs›Roteamento›Middleware stack
Roteamento

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:

#MiddlewareFaz o quê
1EncryptCookiesCriptografa/decripta cookies (nativo Laravel).
2AddQueuedCookiesToResponseAplica cookies enfileirados na resposta (nativo).
3StartSessionAbre a sessão (driver database por padrão — ver SESSION_DRIVER).
4ShareErrorsFromSessionInjeta $errors nas views a partir da sessão (nativo).
5ValidateCsrfTokenCSRF nativo do Laravel — ver CSRF protection.
6SubstituteBindingsResolve route-model binding (nativo).
7SetTenantConnectionRepointa o data-plane a partir de session('tenant_database'). No-op em single-DB.
8SetUserLocaleAplica o locale do usuário (session('user_language')) ao Laravel + ao MadLang interno.
9LogRequestGrava 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:

AliasClasseO que verificaResposta 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.api
mad.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)

ClasseOnde rodaFaz o quê
SetTenantConnectionAppend do grupo webRepointa conexão de dados pelo tenant da sessão. terminate() reseta após a resposta (defesa contra worker long-lived).
SetUserLocaleAppend do grupo webLocale do usuário (session('user_language')) → app()->setLocale() + MadLang.
LogRequestAppend do grupo webGrava log de requisição web no terminate(), gated por config.
InstallGuardGrupo isolado do instaladorBloqueia acesso ao instalador se a app já estiver instalada.
ForceFileSessionGrupo isolado do instaladorForça driver de sessão em arquivo (tabela sessions ainda não existe pré-migração).
McpManifestAuthMiddlewareembed/v1/*, agent-console/v1/*Resolve Authorization: Bearer (token MCP) → sessão/usuário do request.
AgentConsoleAdminMiddlewareagent-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,
]);
Ordem importa. Um middleware que depende da sessão (lê/grava 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