## Summary - Removes the legacy Tenant CRUD create page (`/admin/tenants/create`) so tenant creation is handled exclusively via the onboarding wizard. - Updates tenant selection flows and pages to prevent Livewire polling/notification-related 404s on workspace-scoped routes. - Aligns empty-state UX with enterprise patterns (avoid duplicate CTAs). ## Key changes - Tenant creation - Removed `CreateTenant` page + route from `TenantResource`. - `TenantResource::canCreate()` now returns `false` (CRUD creation disabled). - Tenants list now surfaces an **Add tenant** action that links to onboarding (`admin.onboarding`). - Onboarding wizard - Removed redundant legacy step-cards from the blade view (Wizard schema is the source of truth). - Disabled topbar on the onboarding page to avoid lazy-loaded notifications. - Choose tenant - Enterprise UI redesign + workspace context. - Uses Livewire `selectTenant()` instead of a form POST. - Disabled topbar and gated BODY_END hook to avoid background polling. - Baseline profiles - Hide header create action when table is empty to avoid duplicate CTAs. ## Tests - `vendor/bin/sail artisan test --compact --filter='Onboarding|ManagedTenantOnboarding'` - `vendor/bin/sail artisan test --compact --filter='ManagedTenantsLivewireUpdate'` - `vendor/bin/sail artisan test --compact --filter='TenantSetup|TenantResourceAuth|TenantAdminAuth|ListTenants'` - `vendor/bin/sail artisan test --compact --filter='BaselineProfile'` - `vendor/bin/sail artisan test --compact --filter='ChooseTenant|TenantMake|TenantScoping|AdminTenantScoped|AdminHomeRedirect|WorkspaceContext'` ## Notes - Filament v5 / Livewire v4 compatible. - No new assets introduced; no deploy pipeline changes required. Co-authored-by: Ahmed Darrazi <ahmed.darrazi@live.de> Reviewed-on: #131
127 lines
4.0 KiB
Markdown
127 lines
4.0 KiB
Markdown
# Routes Contract: Workspace Chooser v1
|
|
|
|
**Feature**: 107-workspace-chooser | **Date**: 2026-02-22
|
|
|
|
## Routes (existing, behavior changes)
|
|
|
|
### `GET /admin` (named: `admin.home`)
|
|
|
|
**Change**: After workspace auto-resume, redirect uses the shared `WorkspaceRedirectResolver` instead of inline branching.
|
|
|
|
**Middleware**: `web`, `panel:admin`, `ensure-correct-guard:web`, `FilamentAuthenticate`, `ensure-workspace-selected`
|
|
|
|
**Behavior (updated)**:
|
|
1. `ensure-workspace-selected` middleware handles auto-resume (may set workspace + redirect before this handler runs).
|
|
2. If workspace is resolved, apply tenant-count branching.
|
|
3. If no workspace, redirect to `/admin/choose-workspace`.
|
|
|
|
---
|
|
|
|
### `GET /admin/choose-workspace` (Filament Page: `ChooseWorkspace`)
|
|
|
|
**Change**: Page now displays metadata (role, tenant count), cleaner empty state, "Manage workspaces" link instead of "Create workspace" header action.
|
|
|
|
**Middleware**: Standard admin panel middleware. `ensure-workspace-selected` allows this path (exempted in `isWorkspaceOptionalPath()`).
|
|
|
|
**Query params**:
|
|
- `?choose=1` — forces chooser display (bypasses auto-resume). The middleware redirects here when this param is present.
|
|
|
|
**Response**: Filament page with workspace cards.
|
|
|
|
**Livewire actions**:
|
|
- `selectWorkspace(int $workspaceId)` — validates membership, sets workspace context, emits audit event, redirects via tenant-count branching.
|
|
|
|
---
|
|
|
|
### `POST /admin/switch-workspace` (named: `admin.switch-workspace`)
|
|
|
|
**Change**: Redirect logic replaced with `WorkspaceRedirectResolver`. Audit logging added via `WorkspaceAuditLogger::log()` — emits `workspace.selected` with reason `context_bar` to satisfy FR-005 (every workspace selection must be audited).
|
|
|
|
**Controller**: `SwitchWorkspaceController`
|
|
|
|
**Request body**: `workspace_id` (required, integer)
|
|
|
|
**Middleware**: `web`, `auth`, `ensure-correct-guard:web`
|
|
|
|
---
|
|
|
|
## Middleware Contract: `ensure-workspace-selected`
|
|
|
|
### Algorithm (v1 — 7-step)
|
|
|
|
```
|
|
Step 1: If path is workspace-optional → ALLOW (no redirect)
|
|
Step 2: If query has `choose=1` → REDIRECT to /admin/choose-workspace?choose=1
|
|
Step 3: If session.current_workspace_id is set:
|
|
- If membership valid + not archived → ALLOW
|
|
- Else: clear session + flash warning → REDIRECT to chooser
|
|
Step 4: Load user's selectable workspace memberships (not archived)
|
|
Step 5: If exactly 1 → auto-select, audit log (single_membership) → REDIRECT via tenant branching
|
|
Step 6: If last_workspace_id set:
|
|
- If valid membership + selectable → auto-select, audit log (last_used) → REDIRECT via tenant branching
|
|
- Else: clear last_workspace_id + flash warning → REDIRECT to chooser
|
|
Step 7: Else → REDIRECT to chooser
|
|
```
|
|
|
|
### Exempt Paths (workspace-optional)
|
|
|
|
- `/admin/workspaces*`
|
|
- `/admin/choose-workspace`
|
|
- `/admin/no-access`
|
|
- `/admin/onboarding`
|
|
- `/admin/settings/workspace`
|
|
- `/admin/operations/{id}` (existing exemption)
|
|
- `/admin/t/*` (tenant-scoped routes)
|
|
- Routes with `.auth.` in name
|
|
|
|
---
|
|
|
|
## User Menu Contract
|
|
|
|
### "Switch workspace" menu item
|
|
|
|
**Location**: Admin panel user menu (registered via `AdminPanelProvider::panel()` → `->userMenuItems()`)
|
|
|
|
**Visibility**: Only when current user has > 1 workspace membership.
|
|
|
|
**URL**: `/admin/choose-workspace?choose=1`
|
|
|
|
**Icon**: `heroicon-o-arrows-right-left`
|
|
|
|
---
|
|
|
|
## Audit Event Contracts
|
|
|
|
### `workspace.auto_selected`
|
|
|
|
**Trigger**: Middleware auto-resume (steps 5 or 6).
|
|
|
|
**Payload** (in `audit_logs.metadata`):
|
|
|
|
```json
|
|
{
|
|
"method": "auto",
|
|
"reason": "single_membership" | "last_used",
|
|
"prev_workspace_id": null
|
|
}
|
|
```
|
|
|
|
### `workspace.selected`
|
|
|
|
**Trigger**: Manual selection from chooser (via `selectWorkspace()`).
|
|
|
|
**Payload** (in `audit_logs.metadata`):
|
|
|
|
```json
|
|
{
|
|
"method": "manual",
|
|
"reason": "chooser",
|
|
"prev_workspace_id": 42
|
|
}
|
|
```
|
|
|
|
Both events use `WorkspaceAuditLogger::log()` with:
|
|
- `action`: `AuditActionId::WorkspaceAutoSelected->value` or `AuditActionId::WorkspaceSelected->value`
|
|
- `resource_type`: `'workspace'`
|
|
- `resource_id`: `(string) $workspace->getKey()`
|