feat: workspace context enforcement + ownership safeguards #86
@ -9,7 +9,6 @@
|
||||
use App\Models\WorkspaceMembership;
|
||||
use App\Support\Workspaces\WorkspaceContext;
|
||||
use Filament\Actions\Action;
|
||||
use Filament\Facades\Filament;
|
||||
use Filament\Forms\Components\TextInput;
|
||||
use Filament\Notifications\Notification;
|
||||
use Filament\Pages\Page;
|
||||
@ -138,25 +137,34 @@ public function createWorkspace(array $data): void
|
||||
|
||||
private function redirectAfterWorkspaceSelected(User $user): string
|
||||
{
|
||||
$tenants = $user->getTenants(Filament::getCurrentOrDefaultPanel());
|
||||
$workspaceId = app(WorkspaceContext::class)->currentWorkspaceId();
|
||||
|
||||
$tenants = $tenants instanceof Collection ? $tenants : collect($tenants);
|
||||
if ($workspaceId === null) {
|
||||
return self::getUrl();
|
||||
}
|
||||
|
||||
if ($tenants->isEmpty()) {
|
||||
$workspaceId = app(WorkspaceContext::class)->currentWorkspaceId();
|
||||
$workspace = Workspace::query()->whereKey($workspaceId)->first();
|
||||
|
||||
if ($workspaceId !== null) {
|
||||
$role = WorkspaceMembership::query()
|
||||
->where('workspace_id', $workspaceId)
|
||||
->where('user_id', $user->getKey())
|
||||
->value('role');
|
||||
if (! $workspace instanceof Workspace) {
|
||||
return self::getUrl();
|
||||
}
|
||||
|
||||
if (in_array($role, ['owner', 'manager'], true)) {
|
||||
return route('filament.admin.tenant.registration');
|
||||
}
|
||||
$tenantsQuery = $user->tenants()
|
||||
->where('workspace_id', $workspace->getKey())
|
||||
->where('status', 'active');
|
||||
|
||||
$tenantCount = (int) $tenantsQuery->count();
|
||||
|
||||
if ($tenantCount === 0) {
|
||||
return route('admin.workspace.managed-tenants.index', ['workspace' => $workspace->slug ?? $workspace->getKey()]);
|
||||
}
|
||||
|
||||
if ($tenantCount === 1) {
|
||||
$tenant = $tenantsQuery->first();
|
||||
|
||||
if ($tenant !== null) {
|
||||
return TenantDashboard::getUrl(tenant: $tenant);
|
||||
}
|
||||
|
||||
return ChooseTenant::getUrl();
|
||||
}
|
||||
|
||||
return ChooseTenant::getUrl();
|
||||
|
||||
85
app/Filament/Pages/Workspaces/ManagedTenantsLanding.php
Normal file
85
app/Filament/Pages/Workspaces/ManagedTenantsLanding.php
Normal file
@ -0,0 +1,85 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Filament\Pages\Workspaces;
|
||||
|
||||
use App\Filament\Pages\ChooseTenant;
|
||||
use App\Filament\Pages\Tenancy\RegisterTenant as RegisterTenantPage;
|
||||
use App\Filament\Pages\TenantDashboard;
|
||||
use App\Models\Tenant;
|
||||
use App\Models\User;
|
||||
use App\Models\Workspace;
|
||||
use Filament\Pages\Page;
|
||||
use Illuminate\Database\Eloquent\Collection;
|
||||
|
||||
class ManagedTenantsLanding extends Page
|
||||
{
|
||||
protected static bool $shouldRegisterNavigation = false;
|
||||
|
||||
protected static bool $isDiscovered = false;
|
||||
|
||||
protected static ?string $title = 'Managed tenants';
|
||||
|
||||
protected string $view = 'filament.pages.workspaces.managed-tenants-landing';
|
||||
|
||||
public Workspace $workspace;
|
||||
|
||||
public function mount(Workspace $workspace): void
|
||||
{
|
||||
$this->workspace = $workspace;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Collection<int, Tenant>
|
||||
*/
|
||||
public function getTenants(): Collection
|
||||
{
|
||||
$user = auth()->user();
|
||||
|
||||
if (! $user instanceof User) {
|
||||
return Tenant::query()->whereRaw('1 = 0')->get();
|
||||
}
|
||||
|
||||
return $user->tenants()
|
||||
->where('workspace_id', $this->workspace->getKey())
|
||||
->where('status', 'active')
|
||||
->orderBy('name')
|
||||
->get();
|
||||
}
|
||||
|
||||
public function canRegisterTenant(): bool
|
||||
{
|
||||
return RegisterTenantPage::canView();
|
||||
}
|
||||
|
||||
public function goToChooseTenant(): void
|
||||
{
|
||||
$this->redirect(ChooseTenant::getUrl());
|
||||
}
|
||||
|
||||
public function openTenant(int $tenantId): void
|
||||
{
|
||||
$user = auth()->user();
|
||||
|
||||
if (! $user instanceof User) {
|
||||
abort(403);
|
||||
}
|
||||
|
||||
$tenant = Tenant::query()
|
||||
->where('status', 'active')
|
||||
->where('workspace_id', $this->workspace->getKey())
|
||||
->whereKey($tenantId)
|
||||
->first();
|
||||
|
||||
if (! $tenant instanceof Tenant) {
|
||||
abort(404);
|
||||
}
|
||||
|
||||
if (! $user->canAccessTenant($tenant)) {
|
||||
abort(404);
|
||||
}
|
||||
|
||||
$this->redirect(TenantDashboard::getUrl(tenant: $tenant));
|
||||
}
|
||||
}
|
||||
@ -5,11 +5,10 @@
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Filament\Pages\ChooseTenant;
|
||||
use App\Filament\Pages\Tenancy\RegisterTenant as RegisterTenantPage;
|
||||
use App\Filament\Pages\TenantDashboard;
|
||||
use App\Models\User;
|
||||
use App\Models\Workspace;
|
||||
use App\Support\Workspaces\WorkspaceContext;
|
||||
use Filament\Facades\Filament;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
@ -45,15 +44,22 @@ public function __invoke(Request $request): RedirectResponse
|
||||
|
||||
$context->setCurrentWorkspace($workspace, $user, $request);
|
||||
|
||||
$tenants = $user->getTenants(Filament::getCurrentOrDefaultPanel());
|
||||
$tenants = $tenants instanceof \Illuminate\Database\Eloquent\Collection ? $tenants : collect($tenants);
|
||||
$tenantsQuery = $user->tenants()
|
||||
->where('workspace_id', $workspace->getKey())
|
||||
->where('status', 'active');
|
||||
|
||||
if ($tenants->isEmpty()) {
|
||||
if (RegisterTenantPage::canView()) {
|
||||
return redirect()->route('filament.admin.tenant.registration');
|
||||
$tenantCount = (int) $tenantsQuery->count();
|
||||
|
||||
if ($tenantCount === 0) {
|
||||
return redirect()->route('admin.workspace.managed-tenants.index', ['workspace' => $workspace->slug ?? $workspace->getKey()]);
|
||||
}
|
||||
|
||||
if ($tenantCount === 1) {
|
||||
$tenant = $tenantsQuery->first();
|
||||
|
||||
if ($tenant !== null) {
|
||||
return redirect()->to(TenantDashboard::getUrl(tenant: $tenant));
|
||||
}
|
||||
|
||||
return redirect()->to(ChooseTenant::getUrl());
|
||||
}
|
||||
|
||||
return redirect()->to(ChooseTenant::getUrl());
|
||||
|
||||
@ -4,39 +4,27 @@
|
||||
|
||||
namespace App\Services\Auth;
|
||||
|
||||
use App\Filament\Pages\TenantDashboard;
|
||||
use App\Models\Tenant;
|
||||
use App\Models\User;
|
||||
use Illuminate\Support\Collection;
|
||||
use App\Models\WorkspaceMembership;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
class PostLoginRedirectResolver
|
||||
{
|
||||
public function resolve(User $user): string
|
||||
{
|
||||
$tenants = $this->getActiveTenants($user);
|
||||
$membershipQuery = WorkspaceMembership::query()->where('user_id', $user->getKey());
|
||||
|
||||
if ($tenants->isEmpty()) {
|
||||
$hasAnyActiveMembership = Schema::hasColumn('workspaces', 'archived_at')
|
||||
? $membershipQuery
|
||||
->join('workspaces', 'workspace_memberships.workspace_id', '=', 'workspaces.id')
|
||||
->whereNull('workspaces.archived_at')
|
||||
->exists()
|
||||
: $membershipQuery->exists();
|
||||
|
||||
if (! $hasAnyActiveMembership) {
|
||||
return '/admin/no-access';
|
||||
}
|
||||
|
||||
if ($tenants->count() === 1) {
|
||||
/** @var Tenant $tenant */
|
||||
$tenant = $tenants->first();
|
||||
|
||||
return TenantDashboard::getUrl(tenant: $tenant);
|
||||
}
|
||||
|
||||
return '/admin/choose-tenant';
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Collection<int, Tenant>
|
||||
*/
|
||||
private function getActiveTenants(User $user): Collection
|
||||
{
|
||||
return $user->tenants()
|
||||
->where('status', 'active')
|
||||
->orderBy('name')
|
||||
->get();
|
||||
return '/admin';
|
||||
}
|
||||
}
|
||||
|
||||
@ -25,6 +25,8 @@ public function handle(Request $request, Closure $next): Response
|
||||
{
|
||||
$panel = Filament::getCurrentOrDefaultPanel();
|
||||
|
||||
$path = '/'.ltrim($request->path(), '/');
|
||||
|
||||
if ($request->route()?->hasParameter('tenant')) {
|
||||
$user = $request->user();
|
||||
|
||||
@ -78,6 +80,16 @@ public function handle(Request $request, Closure $next): Response
|
||||
return $next($request);
|
||||
}
|
||||
|
||||
if (
|
||||
str_starts_with($path, '/admin/w/')
|
||||
|| str_starts_with($path, '/admin/workspaces')
|
||||
|| in_array($path, ['/admin/choose-workspace', '/admin/choose-tenant', '/admin/no-access'], true)
|
||||
) {
|
||||
$this->configureNavigationForRequest($panel);
|
||||
|
||||
return $next($request);
|
||||
}
|
||||
|
||||
if (filled(Filament::getTenant())) {
|
||||
$this->configureNavigationForRequest($panel);
|
||||
|
||||
|
||||
@ -0,0 +1,78 @@
|
||||
<x-filament-panels::page>
|
||||
<x-filament::section>
|
||||
<div class="flex flex-col gap-4">
|
||||
<div class="text-sm text-gray-600 dark:text-gray-300">
|
||||
Workspace: <span class="font-medium text-gray-900 dark:text-gray-100">{{ $this->workspace->name }}</span>
|
||||
</div>
|
||||
|
||||
@php
|
||||
$tenants = $this->getTenants();
|
||||
@endphp
|
||||
|
||||
@if ($tenants->isEmpty())
|
||||
<div class="rounded-md border border-gray-200 bg-gray-50 p-4 text-sm text-gray-700 dark:border-gray-800 dark:bg-gray-900 dark:text-gray-200">
|
||||
<div class="font-medium text-gray-900 dark:text-gray-100">No managed tenants yet.</div>
|
||||
<div class="mt-1 text-sm text-gray-600 dark:text-gray-300">
|
||||
Add a managed tenant to start inventory, drift, backups, and policy management.
|
||||
</div>
|
||||
|
||||
<div class="mt-4 flex flex-col gap-2 sm:flex-row">
|
||||
@if ($this->canRegisterTenant())
|
||||
<x-filament::button
|
||||
type="button"
|
||||
color="primary"
|
||||
tag="a"
|
||||
href="{{ route('filament.admin.tenant.registration') }}"
|
||||
>
|
||||
Add managed tenant
|
||||
</x-filament::button>
|
||||
@endif
|
||||
|
||||
<x-filament::button
|
||||
type="button"
|
||||
color="gray"
|
||||
tag="a"
|
||||
href="{{ route('filament.admin.pages.choose-workspace') }}"
|
||||
>
|
||||
Change workspace
|
||||
</x-filament::button>
|
||||
</div>
|
||||
</div>
|
||||
@else
|
||||
<div class="flex items-center justify-between gap-3">
|
||||
<div class="text-sm text-gray-600 dark:text-gray-300">
|
||||
{{ $tenants->count() }} managed tenant{{ $tenants->count() === 1 ? '' : 's' }}
|
||||
</div>
|
||||
|
||||
<x-filament::button
|
||||
type="button"
|
||||
color="gray"
|
||||
wire:click="goToChooseTenant"
|
||||
>
|
||||
Choose tenant
|
||||
</x-filament::button>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 gap-3 sm:grid-cols-2 lg:grid-cols-3">
|
||||
@foreach ($tenants as $tenant)
|
||||
<div wire:key="tenant-{{ $tenant->id }}" class="rounded-lg border border-gray-200 p-4 dark:border-gray-800">
|
||||
<div class="flex flex-col gap-3">
|
||||
<div class="font-medium text-gray-900 dark:text-gray-100">
|
||||
{{ $tenant->name }}
|
||||
</div>
|
||||
|
||||
<x-filament::button
|
||||
type="button"
|
||||
color="primary"
|
||||
wire:click="openTenant({{ (int) $tenant->id }})"
|
||||
>
|
||||
Open
|
||||
</x-filament::button>
|
||||
</div>
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
</x-filament::section>
|
||||
</x-filament-panels::page>
|
||||
@ -1,12 +1,14 @@
|
||||
<?php
|
||||
|
||||
use App\Filament\Pages\Tenancy\RegisterTenant;
|
||||
use App\Filament\Pages\TenantDashboard;
|
||||
use App\Http\Controllers\AdminConsentCallbackController;
|
||||
use App\Http\Controllers\Auth\EntraController;
|
||||
use App\Http\Controllers\RbacDelegatedAuthController;
|
||||
use App\Http\Controllers\SelectTenantController;
|
||||
use App\Http\Controllers\SwitchWorkspaceController;
|
||||
use App\Http\Controllers\TenantOnboardingController;
|
||||
use App\Models\User;
|
||||
use App\Models\Workspace;
|
||||
use App\Support\Middleware\DenyNonMemberTenantAccess;
|
||||
use App\Support\Workspaces\WorkspaceContext;
|
||||
@ -42,10 +44,40 @@
|
||||
->get('/admin', function (Request $request) {
|
||||
$workspaceId = app(WorkspaceContext::class)->currentWorkspaceId($request);
|
||||
|
||||
$user = $request->user();
|
||||
|
||||
if (! $user instanceof User) {
|
||||
return redirect()->to('/admin/choose-workspace');
|
||||
}
|
||||
|
||||
if ($workspaceId === null) {
|
||||
return redirect()->to('/admin/choose-workspace');
|
||||
}
|
||||
|
||||
$workspace = Workspace::query()->whereKey($workspaceId)->first();
|
||||
|
||||
if (! $workspace instanceof Workspace) {
|
||||
return redirect()->to('/admin/choose-workspace');
|
||||
}
|
||||
|
||||
$tenantsQuery = $user->tenants()
|
||||
->where('workspace_id', $workspace->getKey())
|
||||
->where('status', 'active');
|
||||
|
||||
$tenantCount = (int) $tenantsQuery->count();
|
||||
|
||||
if ($tenantCount === 0) {
|
||||
return redirect()->route('admin.workspace.managed-tenants.index', ['workspace' => $workspace->slug ?? $workspace->getKey()]);
|
||||
}
|
||||
|
||||
if ($tenantCount === 1) {
|
||||
$tenant = $tenantsQuery->first();
|
||||
|
||||
if ($tenant !== null) {
|
||||
return redirect()->to(TenantDashboard::getUrl(tenant: $tenant));
|
||||
}
|
||||
}
|
||||
|
||||
return redirect()->to('/admin/choose-tenant');
|
||||
})
|
||||
->name('admin.home');
|
||||
@ -137,18 +169,29 @@
|
||||
Route::middleware(['web', 'auth', 'ensure-workspace-member'])
|
||||
->prefix('/admin/w/{workspace}')
|
||||
->group(function (): void {
|
||||
Route::get('/', fn () => redirect('/admin/choose-tenant'))
|
||||
Route::get('/', fn () => redirect()->route('admin.workspace.managed-tenants.index', ['workspace' => request()->route('workspace')]))
|
||||
->name('admin.workspace.home');
|
||||
|
||||
Route::get('/ping', fn () => response()->noContent())->name('admin.workspace.ping');
|
||||
|
||||
Route::get('/managed-tenants', fn () => redirect('/admin/choose-tenant'))
|
||||
->name('admin.workspace.managed-tenants.index');
|
||||
|
||||
Route::get('/managed-tenants/onboarding', fn () => redirect('/admin/register-tenant'))
|
||||
->name('admin.workspace.managed-tenants.onboarding');
|
||||
});
|
||||
|
||||
Route::middleware([
|
||||
'web',
|
||||
'panel:admin',
|
||||
'ensure-correct-guard:web',
|
||||
DenyNonMemberTenantAccess::class,
|
||||
DisableBladeIconComponents::class,
|
||||
DispatchServingFilamentEvent::class,
|
||||
FilamentAuthenticate::class,
|
||||
'ensure-workspace-member',
|
||||
'ensure-filament-tenant-selected',
|
||||
])
|
||||
->get('/admin/w/{workspace}/managed-tenants', \App\Filament\Pages\Workspaces\ManagedTenantsLanding::class)
|
||||
->name('admin.workspace.managed-tenants.index');
|
||||
|
||||
if (app()->runningUnitTests()) {
|
||||
Route::middleware(['web', 'auth', 'ensure-workspace-selected'])
|
||||
->get('/admin/_test/workspace-context', function (Request $request) {
|
||||
|
||||
@ -21,7 +21,7 @@ ## UX follow-ups
|
||||
- [x] T210 Add a workspace switcher in the user menu (link to Choose Workspace).
|
||||
- [x] T220 Add regression tests for workspace switcher + tenant selection.
|
||||
- [x] T230 Ensure `/admin` lands on workspace-first flow (avoid redirecting to tenant registration).
|
||||
- [x] T240 After choosing a workspace with zero tenants, route into tenant registration (not empty Choose Tenant).
|
||||
- [x] T240 After choosing a workspace with zero tenants, route into the workspace Managed Tenants landing (with CTA).
|
||||
- [x] T250 Allow workspace owners to register the first tenant in a workspace (bootstrap).
|
||||
|
||||
## Security hardening (owners / audit / recovery)
|
||||
|
||||
@ -2,10 +2,11 @@
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use App\Filament\Pages\TenantDashboard;
|
||||
use App\Models\Tenant;
|
||||
use App\Models\TenantMembership;
|
||||
use App\Models\User;
|
||||
use App\Models\Workspace;
|
||||
use App\Models\WorkspaceMembership;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
|
||||
@ -72,7 +73,17 @@ function entra_fake_token_exchange(string $tid, string $oid): void
|
||||
'entra_object_id' => 'object-1',
|
||||
]);
|
||||
|
||||
$tenant = Tenant::factory()->create(['status' => 'active']);
|
||||
$workspace = Workspace::factory()->create();
|
||||
WorkspaceMembership::factory()->create([
|
||||
'workspace_id' => $workspace->getKey(),
|
||||
'user_id' => $user->getKey(),
|
||||
'role' => 'owner',
|
||||
]);
|
||||
|
||||
$tenant = Tenant::factory()->create([
|
||||
'status' => 'active',
|
||||
'workspace_id' => $workspace->getKey(),
|
||||
]);
|
||||
|
||||
TenantMembership::query()->create([
|
||||
'tenant_id' => $tenant->getKey(),
|
||||
@ -89,7 +100,7 @@ function entra_fake_token_exchange(string $tid, string $oid): void
|
||||
->withSession(['entra_state' => $state])
|
||||
->get(route('auth.entra.callback', ['code' => 'code-123', 'state' => $state]));
|
||||
|
||||
$response->assertRedirect(TenantDashboard::getUrl(tenant: $tenant));
|
||||
$response->assertRedirect('/admin');
|
||||
});
|
||||
|
||||
it('routes to choose-tenant when user has multiple tenant memberships', function () {
|
||||
@ -101,7 +112,17 @@ function entra_fake_token_exchange(string $tid, string $oid): void
|
||||
'entra_object_id' => 'object-1',
|
||||
]);
|
||||
|
||||
$tenants = Tenant::factory()->count(2)->create(['status' => 'active']);
|
||||
$workspace = Workspace::factory()->create();
|
||||
WorkspaceMembership::factory()->create([
|
||||
'workspace_id' => $workspace->getKey(),
|
||||
'user_id' => $user->getKey(),
|
||||
'role' => 'owner',
|
||||
]);
|
||||
|
||||
$tenants = Tenant::factory()->count(2)->create([
|
||||
'status' => 'active',
|
||||
'workspace_id' => $workspace->getKey(),
|
||||
]);
|
||||
|
||||
foreach ($tenants as $tenant) {
|
||||
TenantMembership::query()->create([
|
||||
@ -120,5 +141,5 @@ function entra_fake_token_exchange(string $tid, string $oid): void
|
||||
->withSession(['entra_state' => $state])
|
||||
->get(route('auth.entra.callback', ['code' => 'code-123', 'state' => $state]));
|
||||
|
||||
$response->assertRedirect('/admin/choose-tenant');
|
||||
$response->assertRedirect('/admin');
|
||||
});
|
||||
|
||||
@ -2,6 +2,9 @@
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use App\Filament\Pages\TenantDashboard;
|
||||
use App\Models\Tenant;
|
||||
use App\Models\TenantMembership;
|
||||
use App\Models\User;
|
||||
use App\Models\Workspace;
|
||||
use App\Models\WorkspaceMembership;
|
||||
@ -10,7 +13,7 @@
|
||||
|
||||
uses(RefreshDatabase::class);
|
||||
|
||||
it('redirects /admin to choose-tenant when a workspace is selected', function (): void {
|
||||
it('redirects /admin to the workspace managed-tenants landing when a workspace is selected and has no tenants', function (): void {
|
||||
$user = User::factory()->create();
|
||||
|
||||
$workspace = Workspace::factory()->create();
|
||||
@ -25,5 +28,71 @@
|
||||
->actingAs($user)
|
||||
->withSession([WorkspaceContext::SESSION_KEY => (int) $workspace->getKey()])
|
||||
->get('/admin')
|
||||
->assertRedirect(route('filament.admin.pages.choose-tenant'));
|
||||
->assertRedirect(route('admin.workspace.managed-tenants.index', ['workspace' => $workspace->slug ?? $workspace->getKey()]));
|
||||
});
|
||||
|
||||
it('redirects /admin to choose-tenant when a workspace is selected and has multiple tenants', function (): void {
|
||||
$user = User::factory()->create();
|
||||
|
||||
$workspace = Workspace::factory()->create();
|
||||
|
||||
WorkspaceMembership::factory()->create([
|
||||
'workspace_id' => $workspace->getKey(),
|
||||
'user_id' => $user->getKey(),
|
||||
'role' => 'owner',
|
||||
]);
|
||||
|
||||
$tenants = Tenant::factory()->count(2)->create([
|
||||
'status' => 'active',
|
||||
'workspace_id' => $workspace->getKey(),
|
||||
]);
|
||||
|
||||
foreach ($tenants as $tenant) {
|
||||
TenantMembership::query()->create([
|
||||
'tenant_id' => $tenant->getKey(),
|
||||
'user_id' => $user->getKey(),
|
||||
'role' => 'owner',
|
||||
'source' => 'manual',
|
||||
'source_ref' => null,
|
||||
'created_by_user_id' => null,
|
||||
]);
|
||||
}
|
||||
|
||||
$this
|
||||
->actingAs($user)
|
||||
->withSession([WorkspaceContext::SESSION_KEY => (int) $workspace->getKey()])
|
||||
->get('/admin')
|
||||
->assertRedirect('/admin/choose-tenant');
|
||||
});
|
||||
|
||||
it('redirects /admin to the tenant dashboard when a workspace is selected and has exactly one tenant', function (): void {
|
||||
$user = User::factory()->create();
|
||||
|
||||
$workspace = Workspace::factory()->create();
|
||||
|
||||
WorkspaceMembership::factory()->create([
|
||||
'workspace_id' => $workspace->getKey(),
|
||||
'user_id' => $user->getKey(),
|
||||
'role' => 'owner',
|
||||
]);
|
||||
|
||||
$tenant = Tenant::factory()->create([
|
||||
'status' => 'active',
|
||||
'workspace_id' => $workspace->getKey(),
|
||||
]);
|
||||
|
||||
TenantMembership::query()->create([
|
||||
'tenant_id' => $tenant->getKey(),
|
||||
'user_id' => $user->getKey(),
|
||||
'role' => 'owner',
|
||||
'source' => 'manual',
|
||||
'source_ref' => null,
|
||||
'created_by_user_id' => null,
|
||||
]);
|
||||
|
||||
$this
|
||||
->actingAs($user)
|
||||
->withSession([WorkspaceContext::SESSION_KEY => (int) $workspace->getKey()])
|
||||
->get('/admin')
|
||||
->assertRedirect(TenantDashboard::getUrl(tenant: $tenant));
|
||||
});
|
||||
|
||||
@ -3,6 +3,7 @@
|
||||
declare(strict_types=1);
|
||||
|
||||
use App\Filament\Pages\ChooseWorkspace;
|
||||
use App\Filament\Pages\TenantDashboard;
|
||||
use App\Models\Tenant;
|
||||
use App\Models\TenantMembership;
|
||||
use App\Models\User;
|
||||
@ -13,7 +14,7 @@
|
||||
|
||||
uses(RefreshDatabase::class);
|
||||
|
||||
it('redirects to tenant registration after selecting a workspace with no tenants', function (): void {
|
||||
it('redirects to the workspace managed-tenants landing after selecting a workspace with no tenants', function (): void {
|
||||
$user = User::factory()->create();
|
||||
|
||||
$workspace = Workspace::factory()->create();
|
||||
@ -26,10 +27,10 @@
|
||||
Livewire::actingAs($user)
|
||||
->test(ChooseWorkspace::class)
|
||||
->call('selectWorkspace', $workspace->getKey())
|
||||
->assertRedirect(route('filament.admin.tenant.registration'));
|
||||
->assertRedirect(route('admin.workspace.managed-tenants.index', ['workspace' => $workspace->slug ?? $workspace->getKey()]));
|
||||
});
|
||||
|
||||
it('redirects to choose-tenant after selecting a workspace with tenants', function (): void {
|
||||
it('redirects to the tenant dashboard after selecting a workspace with exactly one tenant', function (): void {
|
||||
$user = User::factory()->create();
|
||||
|
||||
$workspace = Workspace::factory()->create();
|
||||
@ -53,6 +54,38 @@
|
||||
'created_by_user_id' => null,
|
||||
]);
|
||||
|
||||
Livewire::actingAs($user)
|
||||
->test(ChooseWorkspace::class)
|
||||
->call('selectWorkspace', $workspace->getKey())
|
||||
->assertRedirect(TenantDashboard::getUrl(tenant: $tenant));
|
||||
});
|
||||
|
||||
it('redirects to choose-tenant after selecting a workspace with multiple tenants', function (): void {
|
||||
$user = User::factory()->create();
|
||||
|
||||
$workspace = Workspace::factory()->create();
|
||||
WorkspaceMembership::factory()->create([
|
||||
'workspace_id' => $workspace->getKey(),
|
||||
'user_id' => $user->getKey(),
|
||||
'role' => 'owner',
|
||||
]);
|
||||
|
||||
$tenants = Tenant::factory()->count(2)->create([
|
||||
'status' => 'active',
|
||||
'workspace_id' => $workspace->getKey(),
|
||||
]);
|
||||
|
||||
foreach ($tenants as $tenant) {
|
||||
TenantMembership::query()->create([
|
||||
'tenant_id' => $tenant->getKey(),
|
||||
'user_id' => $user->getKey(),
|
||||
'role' => 'owner',
|
||||
'source' => 'manual',
|
||||
'source_ref' => null,
|
||||
'created_by_user_id' => null,
|
||||
]);
|
||||
}
|
||||
|
||||
Livewire::actingAs($user)
|
||||
->test(ChooseWorkspace::class)
|
||||
->call('selectWorkspace', $workspace->getKey())
|
||||
|
||||
@ -33,6 +33,42 @@
|
||||
->assertRedirect("/admin/w/{$workspace->slug}/managed-tenants");
|
||||
});
|
||||
|
||||
it('keeps the managed-tenants landing tenantless even if the user has a tenant in another workspace', function (): void {
|
||||
$user = User::factory()->create();
|
||||
|
||||
$workspaceEmpty = Workspace::factory()->create(['slug' => 'empty']);
|
||||
$workspaceOther = Workspace::factory()->create(['slug' => 'other']);
|
||||
|
||||
WorkspaceMembership::factory()->create([
|
||||
'workspace_id' => $workspaceEmpty->getKey(),
|
||||
'user_id' => $user->getKey(),
|
||||
'role' => 'owner',
|
||||
]);
|
||||
|
||||
WorkspaceMembership::factory()->create([
|
||||
'workspace_id' => $workspaceOther->getKey(),
|
||||
'user_id' => $user->getKey(),
|
||||
'role' => 'owner',
|
||||
]);
|
||||
|
||||
$tenantInOther = Tenant::factory()->create([
|
||||
'status' => 'active',
|
||||
'workspace_id' => (int) $workspaceOther->getKey(),
|
||||
'external_id' => 'bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb',
|
||||
'tenant_id' => 'bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb',
|
||||
]);
|
||||
|
||||
$user->tenants()->syncWithoutDetaching([
|
||||
$tenantInOther->getKey() => ['role' => 'owner'],
|
||||
]);
|
||||
|
||||
$this->actingAs($user)
|
||||
->withSession([WorkspaceContext::SESSION_KEY => (int) $workspaceEmpty->getKey()])
|
||||
->get('/admin/w/'.$workspaceEmpty->slug.'/managed-tenants')
|
||||
->assertSuccessful()
|
||||
->assertDontSee('/admin/t/'.$tenantInOther->external_id, false);
|
||||
});
|
||||
|
||||
it('returns 404 on tenant routes when workspace context is missing', function (): void {
|
||||
$user = User::factory()->create();
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user