feat: workspace-first managed tenants + RBAC membership UI fixes (072) #87

Merged
ahmido merged 13 commits from feat/072-managed-tenants-workspace-enforcement into dev 2026-02-02 23:54:23 +00:00
7 changed files with 9 additions and 17 deletions
Showing only changes of commit 37a5587a45 - Show all commits

View File

@ -15,14 +15,14 @@
class WorkspaceResource extends Resource class WorkspaceResource extends Resource
{ {
protected static bool $isDiscovered = false;
protected static ?string $model = Workspace::class; protected static ?string $model = Workspace::class;
protected static bool $isScopedToTenant = false; protected static bool $isScopedToTenant = false;
protected static ?string $recordTitleAttribute = 'name'; protected static ?string $recordTitleAttribute = 'name';
protected static bool $shouldRegisterNavigation = false;
protected static string|BackedEnum|null $navigationIcon = 'heroicon-o-squares-2x2'; protected static string|BackedEnum|null $navigationIcon = 'heroicon-o-squares-2x2';
protected static string|UnitEnum|null $navigationGroup = 'Settings'; protected static string|UnitEnum|null $navigationGroup = 'Settings';

View File

@ -32,10 +32,6 @@ public function handle(Request $request, Closure $next): Response
return $next($request); return $next($request);
} }
if (str_starts_with($path, '/admin/workspaces')) {
return $next($request);
}
if (in_array($path, ['/admin/no-access', '/admin/choose-workspace'], true)) { if (in_array($path, ['/admin/no-access', '/admin/choose-workspace'], true)) {
return $next($request); return $next($request);
} }

View File

@ -8,7 +8,6 @@
use App\Filament\Pages\NoAccess; use App\Filament\Pages\NoAccess;
use App\Filament\Pages\Tenancy\RegisterTenant; use App\Filament\Pages\Tenancy\RegisterTenant;
use App\Filament\Pages\TenantDashboard; use App\Filament\Pages\TenantDashboard;
use App\Filament\Resources\Workspaces\WorkspaceResource;
use App\Models\Tenant; use App\Models\Tenant;
use App\Support\Middleware\DenyNonMemberTenantAccess; use App\Support\Middleware\DenyNonMemberTenantAccess;
use Filament\Facades\Filament; use Filament\Facades\Filament;
@ -40,7 +39,6 @@ public function panel(Panel $panel): Panel
->path('admin') ->path('admin')
->login(Login::class) ->login(Login::class)
->authenticatedRoutes(function (Panel $panel): void { ->authenticatedRoutes(function (Panel $panel): void {
WorkspaceResource::registerRoutes($panel);
ChooseWorkspace::registerRoutes($panel); ChooseWorkspace::registerRoutes($panel);
ChooseTenant::registerRoutes($panel); ChooseTenant::registerRoutes($panel);
NoAccess::registerRoutes($panel); NoAccess::registerRoutes($panel);

View File

@ -40,7 +40,6 @@ public function handle(Request $request, Closure $next): Response
} }
$tenantParameter = $request->route()->parameter('tenant'); $tenantParameter = $request->route()->parameter('tenant');
$tenant = $panel->getTenant($tenantParameter); $tenant = $panel->getTenant($tenantParameter);
if (! $tenant instanceof Tenant) { if (! $tenant instanceof Tenant) {
@ -73,7 +72,6 @@ public function handle(Request $request, Closure $next): Response
} }
Filament::setTenant($tenant, true); Filament::setTenant($tenant, true);
$this->configureNavigationForRequest($panel); $this->configureNavigationForRequest($panel);
return $next($request); return $next($request);

View File

@ -88,6 +88,10 @@ public function resolveInitialWorkspaceFor(User $user, ?Request $request = null)
if (! $workspace instanceof Workspace || ! $this->isWorkspaceSelectable($workspace) || ! $this->isMember($user, $workspace)) { if (! $workspace instanceof Workspace || ! $this->isWorkspaceSelectable($workspace) || ! $this->isMember($user, $workspace)) {
$user->forceFill(['last_workspace_id' => null])->save(); $user->forceFill(['last_workspace_id' => null])->save();
} else {
$session->put(self::SESSION_KEY, (int) $workspace->getKey());
return $workspace;
} }
} }

