Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
import {
getRequiredAdminRoleId,
getRequiredOwnerRoleId,
getRequiredSystemTenantId,
} from '../../../constants';
import { SystemAdminGuard } from '../auth/system-admin.guard';
import { AuthGuard } from '../auth/auth.guard';
Expand Down Expand Up @@ -138,8 +139,9 @@ describe('SystemAdminController', () => {
expect(mockAuthProvider.createInvitation).toHaveBeenCalledWith({
email: 'test@example.com',
role: getRequiredOwnerRoleId(),
organizationId: null,
organizationId: getRequiredSystemTenantId(), // Changed from null
inviterId: 'admin1',
headers: expect.any(Headers) as Headers, // Added headers
});
});
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ export class SystemAdminController {
) {}

@Post('users/:id/invite')
@RequirePermission('system_users', 'invite')
@RequirePermission('system_users', 'create')
async inviteUser(
@Param('id') id: string,
@RequestHeaders() headers: Record<string, string>,
Expand Down Expand Up @@ -82,7 +82,7 @@ export class SystemAdminController {
}

@Post('invitations')
@RequirePermission('system_users', 'invite')
@RequirePermission('system_users', 'create')
@UsePipes(new LazyZodValidationPipe(buildCreateSystemInvitationSchema))
async createSystemInvitation(
@Body() body: CreateSystemInvitationDto,
Expand All @@ -100,8 +100,9 @@ export class SystemAdminController {
const invitation = await this.authProvider.createInvitation({
email: body.email,
role: body.role, // Zod handles default
organizationId: null, // System invitation
organizationId: getRequiredSystemTenantId(), // System tenant (Nexiom Platform)
inviterId: session.user.id,
headers: webHeaders, // Required by Better Auth
});

return invitation;
Expand Down
2 changes: 1 addition & 1 deletion apps/api/src/modules/identity/users/users.validation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ export const CompleteInviteSchema = z.object({
.min(8, { message: 'Password must be at least 8 characters' }),
firstName: z.string().min(1),
lastName: z.string().min(1),
invitationId: z.string().uuid(),
invitationId: z.string().min(1), // Better Auth uses non-UUID format
});

export class CompleteInvite extends createZodDto(CompleteInviteSchema) {}
Expand Down
7 changes: 7 additions & 0 deletions apps/web/src/app/routes/AdminRoutes.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,13 @@ export function AdminRoutes() {
canDelete: true,
}
},
{
name: "admin/invitations",
create: "/admin/invitations",
meta: {
canDelete: false,
},
},
{
name: "admin/tenants",
list: "/admin/tenants",
Expand Down
8 changes: 5 additions & 3 deletions apps/web/src/modules/identity/pages/SignupPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -123,12 +123,14 @@ export function SignupPage() {
accessToken: sessionData.session.token // Using 'token' from session
});


// --- PERMISSION BASED REDIRECT ---
// We check if the user has admin capabilities
// Note: ensure your sessions endpoint returns permissions
// System Owner has admin_dashboard:view or system_* permissions
// Regular users have dashboard:read permission
const user = sessionData.user as { permissions?: string[] };

if (hasPermission(user.permissions, 'dashboard', 'view')) {
if (hasPermission(user.permissions, 'admin_dashboard', 'view') ||
hasPermission(user.permissions, 'system_users', 'read')) {
navigate('/admin');
} else {
navigate('/dashboard');
Expand Down
6 changes: 5 additions & 1 deletion apps/web/src/modules/identity/users/InviteUserDialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -90,9 +90,13 @@ export function InviteUserDialog({ resource = "users" }: Readonly<InviteUserDial
}, [open, roles, form]);

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",
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Comment on lines 92 to +99

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.

values: {
...data,
// The backend handles organizationId injection based on user token.
Expand Down