feat: admin panel module with tenant-aware infrastructure#206
Conversation
Filament v5 changed $navigationIcon to accept string|BackedEnum|null.
Add Filament Authenticate middleware to protect admin routes. Add 5 Pest tests covering login page, auth redirects, and admin-only access enforcement in production.
Admin user now uses 'danielhe4rt' username (matches he4rt.admins config) and is attached to both He4rt and 3pontos tenants.
|
Warning Rate limit exceeded
To keep reviews running without waiting, you can enable usage-based add-on for your organization. This allows additional reviews beyond the hourly cap. Account admins can enable it under billing. ⌛ How to resolve this issue?After the wait time has elapsed, a review can be triggered using the We recommend that you space out your commits to avoid hitting the rate limit. 🚦 How do rate limits work?CodeRabbit enforces hourly rate limits for each developer per organization. Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout. Please see our FAQ for further information. ℹ️ Review info⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro Run ID: ⛔ Files ignored due to path filters (1)
📒 Files selected for processing (2)
📝 WalkthroughWalkthroughThe PR introduces a new Suggested reviewers
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Review rate limit: 0/1 reviews remaining, refill in 50 minutes and 14 seconds.Comment |
There was a problem hiding this comment.
Actionable comments posted: 6
🧹 Nitpick comments (3)
database/seeders/BaseSeeder.php (1)
23-31: ⚡ Quick winConsider adding an environment guard to prevent accidental production execution.
BaseSeedercontains no guard against running in production. Given the PR already ships a production migration, a misfire of this seeder in production would create duplicate admin users and tenants, or fail with constraint violations.🔒️ Suggested guard
public function run(): void { + if (app()->isProduction()) { + $this->command->warn('BaseSeeder is not intended for production. Skipping.'); + return; + } + $admin = User::factory()🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@database/seeders/BaseSeeder.php` around lines 23 - 31, Add an environment guard to BaseSeeder::run() so it refuses to run in production: check the application environment (e.g., using App::environment('production') or app()->environment()) at the start of run() and exit/throw/log a clear message if running in production; keep the rest of the seeding logic (User::factory(), tenant creation, etc.) unchanged so the seeder only executes in non-production environments.app-modules/panel-admin/tests/Feature/AdminPanelAccessTest.php (2)
31-34: ⚡ Quick win
assertRedirect()without a URL makes the test vacuously pass on a login redirect.Hitting
/admin(panel root without a tenant slug) while authenticated could still redirect to/admin/loginor a tenant selector, both of which satisfyassertRedirect(). Since the tenant is created in this test, the assertion should target the tenant-scoped URL and confirm the redirect reaches the dashboard rather than the login page.♻️ Suggested improvement
$this ->actingAs($user) - ->get('/admin') - ->assertRedirect(); + ->get('/admin/'.$tenant->slug) + ->assertRedirectContains('/admin/'.$tenant->slug);🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@app-modules/panel-admin/tests/Feature/AdminPanelAccessTest.php` around lines 31 - 34, Replace the loose redirect assertion so the test verifies it redirects to the tenant-scoped admin dashboard: where the test currently calls $this->actingAs($user)->get('/admin')->assertRedirect(), assertRedirect('/admin/'.$tenant->slug) (or the full dashboard path if you have one, e.g. '/admin/'.$tenant->slug.'/dashboard'), then perform a follow-up GET to that exact URL and assert a successful response or that the dashboard content is present (use the existing $tenant variable and the actingAs/get/assert methods).
37-45: ⚡ Quick winTest name is misleading — this exercises the non-production path that allows everyone, not admin-specific logic.
Looking at
app-modules/identity/src/User/Models/User.php(lines 131-137), in a non-production environmentcanAccessPanelfor the'admin'panel unconditionally returnstrueregardless of the username. Theconfig(['he4rt.admins' => 'danielhe4rt'])override on line 40 has no effect on the assertion. The test would pass forUser::factory()->create(['username' => 'not-an-admin'])equally.Either rename the test to reflect what is actually being verified (
'any user can access admin panel in non-production'), or force production mode here and rely on theisAdmin()check to confirm the true admin path.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@app-modules/panel-admin/tests/Feature/AdminPanelAccessTest.php` around lines 37 - 45, The test currently verifies the non-production shortcut (any user can access the admin panel) rather than admin-specific logic; either rename the test to reflect that behavior (e.g., "any user can access admin panel in non-production") or make the test exercise the production/admin path by forcing production environment (set config(['app.env' => 'production']) or equivalent) and then assert using the User::isAdmin() logic via canAccessPanel on Filament::getPanel('admin') with an admin and a non-admin user to confirm correct behavior; locate the test function name and the panel call (the test closure, Filament::getPanel('admin'), and User::factory()->create(...)) to modify accordingly.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@app-modules/panel-admin/src/Http/Middleware/ApplyTenantScopes.php`:
- Around line 23-28: The current loop in ApplyTenantScopes uses
Model::addGlobalScope which mutates protected static $globalScopes and can leak
tenant scopes across requests in long-running servers; update ApplyTenantScopes
to remove those scopes after the response by implementing a terminate() method
that iterates the same config('panel-admin.tenant_scoped_models', []) list and
cleans up each model (either by unsetting the specific 'tenant' scope from the
model's protected static $globalScopes via a small helper/trait or by calling
the model class's clearBootedModels() to force re-boot), ensuring
addGlobalScope(...) in the middleware is paired with a matching cleanup in
terminate() so per-request tenant scoping is not persisted.
In `@app-modules/panel-admin/tests/Feature/AdminPanelAccessTest.php`:
- Around line 47-57: The test mutates global app environment by calling
app()->detectEnvironment(fn () => 'production'), which can pollute other tests;
change the test to avoid permanent mutation by either mocking the environment
check (e.g., stub Application::isProduction() or the isProduction helper used by
canAccessPanel) or add an afterEach teardown to restore the environment (use
afterEach(fn() => app()->detectEnvironment(fn() => 'testing'))); ensure you keep
the Filament::getPanel('admin') and $user->canAccessPanel($panel) assertions
intact while preventing persistent changes to app()->detectEnvironment.
In `@app-modules/panel-admin/tests/Feature/ApplyTenantScopesTest.php`:
- Around line 13-33: The global scope applied by ApplyTenantScopes persists
across tests; add an afterEach in ApplyTenantScopesTest to clear booted model
state by calling Illuminate\Database\Eloquent\Model::clearBootedModels() (or
EventModel::clearBootedModels()) so the global scope added to EventModel during
the test is removed between tests.
In `@app/Providers/Filament/AdminPanelProvider.php`:
- Around line 37-53: The ApplyTenantScopes middleware is not registered so
tenant global scopes never run; add
He4rt\PanelAdmin\Http\Middleware\ApplyTenantScopes to the Filament tenant
middleware list so it runs after Filament resolves the tenant. Concretely, in
AdminPanelProvider (and likewise in PanelAdminServiceProvider if it registers
Filament middleware) add ApplyTenantScopes::class into the
->tenantMiddleware([...]) array (not ->middleware()) and ensure it is placed
after Filament\Tenant\IdentifyTenant (or the IdentifyTenant entry) so
ApplyTenantScopes can safely call Filament::getTenant().
In `@database/migrations/2026_05_01_200459_delete_orphaned_tenant_records.php`:
- Around line 8-49: The migration currently defines up() but leaves down() empty
(inherited no-op); add an explicit public function down(): void in the anonymous
class and make it throw an exception (e.g., new \RuntimeException or \Exception)
with a clear message like "Irreversible migration: deletes tenant records" to
document that rollback is unsupported; update the class that contains up() to
include this down() override so migrate:rollback surfaces the irreversibility
instead of silently doing nothing.
In `@database/seeders/BaseSeeder.php`:
- Around line 25-31: The seeder currently creates an admin with hardcoded PII
and a weak password; update the User::factory() call in BaseSeeder (the block
that assigns $admin and calls Hash::make('admin')) to use role-based placeholder
data (e.g., username 'admin', name 'Admin User', email 'admin@example.com')
instead of real developer details, and replace the weak literal password with a
secure value sourced from an environment variable or generated at runtime (e.g.,
env('SEED_ADMIN_PASSWORD') or a cryptographic random string) passed through
Hash::make; ensure the chosen approach either logs or outputs the generated
password securely for bootstrap use and document the env var so production runs
don’t create a predictable credential.
---
Nitpick comments:
In `@app-modules/panel-admin/tests/Feature/AdminPanelAccessTest.php`:
- Around line 31-34: Replace the loose redirect assertion so the test verifies
it redirects to the tenant-scoped admin dashboard: where the test currently
calls $this->actingAs($user)->get('/admin')->assertRedirect(),
assertRedirect('/admin/'.$tenant->slug) (or the full dashboard path if you have
one, e.g. '/admin/'.$tenant->slug.'/dashboard'), then perform a follow-up GET to
that exact URL and assert a successful response or that the dashboard content is
present (use the existing $tenant variable and the actingAs/get/assert methods).
- Around line 37-45: The test currently verifies the non-production shortcut
(any user can access the admin panel) rather than admin-specific logic; either
rename the test to reflect that behavior (e.g., "any user can access admin panel
in non-production") or make the test exercise the production/admin path by
forcing production environment (set config(['app.env' => 'production']) or
equivalent) and then assert using the User::isAdmin() logic via canAccessPanel
on Filament::getPanel('admin') with an admin and a non-admin user to confirm
correct behavior; locate the test function name and the panel call (the test
closure, Filament::getPanel('admin'), and User::factory()->create(...)) to
modify accordingly.
In `@database/seeders/BaseSeeder.php`:
- Around line 23-31: Add an environment guard to BaseSeeder::run() so it refuses
to run in production: check the application environment (e.g., using
App::environment('production') or app()->environment()) at the start of run()
and exit/throw/log a clear message if running in production; keep the rest of
the seeding logic (User::factory(), tenant creation, etc.) unchanged so the
seeder only executes in non-production environments.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: 3b2765f6-74bd-4c6d-bedc-09e7209ee58a
📒 Files selected for processing (19)
app-modules/panel-admin/composer.jsonapp-modules/panel-admin/config/panel-admin.phpapp-modules/panel-admin/database/migrations/.gitkeepapp-modules/panel-admin/phpstan.ignore.neonapp-modules/panel-admin/phpstan.neonapp-modules/panel-admin/resources/views/.gitkeepapp-modules/panel-admin/src/Http/Middleware/ApplyTenantScopes.phpapp-modules/panel-admin/src/Pages/Dashboard.phpapp-modules/panel-admin/src/PanelAdminServiceProvider.phpapp-modules/panel-admin/tests/Feature/.gitkeepapp-modules/panel-admin/tests/Feature/AdminPanelAccessTest.phpapp-modules/panel-admin/tests/Feature/ApplyTenantScopesTest.phpapp/Providers/Filament/AdminPanelProvider.phpbootstrap/providers.phpcomposer.jsondatabase/migrations/2026_05_01_200459_delete_orphaned_tenant_records.phpdatabase/seeders/BaseSeeder.phpdatabase/seeders/DatabaseSeeder.phpdatabase/seeders/ThreeDotsSeeder.php
💤 Files with no reviewable changes (2)
- database/seeders/DatabaseSeeder.php
- database/seeders/ThreeDotsSeeder.php
## Summary
Sincroniza o feature **Discord ETL** + **upgrade Laravel 12 → 13** da
branch `4.1` para `4.x`, descartando o tooling AI/MCP/Boost individual
que vieram bundled na branch original.
Como 4.x usa squash merges, não havia histórico granular dos 4 commits
originais — solução foi rebuilder via checkout seletivo + ajustes
manuais.
### Categorização dos 4 commits da 4.1
| Commit | Status | Files | Lines | Author |
|---|---|---|---|---|
| `4b4a136` Laravel 12→13 upgrade | KEEP (parcial) | 34 | +768 | Daniel
Reis |
| `84e26cf` Laravel Boost / AI tooling | MIXED — só pieces de Discord
profile | 44 | +5724 | kaster |
| `1449e80` wip console commands | KEEP | 6 | +1808 | Daniel Reis |
| `b0e0f00` Discord ETL feature | KEEP | 55 | +5014 | Daniel Reis |
## What's in
13 commits, +8433/−882 linhas em 94 arquivos:
| Commit | O que |
|---|---|
| `fcff50a` Discord ETL core | 9 migrations,
MembershipEvent/MessageAttachment/MessageEmbed/MessageMention/MessageThread/ModerationEvent/Reaction
models, HasReactions trait, SourceBot enum, 4 Actions, 4 DTOs,
DiscordMessageAdapter, ImportDiscordMessagesCommand, 116 ETL tests |
| `11e5a49` Discord profile ETL | ImportDiscordProfileAction + Command,
ConnectedAccountDTO, DiscordProfileDTO, IdentityProvider expandido com
24 cases (Spotify/Steam/Xbox/etc), ImportDiscordProfileTest |
| `3c31f3e` wip console commands | 6 comandos exploratórios:
discord:fetch-{members,profile,profiles}, discord:import-members,
discord:analyze-profiles, discord:community-report |
| `27d2e6c` Color::Dark → Color::Zinc | Bug do código 4.1 — Filament 5
não tem Color::Dark; crash em runtime quando EpicGames era renderizado |
| `4c5dbc4` ETL upsert + reply resolution | Bug do código 4.1 — Actions
usavam `create()` em vez de `updateOrCreate`, quebrando no unique index
`(tenant_id, provider_message_id)` na 2ª chamada. resolveReplyTargetId
não fazia fallback pra DB quando cache era miss |
| `1471958` PHPStan level 6 cleanup | Resolve 34 erros que vieram com o
transplante (return type narrowing, undefined property docblocks,
match.alwaysTrue, mixed!==null narrows, table-row casts em wip commands)
|
| `f23ffca` Laravel 12→13 upgrade + .gitignore align | composer bumps
(framework 12→13.7, tinker 2→3, backup 9→10), config/cache.php
serializable_classes, rector LARAVEL_130 set + skip 3 attribute
conversions (Fillable/Table/Appends), .gitignore alinhado com sycorax
(AI Agents section completa) |
| `95d75b6` Mantém laracord/bot-discord | Reverte remoção indevida —
fork `danielhe4rt/laracord-framework` + `tinker-zero` já suportam L13.
PR #195 body original mentia ao dizer que removeu |
| `762740a` Cleanup rector | Remove regras LARAVEL_130 redundantes do
`withSets`, drop `LARAVEL_FACTORIES` e outros sets que não rodavam |
| `9eff469` Style — object instantiation | Simplifica `(new
X)->method()` → `new X->method()` com PHP 8.4 syntax |
| `1141f2f` Merge `origin/4.x` | Traz PR #206 (admin panel module com
tenant-aware infrastructure) |
| `fe4f422` Untrack `.ai/mcp/mcp.json` | Config MCP do Laravel Boost com
path absoluto hardcoded — agora ignored pelo .gitignore |
| `a5b227f` Guidelines do sycorax | Adiciona
`.ai/guidelines/{filament,knowledge-base}.blade.php` (exception
`!.ai/guidelines/` no .gitignore) |
| `9a50d5a` Format .gitignore | Header da seção AI Agents corrigido |
## What's out
- **Refactor de 25 models** pra atributos PHP
`#[Fillable]`/`#[Table]`/`#[Appends]` — revertido pra `protected
$fillable` por consistência com 4.x (decisão do user). Rules
`FillablePropertyToFillableAttributeRector`,
`TablePropertyToTableAttributeRector`,
`AppendsPropertyToAppendsAttributeRector` skipped no rector
- **CSRF middleware rename** (`VerifyCsrfToken` →
`PreventRequestForgery`) nos PanelProviders — irrelevante pois 4.x
deletou todos esses providers via PR #203/#204 (admin virou módulo,
guest virou portal Livewire)
- **`.agents/skills/**`** (25 markdown skills do Laravel Boost)
- **`.mcp.json`, `boost.json`, `opencode.json`, `AGENTS.md`,
`CLAUDE.md`** (AI/MCP tooling individual)
- **Http Controllers/Requests removidos pelo PR #203**
(MessagesController, CreateMessageRequest, CreateVoiceMessageRequest) —
não re-adicionados
- **`tests/Feature/NewMessageTest.php`,
`tests/Feature/NewVoiceMessageTest.php`** — dependiam dos Http acima
## Sumário verificação cross-branch
Comparação byte-a-byte entre `sync/from-4.1-discord-etl`, `origin/4.x` e
`origin/4.1`:
| Categoria | Count | Status |
|---|---|---|
| Arquivos da 4.1 trazidos **idênticos** ao sync | **53** | ✅ bulk
checkout limpo (50 ETL + .gitignore + Makefile + BaseSeeder) |
| Arquivos da 4.1 com **diff intencional** no sync | **12** | ✅ 9 models
reverteram L13 attrs + 3 fixes nossos (Color, upsert profile,
upsert/reply message) |
| Arquivos da 4.1 **dropados** | **39** (era 69 — incluiu 25 model attrs
+ 5 PanelProviders + composer/cache/rector/L13 que agora trouxemos) | ✅
tudo no plano original |
| Arquivos no sync **fora da 4.1** | **2** | ✅ ActivityServiceProvider +
IntegrationDiscordServiceProvider em path 4.x (`src/`) |
## Co-authors
| Pessoa | Origem | Email usado |
|---|---|---|
| Daniel Reis | autor PR #195 (L13), #197 (ETL), commit wip |
`danielhe4rt@gmail.com` |
| kaster | autor commit Boost (parts de Discord profile vieram junto) |
`diogokaster@gmail.com` |
| thalesmengue | approved PR #197 |
`102062680+thalesmengue@users.noreply.github.com` |
| 1pride | approved PR #197 | `43507992+1pride@users.noreply.github.com`
|
## Test plan
- [x] `vendor/bin/pint --test --format agent` — pass
- [x] `vendor/bin/phpstan analyse --memory-limit=2G` — **0 errors**
(level 6)
- [x] `vendor/bin/rector --dry-run` — **0 changes**
- [x] `php artisan migrate` — todas 9 migrations aplicaram limpo
- [x] `php artisan test --compact` — **182 tests, 179 passed, 3 skipped,
0 failed** (608 assertions)
- 70 ImportDiscordMessage tests ✅
- 13 ImportDiscordProfile tests ✅
- 46 DiscordMessageAdapter tests ✅
- [x] Laravel framework 13.7.0 instalado e funcional
- [ ] Smoke test `php artisan discord:import-messages` com dump real
- [ ] Smoke test `php artisan discord:import-profiles` com chunks JSON
- [ ] Smoke test backup com `spatie/laravel-backup` v10
## Notas técnicas
### CodeRabbit false positive — `SimpleStrategy.php`
CodeRabbit alertou que `app/Tasks/Cleanup/Strategies/SimpleStrategy.php`
usa classes removidas em `spatie/laravel-backup` v10. **False
positive:**
- `Spatie\Backup\BackupDestination\Backup` ✅ existe em v10.2.1
- `Spatie\Backup\BackupDestination\BackupCollection` ✅ existe em v10.2.1
- `CleanupStrategy::deleteOldBackups(BackupCollection)` ✅ assinatura
inalterada
- Sycorax (template) usa código idêntico byte-a-byte com backup v10
CodeRabbit confundiu mudança de **events** (que viraram primitive data
em v10) com a API de cleanup strategy (sem mudança).
### Estratégia técnica
Cherry-pick não funcionaria por causa do conflito com refactor
`#[Fillable]`/`#[Table]` em models que existem em ambas as branches.
Solução foi `git checkout origin/4.1 -- <paths>` por chunks, depois 4
merges manuais:
- `Message.php` — recriado com novos campos ETL mantendo `protected
$fillable`
- `Voice.php` — idem + comentário sobre coexistência de duas
vocabularies de `state`
- `ActivityServiceProvider.php` — adicionado morphMap mantendo path
`src/` (4.x não moveu pra `src/Providers/`)
- `IntegrationDiscordServiceProvider.php` — registrado novos commands no
path `src/` de 4.x
---------
Co-authored-by: Daniel Reis <danielhe4rt@gmail.com>
Co-authored-by: thalesmengue <102062680+thalesmengue@users.noreply.github.com>
Co-authored-by: 1pride <43507992+1pride@users.noreply.github.com>
Co-authored-by: kaster <diogokaster@gmail.com>
Summary
panel-admin(app-modules/panel-admin/) with config, ServiceProvider, Dashboard page, andApplyTenantScopesmiddlewareAdminPanelProviderinapp/Providers/Filament/— Filament v5 panel with purple theme, tenant-aware viaTenantmodel with slug URLs (/admin/{tenant-slug}/...), auth middleware, and discovery-based resource loading from domain modulestenant_id=2records in productionThreeDotsSeeder, updatedBaseSeederwith super admin (danielhe4rt) attached to both tenantsArchitecture
Resources will live in their domain modules (e.g.
identity/src/Filament/Admin/Resources/) and are auto-discovered viaconfig('panel-admin.modules')+ the existingdiscoverResourcesForPanelmacro. Thepanel-adminmodule stays lean — only infrastructure.Test plan
ApplyTenantScopesmiddleware (scope applied, skipped when no tenant, empty config)Summary by CodeRabbit
New Features
Tests
Chores