TenantAtlas/routes/web.php
ahmido a989ef1a23 feat: workspace context enforcement (specs 070–072) (#85)
Implements specs 070–072 (workspace foundation, workspace-scoped tenant selection, managed-tenants workspace enforcement).

Highlights
- Adds Workspace + WorkspaceMembership models/migrations + middleware to persist/enforce current workspace context.
- Scopes tenant selection to the current workspace.
- Makes legacy `/admin/managed-tenants*` routes redirect into workspace-scoped URLs.
- Enforces tenant routes under `/admin/t/{tenant}` to 404 when workspace context is missing or mismatched.
- Fixes Filament page Blade wrappers so header actions render on choose-workspace / choose-tenant / no-access pages.

Verification
- Pint: `vendor/bin/sail bin pint --dirty`
- Tests: `vendor/bin/sail artisan test --compact tests/Feature/Guards/NoAdHocFilamentAuthPatternsTest.php tests/Feature/Workspaces tests/Feature/Filament/ChooseTenantIsWorkspaceScopedTest.php tests/Feature/Filament/ChooseTenantRequiresWorkspaceTest.php tests/Feature/Filament/TenantSwitcherUrlResolvesTenantTest.php tests/Feature/ManagedTenants tests/Feature/AdminNewRedirectTest.php`

Notes
- Filament v5 / Livewire v4 compatible.
- Panel provider registration stays in `bootstrap/providers.php` (Laravel 11+ rule).
- No new heavy frontend assets added.

Co-authored-by: Ahmed Darrazi <ahmeddarrazi@MacBookPro.fritz.box>
Reviewed-on: #85
2026-02-02 10:07:41 +00:00

131 lines
4.8 KiB
PHP

<?php
use App\Filament\Pages\Tenancy\RegisterTenant;
use App\Http\Controllers\AdminConsentCallbackController;
use App\Http\Controllers\Auth\EntraController;
use App\Http\Controllers\RbacDelegatedAuthController;
use App\Http\Controllers\TenantOnboardingController;
use App\Models\Workspace;
use App\Support\Middleware\DenyNonMemberTenantAccess;
use App\Support\Workspaces\WorkspaceContext;
use App\Support\Workspaces\WorkspaceResolver;
use Filament\Http\Middleware\Authenticate as FilamentAuthenticate;
use Filament\Http\Middleware\DisableBladeIconComponents;
use Filament\Http\Middleware\DispatchServingFilamentEvent;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Route;
Route::get('/', function () {
return view('welcome');
});
Route::get('/admin/consent/callback', AdminConsentCallbackController::class)
->name('admin.consent.callback');
Route::get('/admin/consent/start', TenantOnboardingController::class)
->name('admin.consent.start');
// Fallback route: Filament's layout generates this URL when tenancy registration is enabled.
// In this app, package route registration may not always define it early enough, which breaks
// rendering on tenant-scoped routes.
Route::middleware([
'web',
'panel:admin',
'ensure-correct-guard:web',
DenyNonMemberTenantAccess::class,
DisableBladeIconComponents::class,
DispatchServingFilamentEvent::class,
FilamentAuthenticate::class,
'ensure-workspace-selected',
])
->prefix('/admin')
->name('filament.admin.')
->get('/register-tenant', RegisterTenant::class)
->name('tenant.registration');
Route::get('/admin/rbac/start', [RbacDelegatedAuthController::class, 'start'])
->name('admin.rbac.start');
Route::get('/admin/rbac/callback', [RbacDelegatedAuthController::class, 'callback'])
->name('admin.rbac.callback');
Route::get('/auth/entra/redirect', [EntraController::class, 'redirect'])
->name('auth.entra.redirect');
Route::get('/auth/entra/callback', [EntraController::class, 'callback'])
->middleware('throttle:entra-callback')
->name('auth.entra.callback');
Route::middleware(['web', 'auth', 'ensure-workspace-selected'])
->get('/admin/managed-tenants', function (Request $request) {
$workspace = app(WorkspaceContext::class)->currentWorkspace($request);
if (! $workspace instanceof Workspace) {
return redirect('/admin/choose-workspace');
}
return redirect('/admin/w/'.($workspace->slug ?? $workspace->getKey()).'/managed-tenants');
})
->name('admin.legacy.managed-tenants.index');
Route::middleware(['web', 'auth', 'ensure-workspace-selected'])
->get('/admin/managed-tenants/onboarding', function (Request $request) {
$workspace = app(WorkspaceContext::class)->currentWorkspace($request);
if (! $workspace instanceof Workspace) {
return redirect('/admin/choose-workspace');
}
return redirect('/admin/w/'.($workspace->slug ?? $workspace->getKey()).'/managed-tenants/onboarding');
})
->name('admin.legacy.managed-tenants.onboarding');
Route::middleware(['web', 'auth', 'ensure-workspace-selected'])
->get('/admin/new', function (Request $request) {
$workspace = app(WorkspaceContext::class)->currentWorkspace($request);
if (! $workspace instanceof Workspace) {
return redirect('/admin/choose-workspace');
}
return redirect('/admin/w/'.($workspace->slug ?? $workspace->getKey()).'/managed-tenants/onboarding');
})
->name('admin.legacy.onboarding');
Route::bind('workspace', function (string $value): Workspace {
/** @var WorkspaceResolver $resolver */
$resolver = app(WorkspaceResolver::class);
$workspace = $resolver->resolve($value);
abort_unless($workspace instanceof Workspace, 404);
return $workspace;
});
Route::middleware(['web', 'auth', 'ensure-workspace-member'])
->prefix('/admin/w/{workspace}')
->group(function (): void {
Route::get('/', fn () => redirect('/admin/tenants'))
->name('admin.workspace.home');
Route::get('/ping', fn () => response()->noContent())->name('admin.workspace.ping');
Route::get('/managed-tenants', fn () => redirect('/admin/tenants'))
->name('admin.workspace.managed-tenants.index');
Route::get('/managed-tenants/onboarding', fn () => redirect('/admin/tenants/create'))
->name('admin.workspace.managed-tenants.onboarding');
});
if (app()->runningUnitTests()) {
Route::middleware(['web', 'auth', 'ensure-workspace-selected'])
->get('/admin/_test/workspace-context', function (Request $request) {
$workspaceId = app(\App\Support\Workspaces\WorkspaceContext::class)->currentWorkspaceId($request);
return response()->json([
'workspace_id' => $workspaceId,
]);
});
}