TenantAtlas/routes/web.php
ahmido 6f8eb28ca2 feat: add tenant backup health signals (#212)
## Summary
- add the Spec 180 tenant backup-health resolver and value objects to derive absent, stale, degraded, healthy, and schedule-follow-up posture from existing backup and schedule truth
- surface backup posture and reason-driven drillthroughs in the tenant dashboard and preserve continuity on backup-set and backup-schedule destinations
- add deterministic local/testing browser-fixture seeding plus a local fixture-login helper for the blocked drillthrough `403` scenario, along with the related spec artifacts and focused regression coverage

## Testing
- `vendor/bin/sail artisan test --compact tests/Feature/Auth/BackupHealthBrowserFixtureLoginTest.php tests/Feature/Console/TenantpilotSeedBackupHealthBrowserFixtureCommandTest.php`
- `vendor/bin/sail artisan test --compact tests/Unit/Support/BackupHealth/TenantBackupHealthResolverTest.php tests/Feature/Filament/DashboardKpisWidgetTest.php tests/Feature/Filament/NeedsAttentionWidgetTest.php tests/Feature/Filament/TenantDashboardTruthAlignmentTest.php tests/Feature/Filament/TenantDashboardTenantScopeTest.php tests/Feature/Filament/TenantDashboardDbOnlyTest.php tests/Feature/Filament/BackupSetListContinuityTest.php tests/Feature/Filament/BackupSetEnterpriseDetailPageTest.php tests/Feature/BackupScheduling/BackupScheduleLifecycleTest.php tests/Feature/Auth/BackupHealthBrowserFixtureLoginTest.php tests/Feature/Console/TenantpilotSeedBackupHealthBrowserFixtureCommandTest.php`

## Notes
- Filament v5 / Livewire v4 compliant; no panel-provider change was needed, so `bootstrap/providers.php` remains unchanged
- no new globally searchable resource was introduced, so global-search behavior is unchanged
- no new destructive action was added; existing destructive actions and confirmation behavior remain unchanged
- no new asset registration was added; the existing deploy-time `php artisan filament:assets` step remains sufficient
- the local fixture login helper route is limited to `local` and `testing` environments
- the focused and broader Spec 180 packs are green; the full suite was not rerun after these changes

Co-authored-by: Ahmed Darrazi <ahmed.darrazi@live.de>
Reviewed-on: #212
2026-04-07 21:35:58 +00:00

307 lines
11 KiB
PHP

<?php
use App\Filament\Pages\WorkspaceOverview;
use App\Http\Controllers\AdminConsentCallbackController;
use App\Http\Controllers\Auth\EntraController;
use App\Http\Controllers\ClearTenantContextController;
use App\Http\Controllers\OpenFindingExceptionsQueueController;
use App\Http\Controllers\RbacDelegatedAuthController;
use App\Http\Controllers\ReviewPackDownloadController;
use App\Http\Controllers\SelectTenantController;
use App\Http\Controllers\SwitchWorkspaceController;
use App\Http\Controllers\TenantOnboardingController;
use App\Models\ProviderConnection;
use App\Models\Tenant;
use App\Models\TenantOnboardingSession;
use App\Models\User;
use App\Models\Workspace;
use App\Services\Onboarding\OnboardingDraftResolver;
use App\Services\Tenants\TenantOperabilityService;
use App\Support\Tenants\TenantOperabilityQuestion;
use App\Support\Tenants\TenantPageCategory;
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\Auth;
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');
// Avoid Filament's tenancy root redirect which otherwise sends users into legacy flows.
// when no default tenant can be resolved.
Route::middleware([
'web',
'panel:admin',
'ensure-correct-guard:web',
DisableBladeIconComponents::class,
DispatchServingFilamentEvent::class,
FilamentAuthenticate::class,
'ensure-workspace-selected',
])
->get('/admin', WorkspaceOverview::class)
->name('admin.home');
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::get('/admin/local/backup-health-browser-fixture-login', function (Request $request) {
abort_unless(app()->environment(['local', 'testing']), 404);
$fixture = config('tenantpilot.backup_health.browser_smoke_fixture');
$userEmail = is_array($fixture) ? data_get($fixture, 'user.email') : null;
$tenantRouteKey = is_array($fixture)
? (data_get($fixture, 'blocked_drillthrough.tenant_id') ?? data_get($fixture, 'blocked_drillthrough.tenant_external_id'))
: null;
abort_unless(is_string($userEmail) && $userEmail !== '', 404);
abort_unless(is_string($tenantRouteKey) && $tenantRouteKey !== '', 404);
$user = User::query()->where('email', $userEmail)->firstOrFail();
$tenant = Tenant::query()->where('external_id', $tenantRouteKey)->firstOrFail();
$workspace = Workspace::query()->whereKey($tenant->workspace_id)->firstOrFail();
Auth::login($user);
$request->session()->regenerate();
$workspaceContext = app(WorkspaceContext::class);
$workspaceContext->setCurrentWorkspace($workspace, $user, $request);
$workspaceContext->rememberTenantContext($tenant, $request);
return redirect()->to('/admin/t/'.$tenant->external_id);
})->name('admin.local.backup-health-browser-fixture-login');
Route::middleware(['web', 'auth', 'ensure-correct-guard:web'])
->post('/admin/switch-workspace', SwitchWorkspaceController::class)
->name('admin.switch-workspace');
Route::middleware(['web', 'auth', 'ensure-correct-guard:web'])
->get('/admin/finding-exceptions/open-queue/{tenant}', OpenFindingExceptionsQueueController::class)
->name('admin.finding-exceptions.open-queue');
Route::middleware(['web', 'auth', 'ensure-correct-guard:web', 'ensure-workspace-selected'])
->post('/admin/select-tenant', SelectTenantController::class)
->name('admin.select-tenant');
Route::middleware(['web', 'auth', 'ensure-correct-guard:web', 'ensure-workspace-selected'])
->post('/admin/clear-tenant-context', ClearTenantContextController::class)
->name('admin.clear-tenant-context');
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::bind('onboardingDraft', function (string $value): TenantOnboardingSession {
$user = auth()->user();
abort_unless($user instanceof \App\Models\User, 403);
$workspaceId = app(WorkspaceContext::class)->currentWorkspaceId(request());
abort_unless(is_int($workspaceId), 404);
$workspace = Workspace::query()->whereKey($workspaceId)->first();
abort_unless($workspace instanceof Workspace, 404);
return app(OnboardingDraftResolver::class)->resolve((int) $value, $user, $workspace);
});
$authorizeManagedTenantRoute = function (Tenant $tenant, Request $request): void {
$user = $request->user();
abort_unless($user instanceof User, 403);
$workspaceContext = app(WorkspaceContext::class);
$workspaceId = $workspaceContext->currentWorkspaceId($request);
abort_unless(is_int($workspaceId), 404);
abort_unless((int) $tenant->workspace_id === $workspaceId, 404);
$workspace = Workspace::query()->whereKey($workspaceId)->first();
abort_unless($workspace instanceof Workspace, 404);
abort_unless($workspaceContext->isMember($user, $workspace), 404);
$allowed = app(TenantOperabilityService::class)->outcomeFor(
tenant: $tenant,
question: TenantOperabilityQuestion::TenantBoundViewability,
actor: $user,
workspaceId: $workspaceId,
lane: TenantPageCategory::TenantBound->lane(),
)->allowed;
abort_unless($allowed, 404);
};
Route::middleware(['web', 'auth', 'ensure-correct-guard:web', 'ensure-workspace-member'])
->prefix('/admin/w/{workspace}')
->group(function (): void {
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::middleware([
'web',
'panel:admin',
'ensure-correct-guard:web',
DisableBladeIconComponents::class,
DispatchServingFilamentEvent::class,
FilamentAuthenticate::class,
])
->get('/admin/onboarding', \App\Filament\Pages\Workspaces\ManagedTenantOnboardingWizard::class)
->name('admin.onboarding');
Route::middleware([
'web',
'panel:admin',
'ensure-correct-guard:web',
DisableBladeIconComponents::class,
DispatchServingFilamentEvent::class,
FilamentAuthenticate::class,
])
->get('/admin/onboarding/{onboardingDraft}', \App\Filament\Pages\Workspaces\ManagedTenantOnboardingWizard::class)
->name('admin.onboarding.draft');
Route::middleware([
'web',
'panel:admin',
'ensure-correct-guard:web',
DisableBladeIconComponents::class,
DispatchServingFilamentEvent::class,
FilamentAuthenticate::class,
'ensure-workspace-selected',
])
->get('/admin/operations', \App\Filament\Pages\Monitoring\Operations::class)
->name('admin.operations.index');
Route::middleware([
'web',
'panel:admin',
'ensure-correct-guard:web',
DisableBladeIconComponents::class,
DispatchServingFilamentEvent::class,
FilamentAuthenticate::class,
'ensure-workspace-selected',
])
->get('/admin/evidence/overview', \App\Filament\Pages\Monitoring\EvidenceOverview::class)
->name('admin.evidence.overview');
Route::middleware([
'web',
'panel:admin',
'ensure-correct-guard:web',
DisableBladeIconComponents::class,
DispatchServingFilamentEvent::class,
FilamentAuthenticate::class,
'ensure-workspace-selected',
])
->prefix('/admin/tenants/{tenant:external_id}/provider-connections')
->group(function () use ($authorizeManagedTenantRoute): void {
Route::get('/', function (Tenant $tenant, Request $request) use ($authorizeManagedTenantRoute) {
$authorizeManagedTenantRoute($tenant, $request);
return redirect()->to('/admin/provider-connections?tenant_id='.$tenant->external_id);
})->name('admin.provider-connections.legacy-index');
Route::get('/create', function (Tenant $tenant, Request $request) use ($authorizeManagedTenantRoute) {
$authorizeManagedTenantRoute($tenant, $request);
return redirect()->to('/admin/provider-connections/create?tenant_id='.$tenant->external_id);
})->name('admin.provider-connections.legacy-create');
Route::get('/{record}/edit', function (Tenant $tenant, mixed $record, Request $request) use ($authorizeManagedTenantRoute) {
$authorizeManagedTenantRoute($tenant, $request);
$connection = ProviderConnection::query()
->whereKey((int) $record)
->where('tenant_id', (int) $tenant->getKey())
->where('workspace_id', (int) $tenant->workspace_id)
->first();
abort_unless($connection instanceof ProviderConnection, 404);
return redirect()->to('/admin/provider-connections/'.$connection->getKey().'/edit?tenant_id='.$tenant->external_id);
})->name('admin.provider-connections.legacy-edit');
});
Route::middleware([
'web',
'panel:admin',
'ensure-correct-guard:web',
DisableBladeIconComponents::class,
DispatchServingFilamentEvent::class,
FilamentAuthenticate::class,
'ensure-workspace-selected',
'ensure-filament-tenant-selected',
])
->get('/admin/audit-log', \App\Filament\Pages\Monitoring\AuditLog::class)
->name('admin.monitoring.audit-log');
Route::middleware([
'web',
'panel:admin',
'ensure-correct-guard:web',
DisableBladeIconComponents::class,
DispatchServingFilamentEvent::class,
FilamentAuthenticate::class,
'ensure-workspace-selected',
])
->get('/admin/operations/{run}', \App\Filament\Pages\Operations\TenantlessOperationRunViewer::class)
->name('admin.operations.view');
Route::middleware([
'web',
'panel:admin',
'ensure-correct-guard:web',
DisableBladeIconComponents::class,
DispatchServingFilamentEvent::class,
FilamentAuthenticate::class,
'ensure-workspace-member',
])
->get('/admin/w/{workspace}/managed-tenants', \App\Filament\Pages\Workspaces\ManagedTenantsLanding::class)
->name('admin.workspace.managed-tenants.index');
Route::middleware(['signed'])
->get('/admin/review-packs/{reviewPack}/download', ReviewPackDownloadController::class)
->name('admin.review-packs.download');
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,
]);
});
}