Implements workspace-first enforcement and UX: - Workspace selected before tenant flows; /admin routes into choose-workspace/choose-tenant - Tenant lists and default tenant selection are scoped to current workspace - Workspaces UI is tenantless at /admin/workspaces Security hardening: - Workspaces can never have 0 owners (blocks last-owner removal/demotion) - Blocked attempts are audited with action_id=workspace_membership.last_owner_blocked + required metadata - Optional break-glass recovery page to re-assign workspace owner (audited) Tests: - Added/updated Pest feature tests covering redirects, scoping, tenantless workspaces, last-owner guards, and break-glass recovery. Notes: - Filament v5 strict Page property signatures respected in RepairWorkspaceOwners. Co-authored-by: Ahmed Darrazi <ahmeddarrazi@MacBookPro.fritz.box> Reviewed-on: #86
73 lines
1.8 KiB
PHP
73 lines
1.8 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace App\Http\Controllers;
|
|
|
|
use App\Filament\Pages\TenantDashboard;
|
|
use App\Models\Tenant;
|
|
use App\Models\User;
|
|
use App\Models\UserTenantPreference;
|
|
use App\Support\Workspaces\WorkspaceContext;
|
|
use Illuminate\Http\RedirectResponse;
|
|
use Illuminate\Http\Request;
|
|
use Illuminate\Support\Facades\Schema;
|
|
|
|
final class SelectTenantController
|
|
{
|
|
public function __invoke(Request $request): RedirectResponse
|
|
{
|
|
$user = auth()->user();
|
|
|
|
if (! $user instanceof User) {
|
|
abort(403);
|
|
}
|
|
|
|
$workspaceId = app(WorkspaceContext::class)->currentWorkspaceId($request);
|
|
|
|
if ($workspaceId === null) {
|
|
return redirect()->to('/admin/choose-workspace');
|
|
}
|
|
|
|
$validated = $request->validate([
|
|
'tenant_id' => ['required', 'integer'],
|
|
]);
|
|
|
|
$tenant = Tenant::query()
|
|
->where('status', 'active')
|
|
->where('workspace_id', $workspaceId)
|
|
->whereKey($validated['tenant_id'])
|
|
->first();
|
|
|
|
if (! $tenant instanceof Tenant) {
|
|
abort(404);
|
|
}
|
|
|
|
if (! $user->canAccessTenant($tenant)) {
|
|
abort(404);
|
|
}
|
|
|
|
$this->persistLastTenant($user, $tenant);
|
|
|
|
return redirect()->to(TenantDashboard::getUrl(tenant: $tenant));
|
|
}
|
|
|
|
private function persistLastTenant(User $user, Tenant $tenant): void
|
|
{
|
|
if (Schema::hasColumn('users', 'last_tenant_id')) {
|
|
$user->forceFill(['last_tenant_id' => $tenant->getKey()])->save();
|
|
|
|
return;
|
|
}
|
|
|
|
if (! Schema::hasTable('user_tenant_preferences')) {
|
|
return;
|
|
}
|
|
|
|
UserTenantPreference::query()->updateOrCreate(
|
|
['user_id' => $user->getKey(), 'tenant_id' => $tenant->getKey()],
|
|
['last_used_at' => now()]
|
|
);
|
|
}
|
|
}
|