Skip to content

feat(user-mgmt): user lifecycle (invite + deactivate) + activity, in Settings (#995)#996

Merged
vybe merged 8 commits into
devfrom
feature/995-user-management-ui
Jun 2, 2026
Merged

feat(user-mgmt): user lifecycle (invite + deactivate) + activity, in Settings (#995)#996
vybe merged 8 commits into
devfrom
feature/995-user-management-ui

Conversation

@dolho

@dolho dolho commented Jun 1, 2026

Copy link
Copy Markdown
Contributor

Summary

Enterprise user lifecycle (#995) — admin invite (sends a real invite email) + deactivate/reactivate, plus a per-user activity view — integrated into the existing Settings → User Management table. Pivoted from the earlier Organizations approach (dropped — single-tenant). Backend logic lives in the private trinity-enterprise submodule (PR #2); this PR is the OSS seam.

OSS primitive (edition-agnostic)

  • users.suspended_at column + migration; surfaced in get_user/list_users + /api/users.
  • get_current_user rejects suspended users on both the JWT and MCP-key paths → setting the column blocks new logins and invalidates live tokens on the next request (immediate sign-out). Reversible.

Enterprise UI (gated by user_management, hidden in OSS)

  • Settings → User Management gains: Invite user form, per-row Deactivate/Reactivate (never for self or the built-in admin), and a per-user Activity drawer.
  • All call the private /api/enterprise/user-management/* endpoints.
  • Table fits the max-w-4xl card (trimmed padding + wrapping actions) — no clipping, no horizontal scrollbar.

Removed

  • The separate /enterprise/user-management Orgs page + route + store + Index card. Index card now links to Settings.

Behaviour notes

  • Deactivation = reversible auth lock, not delete: the user row + all owned agents/data stay; the person just can't authenticate until reactivated. (Agents/schedules they own keep running — out of scope here.)
  • Distinct from the email whitelist: whitelist is the login-time admission gate (who may log in); deactivation is per-request enforcement on an existing account (kills live sessions). Invite uses the whitelist to admit; deactivate locks an existing user.

Verification (live + unit)

  • Live on a running stack: /api/users exposes suspended_at; suspend writes it + reactivate clears; guards (self/admin → 400, unknown → 404); invite → 201 and the invite email fired (console-rendered: To/Subject/body, email_sent: true); dup → 409; revoke → 204; activity → 200.
  • Settings.vue compiles; OSS-only builds hide every enterprise control.

Submodule pointer not bumped here; advances after trinity-enterprise#2 merges. Related to #995, #847.

🤖 Generated with Claude Code

Public OSS-bundle frontend for the private user_management module
(Abilityai/trinity-enterprise#2). Gated entirely server-side by the
`user_management` entitlement — hidden in OSS-only builds and bounced by
the route guard on direct URL visits.

- views/enterprise/UserManagement.vue: org list + create, membership
  add/remove, seat counts. Light + dark. No algorithmic IP (CRUD glue
  over the private /api/enterprise/user-management/* endpoints).
- stores/orgManagement.js: domain store, calls via shared axios + auth
  header (Invariants #6/#7).
- router: /enterprise/user-management gated meta.requiresEntitlement:
  'user_management' (mirrors the audit route).
- views/enterprise/Index.vue: add the catalogue card (available).

No public backend/schema/model changes — the entire data model + logic
lives in the private submodule per the enterprise open-core split. The
submodule pointer is intentionally NOT bumped here; it advances after
trinity-enterprise#2 merges.

Verified: all four files compile; the only build blocker is the
pre-existing unrelated `mermaid` import in AgentWorkspace.vue (stale
local node_modules; resolved by CI npm ci).

Related to #995, #847

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@dolho dolho changed the title feat(enterprise-ui): User & Org Management view (#995, Phase 1a) wip: feat(enterprise-ui): User & Org Management view (#995, Phase 1a) Jun 1, 2026
dolho and others added 2 commits June 1, 2026 13:21
…ement (#995)

Integrates the enterprise activity view INTO the existing OSS user
management table (not a separate page). When user_management is
entitled, each user row gets a "View activity" action opening a drawer
with that user's audit summary + timeline, fetched from the private
/api/enterprise/user-management/users/{id}/activity endpoint.

- Gated entirely by enterpriseStore.isEntitled('user_management') —
  column + drawer hidden in OSS-only builds.
- No change to the existing role-CRUD behaviour; purely additive column.
- loadFeatureFlags() in onMounted (cached/no-op when NavBar already ran).

Related to #995
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…#995)

Pivots #995 from Organizations to the real net-new gap — user
onboarding/offboarding — integrated into the existing Settings → User
Management table (not a separate page).

OSS primitive (edition-agnostic, small):
- users.suspended_at column + migration + surfaced in get_user/list_users.
- get_current_user rejects suspended users (both JWT + MCP-key paths), so
  setting the column blocks new logins AND invalidates live tokens on the
  next request.
- /api/users exposes suspended_at (read-only) so the gated UI can render
  Deactivate/Reactivate.

Enterprise UI (gated by user_management entitlement, hidden in OSS):
- Settings → User Management gains an "Invite user" form, per-row
  Deactivate/Reactivate (not for self or the built-in admin), and the
  per-user Activity drawer. All call the private
  /api/enterprise/user-management/* endpoints.

Removed: the separate /enterprise/user-management Orgs page, its route,
store, and Index card (orgs dropped — single-tenant). Index card now
points at Settings.

No change to the existing OSS role-CRUD behaviour. Verified live:
/api/users carries suspended_at; suspend/reactivate/invite/activity all
work; OSS-only builds hide every enterprise control.

Related to #995, #847
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@dolho dolho changed the title wip: feat(enterprise-ui): User & Org Management view (#995, Phase 1a) feat(user-mgmt): user lifecycle (invite + deactivate) + activity, in Settings (#995) Jun 1, 2026
dolho and others added 4 commits June 1, 2026 14:00
…ble (#995)

The users table wrapper was overflow-hidden; the extra entitlement-gated
Management column pushed total width past the card and clipped the
right-side action buttons. Switch to overflow-x-auto so the wider table
scrolls within the card instead of clipping.

Related to #995
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…995)

Replace the overflow-x-auto stopgap with an actual fit: trim cell padding
px-6→px-4 across the table and let the Management actions wrap within
their column (flex-wrap, text-xs, no whitespace-nowrap). The 5-column
table now fits the max-w-4xl settings card without clipping or a
horizontal scrollbar.

Related to #995
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The #995 users.suspended_at primitive added the column to _USER_COLUMNS,
so get_user_by_*() now SELECTs it. test_schedule_soft_delete builds its
own users table with a hardcoded DDL that lacked the column, causing
"no such column: suspended_at" (4 regression-diff failures). Mirror the
schema change in the test DDL.

Related to #995
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
#995/#997)

- users.suspended_at deactivation primitive (OSS column + enforcement;
  enterprise-only setter) on the users table + a callout.
- Invariant #3 extended: enterprise migrates enterprise_* tables via a
  separate runner tracked in enterprise_schema_migrations (one file per
  migration; never ALTERs OSS tables; runs after OSS init).
- feature-flags doc gains enterprise_features.
- New "Enterprise Modules (#847 seam)" section: audit / user_management /
  siem entitlements, surfaces, and the gating model.

Related to #995, #997, #847
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@dolho

dolho commented Jun 1, 2026

Copy link
Copy Markdown
Contributor Author

🔧 Make-it-functional notes & merge order

This PR is the OSS seam only (gated UI + the users.suspended_at primitive + enforcement + docs). The actual user-management endpoints live in trinity-enterprise#2.

Cross-PR dependency: the Settings UI calls /api/enterprise/user-management/*, which only exist once the private module is merged and the public submodule pointer is bumped. Until then the user_management entitlement never registers, so the UI stays hidden — this PR is safe to land alone (no 404s for users).

Deactivation needs both halves: this PR adds the OSS suspended_at column + the get_current_user reject (the enforcement); #2 adds the setter. Both must be deployed for "Deactivate" to actually block login.

Merge order:

  1. Merge trinity-enterprise#2 (→ its main).
  2. Bump this repo's submodule pointer (src/backend/enterprise) to that commit (separate public commit).
  3. Merge this PR (→ dev).

Deploy prereq: the dev/prod host must be able to fetch the private submodule (the .gitmodules SSH URL needs a deploy key/PAT) — and the dev deploy is currently stalled (VM off the tailnet), tracked separately.

Docs updated in docs/memory/architecture.md (enterprise modules + suspended_at primitive).

@dolho

dolho commented Jun 1, 2026

Copy link
Copy Markdown
Contributor Author

/review — pre-landing review

Scope: CLEAN. Committed contents are exactly the 9 intended files (verified via gh pr diff + git log origin/dev..HEAD). The submodule pointer is not bumped here — correct, matches the documented merge order.

Critical: none

  • Auth ✅ — OSS get_current_user rejects suspended users on both JWT and MCP-key paths; migration defaults suspended_at NULL so no existing session breaks on deploy; built-in admin/self can't be suspended (enforced in the enterprise service).
  • SQL ✅suspended_at added to _USER_COLUMNS, row mapper, list_users, and the migration uses _safe_add_column (idempotent, no data loss).
  • Frontend ✅ — no v-html; activity drawer renders via escaped {{ }}; all axios calls use the shared auth'd instance with catch blocks.

Informational

  • [I4] Settings.vue uses native alert() for suspend/reactivate errors — functional, crude UX.
  • [I5] Working-tree hazard (this branch): locally, src/backend/enterprise is checked out to the unmerged siem branch and VERSION/.claude are dirty. A future git add -A on this branch would sweep the pointer bump (to an unmerged commit) into the PR. Use explicit git add until the tree is reset.

Verdict: 0 critical — clear to merge in order (enterprise#2 → bump submodule pointer → this).

@dolho
dolho requested a review from vybe June 1, 2026 13:40
…boot (#995/#997)

main.py wrapped register_enterprise(app) in `except ImportError` only — so a
bug in enterprise registration (schema init, migration, router mount, pusher
start) would propagate and crash backend startup on an enterprise build.

Add a broad `except Exception` that logs loudly + a traceback and continues
in OSS-only mode. Modules registered before the failure stay active; the
rest are simply absent from feature-flags. The core platform always boots.

OSS-only builds are unaffected (still the ImportError path). Verified: happy
path still boots (health 200) and registers ['audit','siem'].

Related to #995, #997, #847
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@github-actions

github-actions Bot commented Jun 2, 2026

Copy link
Copy Markdown

⚠️ Nightly unit-suite check skipped — merge conflict against dev.

Resolve by running git merge dev locally and pushing the result. The next nightly run will re-test once the conflict is gone.

@vybe vybe left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Validated: suspended-user rejection correct on BOTH auth paths (JWT + MCP key fall through to 401, no None leak), main.py broad-except scoped only to enterprise registration, migration idempotent via _safe_add_column, schema+migration consistent, architecture.md thoroughly updated, UI fully entitlement-gated and OSS-inert. CI 15/15 green. APPROVE.

@vybe
vybe merged commit a5cc93e into dev Jun 2, 2026
15 checks passed
vybe pushed a commit that referenced this pull request Jun 2, 2026
Resolve MIGRATIONS conflict in src/backend/db/migrations.py: #902
(_migrate_agent_loops_tables) and #996 (_migrate_users_suspended_at)
both appended near the list tail. Kept both functions and both
name-keyed MIGRATIONS entries — they touch disjoint tables and the
runner is order-independent. All other files auto-merged.

Verified: 66 migrations (no dup names), 34 migration smoke tests
(incl. concurrent race) + 16 loop + soft-delete tests pass.

Co-Authored-By: Claude Opus 4.8 (1M context) <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.

2 participants