Skip to content

Fixed system owner invitation flow - #52

Merged
pramodnarayana merged 2 commits into
developmentfrom
fix/system-owner-invitation-flow
Feb 12, 2026
Merged

pramodnarayana merged 2 commits into
developmentfrom
fix/system-owner-invitation-flow

Conversation

@pramodnarayana

@pramodnarayana pramodnarayana commented Feb 12, 2026 •

Copy link
Copy Markdown
Owner

Summary by CodeRabbit

  • New Features

    • Added admin invitations management resource for centralized invite handling.
  • Bug Fixes

    • Fixed admin dashboard redirect to recognize additional admin permission types.
  • Improvements

    • Relaxed invitation ID validation to accept more formats.
    • Endpoint selection for invitation submissions now adapts to admin context.
    • Updated admin invitation permission requirement to the create scope.

@coderabbitai

coderabbitai Bot commented Feb 12, 2026 •

Copy link
Copy Markdown
Contributor
📝 Walkthrough

Walkthrough

Permission checks for system invitation endpoints change from 'invite' to 'create'. System invitations now use the system tenant ID and include request headers when creating invitations. invitationId validation is relaxed to non-empty string. Admin invitations route added; invite dialog and signup redirect logic updated for admin/system permissions.

Changes

Cohort / File(s) Summary
System Admin Permission & Invitation
apps/api/src/modules/identity/system-admin/system-admin.controller.ts, apps/api/src/modules/identity/system-admin/system-admin.controller.spec.ts
RequirePermission action changed from invite → create for system user endpoints. createInvitation now passes getRequiredSystemTenantId() as organizationId and forwards webHeaders to the auth provider; tests updated accordingly.
Validation Schema
apps/api/src/modules/identity/users/users.validation.ts
CompleteInviteSchema.invitationId relaxed from z.string().uuid() to z.string().min(1), allowing non-UUID non-empty values.
Web Routes & UI
apps/web/src/app/routes/AdminRoutes.tsx, apps/web/src/modules/identity/pages/SignupPage.tsx, apps/web/src/modules/identity/users/InviteUserDialog.tsx
Added admin invitations resource route. Signup post-login redirect now checks admin_dashboard:view OR system_users:read. InviteUserDialog detects admin context and targets admin/invitations endpoint when appropriate.

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant WebApp
    participant API
    participant AuthProvider
    participant TenantService

    User->>WebApp: Submit invite (admin context)
    WebApp->>API: POST /admin/invitations (includes headers)
    API->>TenantService: getRequiredSystemTenantId()
    TenantService-->>API: systemTenantId
    API->>AuthProvider: createInvitation(payload, organizationId=systemTenantId, headers)
    AuthProvider-->>API: InvitationCreated
    API-->>WebApp: 201 Created
    WebApp-->>User: show success
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

Poem

🐰 I hopped in code where headers blend,
I planted tenant seeds to send,
Permissions traded 'invite' for 'create',
Routes and invites now navigate,
A tiny leap — a joyful sprint! 🥕

🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 33.33% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'Fixed system owner invitation flow' accurately summarizes the main change across the changeset—fixing the system owner/admin invitation workflow by adjusting permissions, updating validation schemas, and routing invitations correctly.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch fix/system-owner-invitation-flow

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot 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.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (3)
apps/api/src/modules/identity/system-admin/system-admin.controller.ts (2)

73-79: ⚠️ Potential issue | 🟠 Major

Inconsistency: inviteUser still uses organizationId: null and omits headers, unlike createSystemInvitation.

createSystemInvitation (line 103) was updated to use getRequiredSystemTenantId() and pass headers: webHeaders (marked as "Required by Better Auth"). However, inviteUser still passes organizationId: null and omits headers entirely. If Better Auth requires headers for invitation creation, this endpoint will fail at runtime.

Either align both endpoints or document why they intentionally differ.

🐛 Proposed fix to align inviteUser with createSystemInvitation
     // Create System Invitation (OrgId = null)
     await this.authProvider.createInvitation({
       email: user.email,
       role: getRequiredAdminRoleId(),
-      organizationId: null, // System Invite
+      organizationId: getRequiredSystemTenantId(), // System Invite
       inviterId: session.user.id,
+      headers: webHeaders, // Required by Better Auth
     });