View File

@ -54,7 +54,6 @@ class="cursor-pointer rounded-lg border border-gray-200 p-4 dark:border-gray-800
<form x-ref="form" method="POST" action="{{ route('admin.select-tenant') }}" class="flex flex-col gap-3"> <form x-ref="form" method="POST" action="{{ route('admin.select-tenant') }}" class="flex flex-col gap-3">
@csrf @csrf
<input type="hidden" name="tenant_id" value="{{ (int) $tenant->id }}" /> <input type="hidden" name="tenant_id" value="{{ (int) $tenant->id }}" />
<div class="font-medium text-gray-900 dark:text-gray-100"> <div class="font-medium text-gray-900 dark:text-gray-100">
{{ $tenant->name }} {{ $tenant->name }}
</div> </div>

View File

@ -26,7 +26,6 @@
Route::get('/admin/consent/start', TenantOnboardingController::class) Route::get('/admin/consent/start', TenantOnboardingController::class)
->name('admin.consent.start'); ->name('admin.consent.start');
// Panel root override: keep the app's workspace-first flow. // Panel root override: keep the app's workspace-first flow.
// Avoid Filament's tenancy root redirect which otherwise sends users to /admin/register-tenant // Avoid Filament's tenancy root redirect which otherwise sends users to /admin/register-tenant
// when no default tenant can be resolved. // when no default tenant can be resolved.
@ -50,7 +49,6 @@
return redirect()->to('/admin/choose-tenant'); return redirect()->to('/admin/choose-tenant');
}) })
->name('admin.home'); ->name('admin.home');
// Fallback route: Filament's layout generates this URL when tenancy registration is enabled. // 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 // In this app, package route registration may not always define it early enough, which breaks
// rendering on tenant-scoped routes. // rendering on tenant-scoped routes.
@ -125,7 +123,6 @@
Route::middleware(['web', 'auth', 'ensure-correct-guard:web', 'ensure-workspace-selected']) Route::middleware(['web', 'auth', 'ensure-correct-guard:web', 'ensure-workspace-selected'])
->post('/admin/select-tenant', SelectTenantController::class) ->post('/admin/select-tenant', SelectTenantController::class)
->name('admin.select-tenant'); ->name('admin.select-tenant');
Route::bind('workspace', function (string $value): Workspace { Route::bind('workspace', function (string $value): Workspace {
/** @var WorkspaceResolver $resolver */ /** @var WorkspaceResolver $resolver */
$resolver = app(WorkspaceResolver::class); $resolver = app(WorkspaceResolver::class);
@ -140,15 +137,15 @@
Route::middleware(['web', 'auth', 'ensure-workspace-member']) Route::middleware(['web', 'auth', 'ensure-workspace-member'])
->prefix('/admin/w/{workspace}') ->prefix('/admin/w/{workspace}')
->group(function (): void { ->group(function (): void {
Route::get('/', fn () => redirect('/admin/tenants')) Route::get('/', fn () => redirect('/admin/choose-tenant'))
->name('admin.workspace.home'); ->name('admin.workspace.home');
Route::get('/ping', fn () => response()->noContent())->name('admin.workspace.ping'); Route::get('/ping', fn () => response()->noContent())->name('admin.workspace.ping');
Route::get('/managed-tenants', fn () => redirect('/admin/tenants')) Route::get('/managed-tenants', fn () => redirect('/admin/choose-tenant'))
->name('admin.workspace.managed-tenants.index'); ->name('admin.workspace.managed-tenants.index');
Route::get('/managed-tenants/onboarding', fn () => redirect('/admin/tenants/create')) Route::get('/managed-tenants/onboarding', fn () => redirect('/admin/register-tenant'))
->name('admin.workspace.managed-tenants.onboarding'); ->name('admin.workspace.managed-tenants.onboarding');
}); });