Skip to content

feat: organization dashboard (multi-tenant) — issue #23#82

Open
rathorevaibhav wants to merge 22 commits into
mainfrom
feat/issue-23-organization-dashboard
Open

feat: organization dashboard (multi-tenant) — issue #23#82
rathorevaibhav wants to merge 22 commits into
mainfrom
feat/issue-23-organization-dashboard

Conversation

@rathorevaibhav

Copy link
Copy Markdown
Member

Closes #23.

Introduces a multi-tenant Organization boundary so each organization gets its own scoped dashboard, with admin/member roles and a platform super-admin tier — without changing the background uptime/cert/domain checks, which run in a console context with no HTTP session.

What's in here

  • Data model: organizations + organization_user membership pivot (per-membership admin/member role); users.is_super_admin; direct organization_id on monitors and groups (group stays an optional sub-grouping within an org).
  • Active org: held in the session, resolved per request by SetActiveOrganization middleware into a CurrentOrganization request singleton; an org switcher in the nav (desktop + mobile).
  • Isolation & authz: explicit forOrganization() scoping + org-scoped route-model binding (cross-org URL → 404) + policies/gates (Gate::before super-admin bypass; member = read-only). Three independent isolation barriers.
  • Onboarding: open /register disabled; super-admins onboard an org + its first admin; org-admins self-manage users (find-or-link by email, detach-not-delete) and can rename their org.
  • Migration: backfills existing monitors/groups/users into a default "ColoredCow" org (no-op on empty DBs, idempotent), then makes organization_id NOT NULL.

Key design decision — no global scope

Spatie's scheduled checks and our console commands enumerate monitors through MonitorRepository::query() with no session. A session-keyed global scope would silently filter every background check to zero. So no global Eloquent scope was added — web paths scope explicitly; console paths stay unscoped. This invariant is locked by a regression test asserting the repository's enumeration entry point still sees all orgs' monitors with no active org bound.

The active-org resolution is also middleware-ordering-independent: a single pure CurrentOrganization::resolveFor() is shared by the middleware, the route binding (which runs before the middleware), and the lazy Inertia auth prop (resolved after it).

Testing

  • PHP: 112 passing (506 assertions) — incl. tenant isolation (index + cross-org 404), role/authz 403s, IDOR negative cases for user management, the switcher, onboarding, the backfill NOT NULL enforcement, and the console-scope regression test.
  • JS (vitest): 68 passing; npm run build succeeds.
  • Built TDD task-by-task with a per-task review gate and a final whole-branch review (verdict: ready to merge with the three small fixes already applied).

Deferred (fast-follow, non-blocking)

  • Surface server-side validation errors in the Organizations/Users React forms (matches the existing app-wide gap).
  • Optional org-name uniqueness (slug is already unique).

Design spec and full implementation plan are committed under plans/.

🤖 Generated with Claude Code

rathorevaibhav and others added 22 commits June 25, 2026 23:19
Design for multi-tenant organizations: a session-scoped active org with a
membership pivot (admin/member roles), platform super-admin tier, direct
organization_id on monitors and groups, and explicit forOrganization()
scoping + scoped route-model binding + policies (no global scope, so
console/background checks stay unscoped). Includes the existing-data
backfill into a default org and a TDD test plan.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
12 TDD tasks covering schema, membership/roles, the CurrentOrganization
service, explicit forOrganization() scoping, scoped route-model binding,
policies, the org switcher, registration disable, super-admin onboarding,
org-scoped user management, and the default-org backfill.

Incorporates an adversarial review pass: the design is now middleware-order
independent (binding self-resolves via resolveFor + 404; lazy Inertia auth
prop; policy ownership re-checks), and the MonitorHistory test migration is
sequenced so every commit stays green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…mport

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…tests

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Organization dashboard

1 participant