diff --git a/backend/src/rbac/rbac.module.ts b/backend/src/rbac/rbac.module.ts index b001756..8e2a820 100644 --- a/backend/src/rbac/rbac.module.ts +++ b/backend/src/rbac/rbac.module.ts @@ -3,11 +3,12 @@ import { RbacService } from './rbac.service'; import { AbilityFactory } from './ability.factory'; import { AuthorizationService } from './authorization.service'; import { SetupRolesController } from './setup-roles.controller'; +import { SetupUsersController } from './setup-users.controller'; import { TenantModule } from '../tenant/tenant.module'; @Module({ imports: [TenantModule], - controllers: [SetupRolesController], + controllers: [SetupRolesController, SetupUsersController], providers: [RbacService, AbilityFactory, AuthorizationService], exports: [RbacService, AbilityFactory, AuthorizationService], }) diff --git a/backend/src/rbac/setup-roles.controller.ts b/backend/src/rbac/setup-roles.controller.ts index 98465bd..e97d29c 100644 --- a/backend/src/rbac/setup-roles.controller.ts +++ b/backend/src/rbac/setup-roles.controller.ts @@ -1,6 +1,10 @@ import { Controller, Get, + Post, + Delete, + Param, + Body, UseGuards, } from '@nestjs/common'; import { JwtAuthGuard } from '../auth/jwt-auth.guard'; @@ -20,4 +24,77 @@ export class SetupRolesController { return await Role.query(knex).select('*').orderBy('name', 'asc'); } + + @Get(':id') + async getRole( + @TenantId() tenantId: string, + @Param('id') id: string, + ) { + const resolvedTenantId = await this.tenantDbService.resolveTenantId(tenantId); + const knex = await this.tenantDbService.getTenantKnexById(resolvedTenantId); + + return await Role.query(knex).findById(id).withGraphFetched('users'); + } + + @Post() + async createRole( + @TenantId() tenantId: string, + @Body() data: { name: string; description?: string; guardName?: string }, + ) { + const resolvedTenantId = await this.tenantDbService.resolveTenantId(tenantId); + const knex = await this.tenantDbService.getTenantKnexById(resolvedTenantId); + + const role = await Role.query(knex).insert({ + name: data.name, + description: data.description, + guardName: data.guardName || 'tenant', + }); + + return role; + } + + @Post(':roleId/users') + async addUserToRole( + @TenantId() tenantId: string, + @Param('roleId') roleId: string, + @Body() data: { userId: string }, + ) { + const resolvedTenantId = await this.tenantDbService.resolveTenantId(tenantId); + const knex = await this.tenantDbService.getTenantKnexById(resolvedTenantId); + + // Check if assignment already exists + const existing = await knex('user_roles') + .where({ userId: data.userId, roleId }) + .first(); + + if (existing) { + return { success: true, message: 'User already assigned' }; + } + + await knex('user_roles').insert({ + id: knex.raw('(UUID())'), + userId: data.userId, + roleId, + created_at: knex.fn.now(), + updated_at: knex.fn.now(), + }); + + return { success: true }; + } + + @Delete(':roleId/users/:userId') + async removeUserFromRole( + @TenantId() tenantId: string, + @Param('roleId') roleId: string, + @Param('userId') userId: string, + ) { + const resolvedTenantId = await this.tenantDbService.resolveTenantId(tenantId); + const knex = await this.tenantDbService.getTenantKnexById(resolvedTenantId); + + await knex('user_roles') + .where({ userId, roleId }) + .delete(); + + return { success: true }; + } } diff --git a/backend/src/rbac/setup-users.controller.ts b/backend/src/rbac/setup-users.controller.ts new file mode 100644 index 0000000..5c9d6b1 --- /dev/null +++ b/backend/src/rbac/setup-users.controller.ts @@ -0,0 +1,104 @@ +import { + Controller, + Get, + Post, + Delete, + Param, + Body, + UseGuards, +} from '@nestjs/common'; +import { JwtAuthGuard } from '../auth/jwt-auth.guard'; +import { TenantId } from '../tenant/tenant.decorator'; +import { TenantDatabaseService } from '../tenant/tenant-database.service'; +import { User } from '../models/user.model'; +import * as bcrypt from 'bcrypt'; + +@Controller('setup/users') +@UseGuards(JwtAuthGuard) +export class SetupUsersController { + constructor(private tenantDbService: TenantDatabaseService) {} + + @Get() + async getUsers(@TenantId() tenantId: string) { + const resolvedTenantId = await this.tenantDbService.resolveTenantId(tenantId); + const knex = await this.tenantDbService.getTenantKnexById(resolvedTenantId); + return await User.query(knex).withGraphFetched('roles'); + } + + @Get(':id') + async getUser( + @TenantId() tenantId: string, + @Param('id') id: string, + ) { + const resolvedTenantId = await this.tenantDbService.resolveTenantId(tenantId); + const knex = await this.tenantDbService.getTenantKnexById(resolvedTenantId); + return await User.query(knex).findById(id).withGraphFetched('roles'); + } + + @Post() + async createUser( + @TenantId() tenantId: string, + @Body() data: { email: string; password: string; firstName?: string; lastName?: string }, + ) { + const resolvedTenantId = await this.tenantDbService.resolveTenantId(tenantId); + const knex = await this.tenantDbService.getTenantKnexById(resolvedTenantId); + + // Hash password + const hashedPassword = await bcrypt.hash(data.password, 10); + + const user = await User.query(knex).insert({ + email: data.email, + password: hashedPassword, + firstName: data.firstName, + lastName: data.lastName, + isActive: true, + }); + + return user; + } + + @Post(':userId/roles') + async addRoleToUser( + @TenantId() tenantId: string, + @Param('userId') userId: string, + @Body() data: { roleId: string }, + ) { + const resolvedTenantId = await this.tenantDbService.resolveTenantId(tenantId); + const knex = await this.tenantDbService.getTenantKnexById(resolvedTenantId); + + // Check if assignment already exists + const existing = await knex('user_roles') + .where({ userId, roleId: data.roleId }) + .first(); + + if (existing) { + return { success: true, message: 'Role already assigned' }; + } + + await knex('user_roles').insert({ + id: knex.raw('(UUID())'), + userId, + roleId: data.roleId, + created_at: knex.fn.now(), + updated_at: knex.fn.now(), + }); + + return { success: true }; + } + + @Delete(':userId/roles/:roleId') + async removeRoleFromUser( + @TenantId() tenantId: string, + @Param('userId') userId: string, + @Param('roleId') roleId: string, + ) { + const resolvedTenantId = await this.tenantDbService.resolveTenantId(tenantId); + const knex = await this.tenantDbService.getTenantKnexById(resolvedTenantId); + + await knex('user_roles') + .where({ userId, roleId }) + .delete(); + + return { success: true }; + } +} diff --git a/frontend/components/AppSidebar.vue b/frontend/components/AppSidebar.vue index 2137b62..de7fd30 100644 --- a/frontend/components/AppSidebar.vue +++ b/frontend/components/AppSidebar.vue @@ -105,6 +105,16 @@ const staticMenuItems = [ url: '/setup/objects', icon: Boxes, }, + { + title: 'Users', + url: '/setup/users', + icon: Users, + }, + { + title: 'Roles', + url: '/setup/roles', + icon: Layers, + }, ], }, ] diff --git a/frontend/pages/setup/roles/[id].vue b/frontend/pages/setup/roles/[id].vue new file mode 100644 index 0000000..d1cf4b4 --- /dev/null +++ b/frontend/pages/setup/roles/[id].vue @@ -0,0 +1,231 @@ + + + diff --git a/frontend/pages/setup/roles/index.vue b/frontend/pages/setup/roles/index.vue new file mode 100644 index 0000000..9774398 --- /dev/null +++ b/frontend/pages/setup/roles/index.vue @@ -0,0 +1,166 @@ + + + diff --git a/frontend/pages/setup/users/[id].vue b/frontend/pages/setup/users/[id].vue new file mode 100644 index 0000000..eca9a9a --- /dev/null +++ b/frontend/pages/setup/users/[id].vue @@ -0,0 +1,227 @@ + + + diff --git a/frontend/pages/setup/users/index.vue b/frontend/pages/setup/users/index.vue new file mode 100644 index 0000000..beb7b1a --- /dev/null +++ b/frontend/pages/setup/users/index.vue @@ -0,0 +1,169 @@ + + +