TenantAtlas/apps/platform/app/Support/OperateHub/OperateHubShell.php
Ahmed Darrazi b515796839
Some checks failed
PR Fast Feedback / fast-feedback (pull_request) Failing after 53s
feat: finalize global shell context contract
2026-04-18 15:59:02 +02:00

427 lines
14 KiB
PHP

<?php
declare(strict_types=1);
namespace App\Support\OperateHub;
use App\Filament\Pages\TenantDashboard;
use App\Models\Tenant;
use App\Models\User;
use App\Models\Workspace;
use App\Services\Auth\CapabilityResolver;
use App\Services\Tenants\TenantOperabilityService;
use App\Support\Tenants\TenantOperabilityQuestion;
use App\Support\Tenants\TenantPageCategory;
use App\Support\Workspaces\WorkspaceContext;
use Filament\Actions\Action;
use Filament\Facades\Filament;
use Illuminate\Http\Request;
final class OperateHubShell
{
private const string REQUEST_ATTRIBUTE = 'tenantpilot.resolved_shell_context';
public function __construct(
private WorkspaceContext $workspaceContext,
private CapabilityResolver $capabilityResolver,
private TenantOperabilityService $tenantOperabilityService,
) {}
public function scopeLabel(?Request $request = null): string
{
$activeTenant = $this->activeEntitledTenant($request);
if ($activeTenant instanceof Tenant) {
return 'Tenant scope: '.$activeTenant->name;
}
return 'All tenants';
}
/**
* @return array{label: string, url: string}|null
*/
public function returnAffordance(?Request $request = null): ?array
{
$activeTenant = $this->activeEntitledTenant($request);
if ($activeTenant instanceof Tenant) {
return [
'label' => 'Back to '.$activeTenant->name,
'url' => TenantDashboard::getUrl(panel: 'tenant', tenant: $activeTenant),
];
}
return null;
}
/**
* @return array<Action>
*/
public function headerActions(
string $scopeActionName = 'operate_hub_scope',
string $returnActionName = 'operate_hub_return',
?Request $request = null,
): array {
$actions = [
Action::make($scopeActionName)
->label($this->scopeLabel($request))
->color('gray')
->disabled(),
];
$returnAffordance = $this->returnAffordance($request);
if (is_array($returnAffordance)) {
$actions[] = Action::make($returnActionName)
->label($returnAffordance['label'])
->icon('heroicon-o-arrow-left')
->color('gray')
->url($returnAffordance['url']);
}
return $actions;
}
public function activeEntitledTenant(?Request $request = null): ?Tenant
{
return $this->resolvedContext($request)->tenant;
}
public function tenantOwnedPanelContext(?Request $request = null): ?Tenant
{
return $this->activeEntitledTenant($request);
}
public function resolvedContext(?Request $request = null): ResolvedShellContext
{
$request ??= request();
if ($request instanceof Request) {
$cached = $request->attributes->get(self::REQUEST_ATTRIBUTE);
if ($cached instanceof ResolvedShellContext) {
return $cached;
}
}
$resolved = $this->buildResolvedContext($request);
if ($request instanceof Request) {
$request->attributes->set(self::REQUEST_ATTRIBUTE, $resolved);
}
return $resolved;
}
private function buildResolvedContext(?Request $request = null): ResolvedShellContext
{
$pageCategory = $this->pageCategory($request);
$routeTenantCandidate = $this->resolveRouteTenantCandidate($request, $pageCategory);
$sessionWorkspace = $this->workspaceContext->currentWorkspace($request);
$workspace = $this->workspaceContext->currentWorkspaceOrTenantWorkspace($routeTenantCandidate, $request);
$workspaceSource = match (true) {
$workspace instanceof Workspace && $sessionWorkspace instanceof Workspace && $workspace->is($sessionWorkspace) => 'session_workspace',
$workspace instanceof Workspace && $routeTenantCandidate instanceof Tenant && (int) $routeTenantCandidate->workspace_id === (int) $workspace->getKey() => 'route',
default => 'none',
};
if (! $workspace instanceof Workspace) {
return new ResolvedShellContext(
workspace: null,
tenant: null,
pageCategory: $pageCategory,
state: 'missing_workspace',
displayMode: 'recovery',
recoveryAction: $pageCategory === TenantPageCategory::WorkspaceChooserException ? 'none' : 'redirect_choose_workspace',
recoveryDestination: '/admin/choose-workspace',
recoveryReason: 'missing_workspace',
);
}
$routeTenant = $this->resolveValidatedRouteTenant($routeTenantCandidate, $workspace, $request, $pageCategory);
if ($routeTenant['tenant'] instanceof Tenant) {
return new ResolvedShellContext(
workspace: $workspace,
tenant: $routeTenant['tenant'],
pageCategory: $pageCategory,
state: 'tenant_scoped',
displayMode: 'tenant_scoped',
workspaceSource: $workspaceSource,
tenantSource: 'route',
);
}
$recoveryReason = $routeTenant['reason'];
if ($pageCategory === TenantPageCategory::TenantBound && $recoveryReason !== null) {
return new ResolvedShellContext(
workspace: $workspace,
tenant: null,
pageCategory: $pageCategory,
state: 'invalid_tenant',
displayMode: 'recovery',
workspaceSource: $workspaceSource,
recoveryAction: 'abort_not_found',
recoveryReason: $recoveryReason,
);
}
$queryHintTenant = $this->resolveValidatedQueryHintTenant($request, $workspace, $pageCategory);
if ($queryHintTenant['tenant'] instanceof Tenant) {
return new ResolvedShellContext(
workspace: $workspace,
tenant: $queryHintTenant['tenant'],
pageCategory: $pageCategory,
state: 'tenant_scoped',
displayMode: 'tenant_scoped',
workspaceSource: $workspaceSource,
tenantSource: 'query_hint',
);
}
$recoveryReason ??= $queryHintTenant['reason'];
$tenant = $this->resolveValidatedFilamentTenant($request, $pageCategory, $workspace);
if ($tenant instanceof Tenant) {
return new ResolvedShellContext(
workspace: $workspace,
tenant: $tenant,
pageCategory: $pageCategory,
state: 'tenant_scoped',
displayMode: 'tenant_scoped',
workspaceSource: $workspaceSource,
tenantSource: 'filament_tenant',
);
}
if ($pageCategory->allowsRememberedTenantRestore()) {
$rememberedTenant = $this->workspaceContext->rememberedTenant($request);
if ($rememberedTenant instanceof Tenant) {
return new ResolvedShellContext(
workspace: $workspace,
tenant: $rememberedTenant,
pageCategory: $pageCategory,
state: 'tenant_scoped',
displayMode: 'tenant_scoped',
workspaceSource: $workspaceSource,
tenantSource: 'remembered',
);
}
}
if ($pageCategory->requiresExplicitTenant()) {
return new ResolvedShellContext(
workspace: $workspace,
tenant: null,
pageCategory: $pageCategory,
state: 'missing_tenant',
displayMode: 'recovery',
workspaceSource: $workspaceSource,
recoveryAction: $pageCategory === TenantPageCategory::TenantScopedEvidence
? 'redirect_evidence_overview'
: 'abort_not_found',
recoveryDestination: $pageCategory === TenantPageCategory::TenantScopedEvidence
? '/admin/evidence/overview'
: null,
recoveryReason: $recoveryReason ?? 'missing_tenant',
);
}
return new ResolvedShellContext(
workspace: $workspace,
tenant: null,
pageCategory: $pageCategory,
state: 'tenantless_workspace',
displayMode: 'tenantless',
workspaceSource: $workspaceSource,
recoveryReason: $recoveryReason,
);
}
private function resolveValidatedFilamentTenant(
?Request $request = null,
?TenantPageCategory $pageCategory = null,
?Workspace $workspace = null,
): ?Tenant {
$tenant = Filament::getTenant();
if (! $tenant instanceof Tenant) {
return null;
}
$pageCategory ??= $this->pageCategory($request);
$workspace ??= $this->workspaceContext->currentWorkspaceOrTenantWorkspace($tenant, $request);
if ($workspace instanceof Workspace && $this->tenantValidationReason($tenant, $workspace, $request, $pageCategory) === null) {
return $tenant;
}
Filament::setTenant(null, true);
return null;
}
private function resolveValidatedRouteTenant(
?Tenant $tenant,
Workspace $workspace,
?Request $request = null,
?TenantPageCategory $pageCategory = null,
): array {
$pageCategory ??= $this->pageCategory($request);
if (! $tenant instanceof Tenant) {
return ['tenant' => null, 'reason' => null];
}
$reason = $this->tenantValidationReason($tenant, $workspace, $request, $pageCategory);
if ($reason !== null) {
return ['tenant' => null, 'reason' => $reason];
}
return ['tenant' => $tenant, 'reason' => null];
}
private function resolveValidatedQueryHintTenant(
?Request $request,
Workspace $workspace,
TenantPageCategory $pageCategory,
): array {
if (! $pageCategory->allowsQueryTenantHints()) {
return ['tenant' => null, 'reason' => null];
}
$queryTenant = $this->resolveQueryTenantHint($request);
if (! $queryTenant instanceof Tenant) {
return ['tenant' => null, 'reason' => null];
}
$reason = $this->tenantValidationReason($queryTenant, $workspace, $request, $pageCategory);
if ($reason !== null) {
return ['tenant' => null, 'reason' => $reason];
}
return ['tenant' => $queryTenant, 'reason' => null];
}
private function resolveRouteTenantCandidate(?Request $request = null, ?TenantPageCategory $pageCategory = null): ?Tenant
{
$route = $request?->route();
$pageCategory ??= $this->pageCategory($request);
if ($route?->hasParameter('tenant')) {
return $this->resolveTenantIdentifier($route->parameter('tenant'));
}
if (
$pageCategory !== TenantPageCategory::TenantBound
|| ! $route?->hasParameter('record')
|| ! str_starts_with((string) ($route->getName() ?? ''), 'filament.admin.resources.tenants.')
) {
return null;
}
return $this->resolveTenantIdentifier($route->parameter('record'));
}
private function resolveQueryTenantHint(?Request $request = null): ?Tenant
{
$queryTenant = $request?->query('tenant');
if (filled($queryTenant)) {
return $this->resolveTenantIdentifier($queryTenant);
}
$queryTenantId = $request?->query('tenant_id');
if (filled($queryTenantId)) {
return $this->resolveTenantIdentifier($queryTenantId);
}
return null;
}
private function resolveTenantIdentifier(mixed $tenantIdentifier): ?Tenant
{
if ($tenantIdentifier instanceof Tenant) {
return $tenantIdentifier;
}
$tenantIdentifier = trim((string) $tenantIdentifier);
if ($tenantIdentifier === '') {
return null;
}
$tenantKeyColumn = (new Tenant)->getQualifiedKeyName();
return Tenant::query()
->withTrashed()
->where(static function ($query) use ($tenantIdentifier, $tenantKeyColumn): void {
$query->where('external_id', $tenantIdentifier);
if (ctype_digit($tenantIdentifier)) {
$query->orWhere($tenantKeyColumn, (int) $tenantIdentifier);
}
})
->first();
}
private function tenantValidationReason(
Tenant $tenant,
Workspace $workspace,
?Request $request = null,
?TenantPageCategory $pageCategory = null,
): ?string {
$pageCategory ??= $this->pageCategory($request);
if ((int) $tenant->workspace_id !== (int) $workspace->getKey()) {
return 'mismatched_workspace';
}
$user = auth()->user();
if (! $user instanceof User) {
return 'not_member';
}
if (! $this->capabilityResolver->isMember($user, $tenant)) {
return 'not_member';
}
$question = $pageCategory === TenantPageCategory::TenantBound
? TenantOperabilityQuestion::TenantBoundViewability
: TenantOperabilityQuestion::AdministrativeDiscoverability;
$allowed = $this->tenantOperabilityService->outcomeFor(
tenant: $tenant,
question: $question,
actor: $user,
workspaceId: (int) $workspace->getKey(),
lane: $pageCategory->lane(),
selectedTenant: Filament::getTenant() instanceof Tenant ? Filament::getTenant() : null,
)->allowed;
if ($allowed) {
return null;
}
return $pageCategory === TenantPageCategory::TenantBound
? 'inaccessible'
: 'not_operable';
}
private function pageCategory(?Request $request = null): TenantPageCategory
{
return TenantPageCategory::fromRequest($request);
}
}