Search by

twstec / kit-admin

kelvinTws

Painel de super admin do TWS Laravel Starter Kit como plugin do Filament 5: usuários, chaves de API, projetos, uploads, logs de requisição, trilha de auditoria e configurações, login com verificação em duas etapas por e-mail, variantes de dashboard, trilha de auditoria de toda ação do painel (falha

v2.0.0-beta.15 2026-10-02 20:49 UTC

README

Parte do TWS Laravel Starter Kit. O código, as issues e os pull requests ficam no monorepo kelvindk9w/tws-laravel-starter-kit (pasta packages/admin); este repositório é o espelho só-leitura publicado a cada versão. Documentação: docs/ · Segurança: SECURITY.md · Licença: MIT (LICENSE).

O super admin do TWS Laravel Starter Kit (/admin) como plugin do Filament 5: usuários, chaves de API, projetos, uploads, logs de requisição, trilha de auditoria e configurações, login com verificação em duas etapas por e-mail, variantes de dashboard, a trilha de auditoria de toda ação do painel (que falha fechada) e as proteções de acesso — ligadas pelo pacote, não pelo aplicativo.

É a camada de cima do kit e o único pacote com telas: depende do twstec/kit-auth, do twstec/kit-foundation, do Laravel e do Filament (com o Livewire, que o Filament usa). O twstec/kit-accounts e o twstec/kit-uploads são sugeridos (suggest), não exigidos: o painel se adapta ao que está instalado (abaixo). Não conhece o aplicativo — o model de usuário é o configurado em auth.providers.users.model — e um teste de arquitetura na suíte do pacote garante isso.

  • Requisitos: PHP 8.4+, Laravel 13, Filament 5, Livewire 4, twstec/kit-foundation e twstec/kit-auth 2.x (e, se quiser as telas deles, twstec/kit-accounts e twstec/kit-uploads 2.x).
  • Licença: MIT.

O que o pacote traz

Peça O que faz
AdminPlugin O plugin que o aplicativo registra no painel: resources, páginas, login, segundo fator, dashboards, navegação, menu do usuário, avatar de iniciais e as proteções
Resources\… Usuários (CRUD com guardas), Contas (só leitura: membros e papéis, projetos e chaves da conta), Chaves de API, Projetos, Uploads, Logs de requisição e Auditoria
Pages\… Perfil do admin (foto, nome, segundo fator), Configurações editáveis e o Login (Pages\Auth\Login)
Auth\EmailCodeAuthentication Provedor de MFA do Filament com o motor do twstec/kit-auth (código por e-mail, mesmos limites do painel do cliente); obrigatório quando AUTH_TWO_FACTOR_REQUIRED alcança os administradores
Dashboards\…, Widgets\… As variantes "Visão geral" e "Crescimento & API", o DashboardRegistry (lê config/dashboards.php) e a base de widgets (Metric, Period, BaseStatsWidget…)
Support\AdminAudit A trilha de auditoria das ações: toda chamada Livewire de tela do painel roda com um escopo aberto; o AuditTrail do foundation grava cada escrita de model
Support\AdminPanelHardening As garantias de segurança do painel, aplicadas pelo pacote qualquer que seja a ordem do PanelProvider
Access\AdminAccess, Concerns\AccessesAdminPanel, Http\Middleware\EnsureAdminPanelAccess Quem entra: is_admin + conta ativa — o critério, a trait do canAccessPanel e a conferência do pacote
Resources\Users\Support\UserAdminGuard, MarkEmailVerifiedAction Guardas de servidor (conta protegida, a própria conta, o último admin ativo) e a ação de suporte
Support\AvatarUpload O campo de foto: grava pela função global de upload (foto pessoal) e só vincula upload da própria pessoa
Authorization\AdminPermissions, AdminAuthorization, AdminRoles Papéis e permissões do painel: o critério (admin.authorization.roles), a checagem de servidor em toda chamada Livewire (403 + denied na trilha) e a atribuição de papel (ação sensível, sem escalada)
Approvals\…, Resources\ApprovalRequests\… Aprovação em dois passos: ApprovableAction, Approvals::register()/gate(), o ApprovalService (quatro olhos ou um operador, uma execução só, sob trava) e a tela "Aprovações"
Console\MakeAdminUser php artisan user:make-admin email [--role=papel] [--remove] — o resgate de acesso (papel de dono por padrão), auditado no contexto console

Módulos opcionais: o painel se adapta

O plugin pergunta ao ponto único de detecção do kit (Twstec\Kit\Foundation\Kit::has()) o que está instalado e só registra o que existe (AdminPlugin::RESOURCES diz qual tela é de qual módulo):

Sem… O painel
twstec/kit-accounts Sem as telas de contas, chaves de API e projetos (nem menu, nem rota); sem o filtro por conta na trilha de auditoria; os dashboards sem os cards de chaves/projetos e sem a tabela de projetos recentes; sem o modo sistema das contas (não há contas); a guarda de exclusão de usuário não consulta contas
twstec/kit-uploads Sem a tela de uploads e o widget dos últimos uploads; sem o campo de foto no cadastro de usuário e no perfil do admin (o avatar são as iniciais)

Uma trava de arquitetura da suíte (DependenciesTest) confere que todo arquivo do pacote que nomeia uma classe de accounts ou uploads é uma tela registrada só com o módulo ou pergunta Kit::has() antes. A suíte finge a ausência (Kit::pretendAbsent) e sobe o painel de novo (OptionalModulesTest); a prova com os pacotes ausentes de verdade é a suíte do starter em cada combinação, no CI.

Instalação

Pelo Packagist:

composer require "twstec/kit-admin:^2.0@beta"   # durante o beta; na 2.0.0 estável, ^2.0

Durante o beta, cada pacote do kit que você requerer leva o @beta (ou o projeto declara "minimum-stability": "beta" com "prefer-stable": true) — ver docs/instalacao.md.

No monorepo (desenvolvimento do próprio kit), o starter instala o pacote por path repository — como os outros ("url": "../../packages/admin", "twstec/kit-admin": "2.x-dev").

O AdminServiceProvider é descoberto automaticamente. O aplicativo precisa de:

  1. Um painel com o plugin — o registro mínimo:

    use Twstec\Kit\Admin\AdminPlugin;
    
    class AdminPanelProvider extends PanelProvider
    {
        public function panel(Panel $panel): Panel
        {
            return $panel
                ->default()
                ->id('admin')
                ->path('admin')
                ->plugin(AdminPlugin::make());
        }
    }
  2. O model de usuário implementando Filament\Models\Contracts\FilamentUser com a trait Twstec\Kit\Admin\Concerns\AccessesAdminPanel (além das do twstec/kit-auth e do HasAvatar do twstec/kit-uploads), e as colunas users.is_admin (boolean, padrão false) e users.avatar_upload_id numa migration do aplicativo. O pacote não traz migration.

  3. O tema (opcional, para o visual do kit): um tema Vite do Filament que importe as fontes do pacote — ver Tema e CSS.

Depois: php artisan user:make-admin email@exemplo.com para o primeiro admin.

Personalizar

O painel é do aplicativo: marca, cores, fonte, caminho, id e o que mais for do Filament vão no PanelProvider, antes ou depois do plugin (o que vier depois sobrescreve o que o plugin declarou):

$panel
    ->default()->id('admin')->path('painel')
    ->plugin(AdminPlugin::make())
    ->brandName('Minha Empresa')
    ->colors(['primary' => Color::Indigo])
    ->viteTheme('resources/css/filament.css')
    // telas próprias do aplicativo
    ->discoverResources(in: app_path('Filament/Resources'), for: 'App\Filament\Resources')
    // outro plugin, como a demonstração do kit faz
    ->plugin(MeuPlugin::make());

Configuração (config/admin.php e config/dashboards.php, publicáveis com --tag=admin-config; a do aplicativo vence chave a chave):

Chave Padrão O que faz
admin.protections true (ADMIN_PROTECTIONS) Opt-out das proteções do painel (ver abaixo), com aviso no log a cada boot
dashboards.enabled overview,growth (DASHBOARD_ENABLED) Variantes ligadas, na ordem do menu
dashboards.default overview A variante que responde na raiz do painel
dashboards.variants as duas do pacote Catálogo slug → página + ícone + ordem; uma variante nova entra aqui
dashboards.widgets [] Widgets que extensões acrescentam a uma variante (com posição)

Traduções: o grupo admin.* (e as poucas chaves de auth.* e panel.* que as telas usam e que nenhum pacote de baixo traz) vem do pacote; o lang/ do aplicativo vence na mesma chave, em todo idioma e no de reserva. Um bloco novo (admin.faturas, por exemplo) vai no lang/admin.php do aplicativo.

Acrescentar um resource auditado

  1. Escreva o resource no aplicativo (app/Filament/Resources/…), estendendo Twstec\Kit\Admin\Support\BaseResource e a listagem de Twstec\Kit\Admin\Support\BaseListRecords — rota por uuid, rótulos traduzidos, paginação e o alternador tabela/cards vêm da base.
  2. Registre-o no painel (discoverResources ou ->resources([...])).
  3. A auditoria já vale: as telas de <namespace do app>\Filament\ (no starter, App\Filament\) e as do Filament abrem o escopo de auditoria como as do pacote. Toda escrita pelo model (save, update, delete, as Actions de criar/editar/excluir) vira uma linha em audit_events. Tela vinda de outro pacote ou extensão entra declarando o namespace em audit.admin_extension_namespaces (no register da extensão).
  4. Duas regras: escreva pelo model (DB::, query em massa, *Quietly() e withoutEvents() não geram linha) e recuse com AdminAudit::denied($motivo, $record) (registra a tentativa como denied e mostra a notificação numa chamada só). O teste de arquitetura do pacote cobra as duas no código dele; o starter cobra no dele, e a suíte da demonstração (twstec/kit-demo) no dela.

Papéis, permissões e aprovação em dois passos

Entrar no painel é is_admin + conta ativa; o que se faz lá dentro vem do papel (admin_role, migration do pacote — quem já era admin vira owner). Os papéis ficam em config/admin.php (owner, operations, support, auditor de fábrica), com permissões <recurso>.<ação> e curinga. A checagem é no servidor, em toda chamada Livewire de tela do painel: faltou a permissão, 403 e linha denied na trilha. Atribuir papel é ação sensível (senha de transação + código), auditada e sem escalada.

Ação de alto impacto pode exigir aprovação em dois passos: declare uma ApprovableAction, registre com Approvals::register(), passe a Action do Filament por Approvals::gate() e ligue em ADMIN_APPROVALS_ACTIONS. Modo quatro olhos (padrão) ou um operador (ação sensível + espera mínima). O kit traz "excluir usuário" como exemplo (ADMIN_APPROVALS_ACTIONS=users.delete).

Guia completo, com o passo a passo de declarar uma ação num resource novo: docs/admin-e-dashboards.md.

Opt-outs só explícitos, com aviso no log a cada boot: ADMIN_AUTHORIZATION=false (volta o tudo-ou-nada), ADMIN_APPROVALS_MODE=single_operator, ADMIN_APPROVALS_SENSITIVE=false.

O que ele liga sozinho

Em todo painel que registra o plugin, sem nenhuma linha no aplicativo e qualquer que seja a ordem do PanelProvider (Support\AdminPanelHardening, aplicado no registro do plugin e de novo depois que o Filament montou os painéis):

  • Barreira de origem — a allowlist de IP do twstec/kit-foundation (security.admin.allowed_ips, ADMIN_ALLOWED_IPS; em produção sem lista o painel recusa) é o primeiro middleware do painel e é persistente no endpoint de atualização do Livewire: vale para abrir a página e para executar a ação dela. Também entra no grupo filament.actions (o download de exports/imports do Filament, fora do painel).
  • Só administrador com conta ativa — o Authenticate do Filament e o EnsureAdminPanelAccess do pacote, os dois persistentes. O pacote confere mesmo que o model não implemente o contrato do Filament (em ambiente local o Filament deixaria qualquer conta entrar).
  • Pilha sem repetição — um PanelProvider copiado do modelo do Filament, com a pilha de sessão/CSRF inteira, continua funcionando: nenhum middleware entra duas vezes.
  • Transação — Actions e Criar/Salvar em transação: a linha da trilha é gravada na mesma transação da mudança, e sem ela a mudança é desfeita (falha fechada). Recusa das guardas confirma a transação — a tentativa recusada fica registrada.
  • Trilha de auditoria das ações do painel (AdminAudit, ligada no boot do provider) e o segundo fator por e-mail no login.
  • Segundo fator obrigatório — com AUTH_TWO_FACTOR_REQUIRED=admins ou all (regra do twstec/kit-auth), o MFA do Filament fica obrigatório e o EnsureAdminTwoFactorIsConfigured (na pilha persistente, entre o acesso de admin e o modo sistema) leva o administrador sem segundo fator à tela de configuração do front (two-factor.setup) em toda tela e ação Livewire. "Administrador" é quem entra no painel ou tem qualquer papel dele (Access\PanelAdministrators).
  • Papéis — a permissão de cada tela e de cada Action conferida no servidor, pelo mesmo gancho da trilha (AdminAuthorization).
  • Modo sistema das contas (twstec/kit-accounts) — o painel vê projetos e chaves de todas as contas: o OperateAdminPanelAsSystem entra na autenticação do painel, depois do acesso de admin, e é persistente. Ele declara o modo sistema da requisição (Accounts::systemModeForRequest) em vez de envolver o $next: nas ações, o Livewire reaplica os middlewares persistentes num pipeline à parte, antes do componente — um modo sistema só em volta do $next terminaria antes da ação. O fim da requisição o desfaz. As telas de projetos e chaves mostram a conta de cada linha (código ACC-…, com filtro), o dono da conta e quem criou; a exclusão de pessoa dona de conta com outros membros é recusada (ação escondida e, forjada, recusada com o motivo na trilha).

Opt-out só explícito: ADMIN_PROTECTIONS=false desliga a barreira de origem do painel, a conferência de acesso do pacote e a transação obrigatória — o aplicativo assume as três — e o pacote grava um aviso no log a cada boot. A trilha, o segundo fator e o modo sistema das contas continuam ligados (sem ele as telas de projetos e chaves não teriam conta atual).

Telas próprias do aplicativo no painel que leem dado de conta rodam no mesmo modo sistema (a requisição é do painel). Um teste com Livewire::test de uma tela do painel não passa pela pilha HTTP: declare o modo sistema no teste (o starter faz isso para tests/Feature/Admin no tests/Pest.php).

O dashboard de filas (/horizon) é do aplicativo (o Horizon não é dependência do pacote), mas o gate viewHorizon do starter lê o mesmo critério: AdminAccess::allows($user).

Tema e CSS

O pacote não entrega CSS compilado: o tema do painel é do aplicativo (os tokens de marca dele + o preset do Filament), compilado pelo Vite dele. O pacote entrega resources/css/sources.css, que só diz ao Tailwind 4 onde estão as classes das telas do pacote. O tema do aplicativo o importa:

@import '../../vendor/filament/filament/resources/css/theme.css';
@import '../../vendor/twstec/kit-admin/resources/css/sources.css';

Por ser um @import, o build falha se o pacote não estiver ao alcance — em vez de gerar em silêncio um tema sem as classes do painel. Com o pacote instalado por link (path repository), o build em container precisa montar a pasta dos pacotes (no starter: -v $(pwd)/../../packages:/packages).

Foto de perfil: só upload da própria pessoa

O campo de foto (cadastro de usuário e perfil do admin) grava pela função global de upload do twstec/kit-uploads — como foto pessoal (SecureUploadService::handlePersonal, sem conta, com o operador em created_by: a foto é da pessoa e aparece em todas as contas dela) — e só vincula como foto:

  • um upload enviado agora, naquele formulário (o servidor o gravou nesta requisição — Support\FreshAvatarUploads);
  • um upload da própria pessoa: foto pessoal que ela mesma enviou, upload da conta pessoal dela (pela web ou pela chave de API dela), ou a foto atual.

O valor do campo vem do navegador; apontá-lo para o upload de outra pessoa é recusado antes de gravar qualquer coisa (nem os outros campos mudam), com a recusa na trilha como denied (user.updated; na criação, user.created). A regra está em AvatarUpload::denialFor().

Uploads e Auditoria por conta

O painel opera em modo sistema (todas as contas). A tela de Uploads mostra a conta de cada linha (código, com filtro por conta), quem enviou e o tipo de dono (da conta, foto pessoal, órfão — com filtro); a de Auditoria filtra pela conta em que a ação aconteceu (audit_events.tenant_uuid, valor que não é uuid não derruba a consulta). Excluir uma pessoa pelo painel apaga os arquivos dela (regra do twstec/kit-uploads), com a linha upload.erased na trilha em nome do operador — menos o que está sob guarda legal, que fica desvinculado (tipo "Retido") com a recusa na trilha.

Cada upload mostra a classificação (com filtro) e o "guardar até". Abrir um confidencial é a ação "Abrir documento confidencial", com permissão própria uploads.view_confidential (fora de *.view): a URL nasce no clique e a geração e a visualização ficam na trilha com o contexto admin. As ações de guarda legal pedem uploads.legal_hold.

Excluir usuário respeita os impedimentos de exclusão declarados pelo aplicativo (twstec/kit-accounts): a ação some com o motivo; e um registro do aplicativo que aponta para a pessoa com chave estrangeira RESTRICT sem ter sido declarado vira recusa limpa na hora (notificação + denied na trilha, nada apagado), não o erro do banco. Com a aprovação em dois passos (users.delete), o mesmo vale em cada passo: impedimento recusa o pedido; a recusa que só aparece na execução deixa o pedido failed com a mensagem traduzida (Approvals\ExecutionRefused), nada apagado. Nos dois caminhos, a exclusão é a do caminho único do twstec/kit-accounts (Deletion\AccountDeletion::deleteUser), que grava a recusa na trilha uma vez só; o painel só avisa o operador. (UserAdminGuard::deleteRefusal(Closure) deu lugar a UserAdminGuard::delete($pessoa), que lança a recusa já gravada — Support\Exceptions\RecordedDenial.)

Nomes antigos → nomes novos

Até a 1.x o painel morava no aplicativo, em App\Filament\…. Os nomes antigos continuam resolvendo até a 3.0 (apelido preguiçoso, lista fechada em Compat\LegacyNames — as peças novas do pacote e uma tela que o aplicativo escreve em App\Filament\ não ganham nome antigo):

Antigo Novo
App\Filament\<resto> (as 59 classes do painel da 1.x) Twstec\Kit\Admin\<resto>
App\Console\Commands\MakeAdminUser, App\Core\Auth\Console\MakeAdminUser Twstec\Kit\Admin\Console\MakeAdminUser

Onde o nome antigo pode estar gravado, e o que acontece:

  • snapshot de componente Livewire aberto no navegador durante o deploy (o nome do componente de uma página do Filament é a classe): o Livewire acha a classe pelo nome antigo e a ação funciona;
  • config de dashboards publicada: o DashboardRegistry aceita o nome antigo e registra sempre o novo;
  • estado de tabela (itens por página, ordenação, filtros, busca, colunas — o Filament grava na sessão pelo nome da classe) e o modo tabela/cards: migram para o nome novo na primeira abertura da tela (Support\LegacySessionState, idempotente — uma escolha feita depois da atualização vence);
  • exports/imports do Filament e jobs: o painel não tem exportação, importação nem job com o nome da classe; a trilha de auditoria grava o nome curto do model (user), nunca a classe.

Lacunas conhecidas

  • A coluna de código público das listagens pede admin.common.code, chave que nunca existiu (a 1.x já mostrava a chave crua). A extração manteve a tela idêntica; a correção é uma mudança de texto própria.

Testes

A suíte do pacote (Pest + Orchestra Testbench) sobe uma aplicação Laravel limpa com o Filament, o model de usuário mínimo e um PanelProvider que só registra o plugin — nada do starter — e prova que as proteções e a trilha vêm do pacote: não-admin e admin inativo sem acesso (inclusive com model sem o contrato do Filament em ambiente local), allowlist de IP nas páginas, nas ações Livewire e no download de exports, a barreira em primeiro lugar em qualquer ordem do PanelProvider, criar/editar/excluir/bloquear usuário gravando a linha com quem, registro, antes/depois redigido, IP, User-Agent e correlation_id, recusa como denied, falha fechada, contas protegidas, user:make-admin, foto só de upload da própria conta, a matriz de permissões por papel no servidor (inclusive a chamada forjada: 403 + denied), atribuição de papel sem escalada, o último dono protegido, a aprovação em dois passos (quatro olhos, um operador, vencido, estado mudado, falha), nomes antigos, traduções (o aplicativo vence) e a arquitetura (só os pacotes do kit, o Laravel, o Filament e o Livewire).

docker compose exec -w /var/packages/admin app ./vendor/bin/pest
docker compose exec -w /var/packages/admin app ./vendor/bin/pint --test