diff --git a/apps/api/src/modules/identity/system-admin/system-admin.controller.spec.ts b/apps/api/src/modules/identity/system-admin/system-admin.controller.spec.ts index e79a44d7..28c7ded8 100644 --- a/apps/api/src/modules/identity/system-admin/system-admin.controller.spec.ts +++ b/apps/api/src/modules/identity/system-admin/system-admin.controller.spec.ts @@ -9,6 +9,7 @@ import { import { getRequiredAdminRoleId, getRequiredOwnerRoleId, + getRequiredSystemTenantId, } from '../../../constants'; import { SystemAdminGuard } from '../auth/system-admin.guard'; import { AuthGuard } from '../auth/auth.guard'; @@ -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 }); }); }); diff --git a/apps/api/src/modules/identity/system-admin/system-admin.controller.ts b/apps/api/src/modules/identity/system-admin/system-admin.controller.ts index 44cef806..3646b63b 100644 --- a/apps/api/src/modules/identity/system-admin/system-admin.controller.ts +++ b/apps/api/src/modules/identity/system-admin/system-admin.controller.ts @@ -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, @@ -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, @@ -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; diff --git a/apps/api/src/modules/identity/users/users.validation.ts b/apps/api/src/modules/identity/users/users.validation.ts index ea489914..ee555209 100644 --- a/apps/api/src/modules/identity/users/users.validation.ts +++ b/apps/api/src/modules/identity/users/users.validation.ts @@ -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) {} diff --git a/apps/web/src/app/routes/AdminRoutes.tsx b/apps/web/src/app/routes/AdminRoutes.tsx index 4fae1505..7f049b51 100644 --- a/apps/web/src/app/routes/AdminRoutes.tsx +++ b/apps/web/src/app/routes/AdminRoutes.tsx @@ -33,6 +33,13 @@ export function AdminRoutes() { canDelete: true, } }, + { + name: "admin/invitations", + create: "/admin/invitations", + meta: { + canDelete: false, + }, + }, { name: "admin/tenants", list: "/admin/tenants", diff --git a/apps/web/src/modules/identity/pages/SignupPage.tsx b/apps/web/src/modules/identity/pages/SignupPage.tsx index 5b6267e2..27d9eea8 100644 --- a/apps/web/src/modules/identity/pages/SignupPage.tsx +++ b/apps/web/src/modules/identity/pages/SignupPage.tsx @@ -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'); diff --git a/apps/web/src/modules/identity/users/InviteUserDialog.tsx b/apps/web/src/modules/identity/users/InviteUserDialog.tsx index f189c3c5..ada0491b 100644 --- a/apps/web/src/modules/identity/users/InviteUserDialog.tsx +++ b/apps/web/src/modules/identity/users/InviteUserDialog.tsx @@ -90,9 +90,13 @@ export function InviteUserDialog({ resource = "users" }: Readonly { + // 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", values: { ...data, // The backend handles organizationId injection based on user token.