TenantAtlas/specs/107-workspace-chooser/contracts/routes.md
Ahmed Darrazi d67e2c84bc fix: resolve 5 consistency issues from project analysis (F1–F7)
F1: Replace 'EnsureActiveWorkspace' with 'EnsureWorkspaceSelected' in spec (3 occurrences) — aligns with R1 decision
F2: Add audit logging to SwitchWorkspaceController (T036) + context_bar reason — closes FR-005 gap
F3: Remove stale WorkspaceContext.php MODIFY from plan project structure
F4/F5: Add WorkspaceRedirectResolver.php + 2 missing test files to plan project structure
F6: Add single-workspace sub-case (EC3) note to T030
F7: Add archived workspace scenario (EC2) note to T024
2026-02-22 15:18:26 +01:00

4.0 KiB

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):

{
  "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):

{
  "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()