111-119: ⚠️ Potential issue | 🟡 Minor

toWebHeaders type mismatch with NestJS @Headers() decorator.

NestJS's @Headers() returns Record<string, string | string[]> (headers with duplicate keys are arrays), but the parameter is typed as Record<string, string>. While NestJS typically collapses values, if an array-valued header slips through, webHeaders.append(key, value) would call toString() on the array, producing "a,b" instead of properly appending each value.

apps/api/src/modules/identity/system-admin/system-admin.controller.spec.ts (1)

394-399: ⚠️ Potential issue | 🟡 Minor

Update this test if inviteUser is aligned with createSystemInvitation.

This test expects organizationId: null and no headers for inviteUser, matching the current controller code. If inviteUser is updated to use getRequiredSystemTenantId() and pass headers (as suggested in the controller review), this assertion must be updated accordingly.

🤖 Fix all issues with AI agents
In `@apps/web/src/modules/identity/users/InviteUserDialog.tsx`:
- Around line 93-99: In InviteUserDialog, stop using window.location.pathname to
decide the endpoint; instead derive isAdminContext from the existing resource
prop (or the already-derived scope) and use that to pick the create(...)
resource; e.g. compute isAdminContext = resource?.startsWith("admin") (or check
scope === "admin") and then set the create call's resource to isAdminContext ?
"admin/invitations" : "invitations" so the endpoint selection uses the same
single source of truth as scope.

Comment thread apps/web/src/modules/identity/users/InviteUserDialog.tsx

@coderabbitai coderabbitai Bot 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.

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@apps/web/src/modules/identity/users/InviteUserDialog.tsx`:
- Around line 92-99: Hoist a single const isAdminContext = resource ===
"admin/users" at the component scope and use that variable both where scope is
derived (instead of repeating the comparison on line 53) and inside the onSubmit
function to choose the create() resource (admin/invitations vs invitations);
update scope derivation to reference isAdminContext and remove the duplicated
resource === "admin/users" check in onSubmit to keep the logic DRY.

Comment on lines 92 to +99
const onSubmit = (data: InviteUserFormValues) => {
// Detect if we're in admin context (System Owner)
const isAdminContext = resource === "admin/users";

create(
{
resource: "invitations", // Explicitly call invitations endpoint
// System Owner uses admin endpoint, Tenant Admin uses regular endpoint
resource: isAdminContext ? "admin/invitations" : "invitations",

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.

🧹 Nitpick | 🔵 Trivial

Good fix — correctly derives admin context from the resource prop now.

The change properly addresses the prior feedback by eliminating the window.location.pathname dependency. One small nit: the resource === "admin/users" comparison is already evaluated on line 53 to derive scope. You could hoist a single isAdminContext const at component level and reuse it for both scope and the endpoint selection to avoid the duplicated check.

♻️ Optional DRY improvement
-    // Determine scope based on resource
-    const scope = resource === "admin/users" ? "system" : "organization";
+    const isAdminContext = resource === "admin/users";
+    const scope = isAdminContext ? "system" : "organization";

Then in onSubmit, drop the local re-derivation:

     const onSubmit = (data: InviteUserFormValues) => {
-        // Detect if we're in admin context (System Owner)
-        const isAdminContext = resource === "admin/users";
-
         create(
             {
                 // System Owner uses admin endpoint, Tenant Admin uses regular endpoint
                 resource: isAdminContext ? "admin/invitations" : "invitations",
🤖 Prompt for AI Agents
In `@apps/web/src/modules/identity/users/InviteUserDialog.tsx` around lines 92 -
99, Hoist a single const isAdminContext = resource === "admin/users" at the
component scope and use that variable both where scope is derived (instead of
repeating the comparison on line 53) and inside the onSubmit function to choose
the create() resource (admin/invitations vs invitations); update scope
derivation to reference isAdminContext and remove the duplicated resource ===
"admin/users" check in onSubmit to keep the logic DRY.

@pramodnarayana
pramodnarayana merged commit 5b63a2e into development Feb 12, 2026
2 checks passed
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.

1 participant