# 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()`