Docs›Começando›Instalador web (/install)
Começando

Instalador web (/install)

Deep-dive: token timing-safe, picker de driver de banco (+ modo avançado), fail-closed lock, mad:install-reset, Mad\Install\**.

Deep-dive de referência do instalador web (/install) — o wizard de 3 passos que prepara um clone fresco do template: configura a conexão de banco, roda migrations + seed baseline, cria o administrador e fecha a própria rota (fail-closed) pra nunca mais rodar. Para o passo a passo resumido de "subir o projeto", veja Instalação.

Quando usar o wizard

Caminho Quando usar
Wizard web (/install) Deploy novo (staging/produção/demo) — configura banco, roda migrations/seed e cria o admin pelo navegador, sem SSH.
Setup manual via CLI (composer setup / php artisan migrate) Desenvolvimento local — você já sabe as credenciais do banco e prefere rodar tudo num comando.

A rota roda fora do grupo web: o hook then: em bootstrap/app.php registra routes/install.php com sua própria pilha de middleware (InstallGuard → ForceFileSession → cookies/sessão/CSRF), porque o SESSION_DRIVER=database padrão depende de uma tabela sessions que ainda não existe antes da primeira migration:

// bootstrap/app.php
then: function (): void {
    Route::middleware([
        \App\Http\Middleware\InstallGuard::class,
        \App\Http\Middleware\ForceFileSession::class,
        \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'));
},

App\Http\Middleware\ForceFileSession força session.driver para file (sessão em disco em storage/framework/sessions/) antes do StartSession rodar, então o wizard ganha sessão + CSRF sem tocar no banco.

Acessar num clone fresco

Pré-requisitos mínimos — o passo 1 do wizard checa isso (informativo, não bloqueia): PHP ≥ 8.2, extensões pdo/mbstring/openssl/json/tokenizer/ ctype/fileinfo, pelo menos um driver PDO (pdo_sqlite/pdo_mysql/ pdo_pgsql) e storage/, storage/framework/, app/database/ e .env graváveis.

git clone <repo> && cd mad-framework-laravel
composer install
cp .env.example .env
php artisan key:generate
php artisan serve            # http://127.0.0.1:8000

Abra /install. Não precisa rodar php artisan migrate na mão — é o que o wizard faz. Se você já migrou/seedou por fora, o instalador detecta e se fecha sozinho (ver Fail-closed).

Token de instalação

/install é protegido por um token timing-safe que prova posse do servidor antes de liberar qualquer mutação. Implementado em Mad\Install\InstallToken (packages/mad-framework/src/mad/install/InstallToken.php):

  1. INSTALL_TOKEN no .env — se definido, é o token fixo (ensure() devolve direto, nunca regenera).
  2. Senão, ensure() gera bin2hex(random_bytes(16)) no primeiro acesso e grava em storage/app/install-token.txt com permissão 0600.
cat storage/app/install-token.txt

O campo do gate é mascarado — o valor não aparece na tela, você lê do servidor e cola. A comparação é constant-time (hash_equals), token errado dispara sleep(1) antes do erro (throttle anti brute-force). Ao concluir a instalação, Installer::seal() chama InstallToken::forget() e o arquivo do token é descartado.

O gate de token é independente da numeração de passos do wizard: InstallForm::onVerifyToken() seta $authenticated = true uma única vez; toda ação subsequente (onStep, onTestConnection, onInstall, ...) re-valida o guard fail-closed via assertInstallable() antes de prosseguir.

Fluxo (3 passos)

Componente: App\Control\Install\InstallForm (app/control/Install/InstallForm.php), um MadComponent com $wrapper = self::INTERNAL, montado por App\Http\Controllers\InstallController::show() num casco próprio (resources/views/install/shell.blade.php) e servido por um endpoint wire dedicado (POST /install/_wire, fora de mad.auth).

  1. Requisitos — InstallForm::requirements() reporta versão de PHP, extensões, drivers PDO disponíveis e permissões de escrita. Informativo — itens em vermelho podem quebrar a instalação, mas não bloqueiam o avanço.

  2. Banco de dados — escolha o driver (public string $driver = 'sqlite'):

    • SQLite (padrão) — caminho do arquivo (Installer::ensureSqliteFiles() cria o arquivo se não existir).
    • MySQL / MariaDB ou PostgreSQL — host, porta, banco, usuário, senha. O banco já deve existir — o instalador não roda CREATE DATABASE/CREATE USER.

    Testar conexão (onTestConnection → Installer::testConnection()) valida antes de avançar — SQLite checa só o diretório gravável; servidor abre um PDO descartável contra cada alvo distinto.

    Modo avançado (toggle onToggleAdvanced, só disponível em driver ≠ sqlite) permite um banco por conexão lógica no mesmo servidor — grava DB_<CONN>_DATABASE individual no .env para cada uma das 6 conexões (campo vazio = herda o alvo base). Modo simples (default): as 6 conexões + a conexão default do Laravel (sessions/cache/jobs) herdam um único alvo.

  3. Aplicação e administrador — nome/URL do app + nome/e-mail/senha do admin (mínimo 6 caracteres, confirmação obrigatória). Instalar (onInstall → Installer::run()) então:

    • grava o .env (EnvWriter::upsert, preserva comentários e ordem);
    • aplica as conexões em memória no mesmo request (applyRuntimeConnections) e purga o PDO cacheado;
    • roda migrate --force (cobre as 6 conexões numa passada) e db:seed --class=Database\Seeders\DatabaseSeeder (que dispara o MadReferenceSeeder — identidade/referência baseline — e, em apps materializados pelo MadBuilder, os seeders de permissão/papel gerados);
    • atualiza o usuário admin (bcrypt, cost 12) com nome/e-mail/senha informados;
    • persiste um resumo durável (ver abaixo) e sela a instalação.

    set_time_limit(0) + ignore_user_abort(true) blindam essa etapa contra timeout — migrate+seed num banco de servidor "frio" pode passar do max_execution_time default; o trabalho segue mesmo se o cliente desconectar.

Tela de sucesso mostra o resumo (app, banco, e-mail do admin, log de passos) e o link pro login.

Landing durável — GET /install/done

Como o passo de instalação roda inline e pode estourar timeout do client antes do response chegar, Installer::run() persiste um resumo em JSON (Mad\Install\InstallSummary, padrão storage/mad-install-done.json, escrita atômica temp+rename, nunca grava senha) antes de selar. GET /install/done é a única rota sempre liberada pelo InstallGuard (mesmo pós-install): se o resumo existe, InstallController::done() renderiza a tela de sucesso a partir dele; senão redireciona pro login. Isso torna a confirmação idempotente — um refresh ou um retorno à URL depois de instalado ainda mostra a confirmação em vez de cair direto no login sem feedback.

Fail-closed (pós-instalação)

Depois de instalado, qualquer acesso ao instalador é bloqueado pelo middleware App\Http\Middleware\InstallGuard:

  • GET /install → 302 redirect pro login (ou pra /install/done se ainda existir um resumo durável).
  • POST /install/_wire → 403 {"error":"Application already installed."}.

Detecção (InstallGuard::isInstalled()), nesta ordem:

instalado = lockExists()                                   // selo durável, ZERO query
         || (mad_iam_user migrada && usuário admin existe)  // heurística, auto-sela
  • O lock (storage/mad-installed.lock, override por config('mad.install.lock_path')) é gravado por InstallGuard::seal() ao concluir — sinal monotônico, independente do banco. Lock presente ⇒ bloqueia sem nenhuma query.
  • Sem lock, a heurística (mad_iam_user migrada + login admin existe) detecta uma instalação feita por fora (migrate manual) e auto-sela na hora (grava o lock).
  • Banco inalcançável e sem lock ⇒ tratado como fresh (abre o wizard); a mutação continua protegida pelo token.

O guard é a fonte única de verdade: o middleware (defesa-em-profundidade no GET) e InstallForm::assertInstallable() (autoritativo, re-checado a cada ação do wizard — não só no GET inicial) chamam o mesmo InstallGuard::isInstalled().

Reabrir para QA — mad:install-reset

Pra testar o wizard de novo sem decorar caminhos, use o comando — só roda em APP_ENV=local:

php artisan mad:install-reset
# apaga storage/mad-installed.lock, o(s) SQLite alvo das conexões MAD,
# o resumo durável (mad-install-done.json) e o token,
# e imprime um token NOVO.

php artisan mad:install-reset --keep-db
# preserva o(s) banco(s) SQLite; zera só lock + resumo + token
# (reabre o gate sem perder dados).

Fora de local o comando falha sem apagar nada. Bancos MySQL/PostgreSQL não são apagados pelo comando (só arquivos SQLite locais detectados nas 6 conexões MAD + a conexão default) — limpe o servidor à mão se precisar.

// app/Console/Commands/InstallReset.php
protected $signature = 'mad:install-reset {--keep-db : Preserva o banco; apaga só o lock + token}';

Referências de código

Peça Arquivo
Componente do wizard (3 passos + gate) app/control/Install/InstallForm.php
Orquestração (env/migrate/seed/admin/selo) app/Service/Install/Installer.php
Entry point HTTP (show/done/wire) app/Http/Controllers/InstallController.php
Guard fail-closed (lock + heurística) app/Http/Middleware/InstallGuard.php
Sessão file pré-banco app/Http/Middleware/ForceFileSession.php
Reset de QA app/Console/Commands/InstallReset.php
Token timing-safe packages/mad-framework/src/mad/install/InstallToken.php
Upsert atômico do .env packages/mad-framework/src/mad/install/EnvWriter.php
Resumo durável pós-install packages/mad-framework/src/mad/install/InstallSummary.php
Rotas (fora do web) routes/install.php + hook then: em bootstrap/app.php
Casco + UI resources/views/install/* (shell.blade.php, install-form.blade.php, partials/success-page.blade.php)

Gotchas

  • Não confunda com o setup manual. composer setup / php artisan migrate não passam pelo token, pelo lock ou pelo resumo durável — são o caminho de desenvolvimento local, sem o wizard.
  • modelHasColumn-style fail-open não existe aqui — ao contrário de outros guards do framework, InstallGuard é estritamente fail-closed: qualquer ambiguidade (banco inalcançável, lock ausente) tende a reabrir o wizard, nunca a expor uma instalação já feita sem o token.
  • Modo avançado só existe no servidor. O toggle de "um banco por conexão" ($advanced) é forçado a false sempre que driver === 'sqlite' — SQLite é sempre um arquivo único compartilhado pelas 6 conexões.
  • INSTALL_TOKEN fixo no .env nunca é regenerado por mad:install-reset — o comando avisa e mantém o valor fixo.