From 6c29d18696f8b4df4897b64e9b0edf7a5383e707 Mon Sep 17 00:00:00 2001 From: Francisco Gaona Date: Tue, 30 Dec 2025 09:10:45 +0100 Subject: [PATCH] WIP - more admin users and roles --- backend/src/rbac/setup-roles.controller.ts | 41 +++++++ backend/src/rbac/setup-users.controller.ts | 42 +++++++ frontend/pages/setup/roles/index.vue | 127 +++++++++++++++++++- frontend/pages/setup/users/index.vue | 129 ++++++++++++++++++++- 4 files changed, 331 insertions(+), 8 deletions(-) diff --git a/backend/src/rbac/setup-roles.controller.ts b/backend/src/rbac/setup-roles.controller.ts index e97d29c..1a1655f 100644 --- a/backend/src/rbac/setup-roles.controller.ts +++ b/backend/src/rbac/setup-roles.controller.ts @@ -2,6 +2,7 @@ import { Controller, Get, Post, + Patch, Delete, Param, Body, @@ -53,6 +54,46 @@ export class SetupRolesController { return role; } + @Patch(':id') + async updateRole( + @TenantId() tenantId: string, + @Param('id') id: string, + @Body() data: { name?: string; description?: string; guardName?: string }, + ) { + const resolvedTenantId = await this.tenantDbService.resolveTenantId(tenantId); + const knex = await this.tenantDbService.getTenantKnexById(resolvedTenantId); + + const updateData: any = {}; + + if (data.name) updateData.name = data.name; + if (data.description !== undefined) updateData.description = data.description; + if (data.guardName) updateData.guardName = data.guardName; + + const role = await Role.query(knex).patchAndFetchById(id, updateData); + return role; + } + + @Delete(':id') + async deleteRole( + @TenantId() tenantId: string, + @Param('id') id: string, + ) { + const resolvedTenantId = await this.tenantDbService.resolveTenantId(tenantId); + const knex = await this.tenantDbService.getTenantKnexById(resolvedTenantId); + + // Delete role user assignments first + await knex('user_roles').where({ roleId: id }).delete(); + + // Delete role permissions + await knex('role_permissions').where({ roleId: id }).delete(); + await knex('role_object_permissions').where({ roleId: id }).delete(); + + // Delete the role + await Role.query(knex).deleteById(id); + + return { success: true }; + } + @Post(':roleId/users') async addUserToRole( @TenantId() tenantId: string, diff --git a/backend/src/rbac/setup-users.controller.ts b/backend/src/rbac/setup-users.controller.ts index 5c9d6b1..6dbb3a4 100644 --- a/backend/src/rbac/setup-users.controller.ts +++ b/backend/src/rbac/setup-users.controller.ts @@ -2,6 +2,7 @@ import { Controller, Get, Post, + Patch, Delete, Param, Body, @@ -57,6 +58,47 @@ export class SetupUsersController { return user; } + @Patch(':id') + async updateUser( + @TenantId() tenantId: string, + @Param('id') id: 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); + + const updateData: any = {}; + + if (data.email) updateData.email = data.email; + if (data.firstName !== undefined) updateData.firstName = data.firstName; + if (data.lastName !== undefined) updateData.lastName = data.lastName; + + // Hash password if provided + if (data.password) { + updateData.password = await bcrypt.hash(data.password, 10); + } + + const user = await User.query(knex).patchAndFetchById(id, updateData); + return user; + } + + @Delete(':id') + async deleteUser( + @TenantId() tenantId: string, + @Param('id') id: string, + ) { + const resolvedTenantId = await this.tenantDbService.resolveTenantId(tenantId); + const knex = await this.tenantDbService.getTenantKnexById(resolvedTenantId); + + // Delete user role assignments first + await knex('user_roles').where({ userId: id }).delete(); + + // Delete the user + await User.query(knex).deleteById(id); + + return { success: true }; + } + @Post(':userId/roles') async addRoleToUser( @TenantId() tenantId: string, diff --git a/frontend/pages/setup/roles/index.vue b/frontend/pages/setup/roles/index.vue index 9774398..2ddf358 100644 --- a/frontend/pages/setup/roles/index.vue +++ b/frontend/pages/setup/roles/index.vue @@ -49,9 +49,17 @@ {{ formatDate(role.createdAt) }} - +
+ + + +
@@ -97,6 +105,64 @@ + + + + + + Edit Role + + Update role information + + +
+
+ + +
+
+ + +
+
+ + +
+
+ + + + +
+
+ + + + + + Delete Role + + Are you sure you want to delete this role? This action cannot be undone. + + + + + + + + @@ -111,7 +177,7 @@ import { Input } from '~/components/ui/input'; import { Label } from '~/components/ui/label'; import { Badge } from '~/components/ui/badge'; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '~/components/ui/select'; -import { Plus, Eye } from 'lucide-vue-next'; +import { Plus, Eye, Edit, Trash2 } from 'lucide-vue-next'; definePageMeta({ layout: 'default', @@ -123,11 +189,20 @@ const { toast } = useToast(); const loading = ref(true); const roles = ref([]); const showCreateDialog = ref(false); +const showEditDialog = ref(false); +const showDeleteDialog = ref(false); const newRole = ref({ name: '', description: '', guardName: 'tenant', }); +const editRole = ref({ + id: '', + name: '', + description: '', + guardName: 'tenant', +}); +const roleToDelete = ref(null); const loadRoles = async () => { try { @@ -155,6 +230,50 @@ const createRole = async () => { } }; +const openEditDialog = (role: any) => { + editRole.value = { + id: role.id, + name: role.name, + description: role.description || '', + guardName: role.guardName || 'tenant', + }; + showEditDialog.value = true; +}; + +const updateRole = async () => { + try { + await api.patch(`/setup/roles/${editRole.value.id}`, { + name: editRole.value.name, + description: editRole.value.description, + guardName: editRole.value.guardName, + }); + toast.success('Role updated successfully'); + showEditDialog.value = false; + await loadRoles(); + } catch (error: any) { + console.error('Failed to update role:', error); + toast.error(error.message || 'Failed to update role'); + } +}; + +const openDeleteDialog = (role: any) => { + roleToDelete.value = role; + showDeleteDialog.value = true; +}; + +const deleteRole = async () => { + try { + await api.delete(`/setup/roles/${roleToDelete.value.id}`); + toast.success('Role deleted successfully'); + showDeleteDialog.value = false; + roleToDelete.value = null; + await loadRoles(); + } catch (error: any) { + console.error('Failed to delete role:', error); + toast.error(error.message || 'Failed to delete role'); + } +}; + const formatDate = (date: string) => { if (!date) return 'N/A'; return new Date(date).toLocaleDateString(); diff --git a/frontend/pages/setup/users/index.vue b/frontend/pages/setup/users/index.vue index beb7b1a..bf594e4 100644 --- a/frontend/pages/setup/users/index.vue +++ b/frontend/pages/setup/users/index.vue @@ -52,9 +52,17 @@ {{ formatDate(user.createdAt) }} - +
+ + + +
@@ -96,6 +104,60 @@ + + + + + + Edit User + + Update user information + + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + + + +
+
+ + + + + + Delete User + + Are you sure you want to delete this user? This action cannot be undone. + + + + + + + + @@ -109,7 +171,7 @@ import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, D import { Input } from '~/components/ui/input'; import { Label } from '~/components/ui/label'; import { Badge } from '~/components/ui/badge'; -import { UserPlus, Eye } from 'lucide-vue-next'; +import { UserPlus, Eye, Edit, Trash2 } from 'lucide-vue-next'; const { api } = useApi(); @@ -118,12 +180,22 @@ const { toast } = useToast(); const loading = ref(true); const users = ref([]); const showCreateDialog = ref(false); +const showEditDialog = ref(false); +const showDeleteDialog = ref(false); const newUser = ref({ email: '', password: '', firstName: '', lastName: '', }); +const editUser = ref({ + id: '', + email: '', + password: '', + firstName: '', + lastName: '', +}); +const userToDelete = ref(null); const loadUsers = async () => { try { @@ -151,6 +223,55 @@ const createUser = async () => { } }; +const openEditDialog = (user: any) => { + editUser.value = { + id: user.id, + email: user.email, + password: '', + firstName: user.firstName || '', + lastName: user.lastName || '', + }; + showEditDialog.value = true; +}; + +const updateUser = async () => { + try { + const payload: any = { + email: editUser.value.email, + firstName: editUser.value.firstName, + lastName: editUser.value.lastName, + }; + if (editUser.value.password) { + payload.password = editUser.value.password; + } + await api.patch(`/setup/users/${editUser.value.id}`, payload); + toast.success('User updated successfully'); + showEditDialog.value = false; + await loadUsers(); + } catch (error: any) { + console.error('Failed to update user:', error); + toast.error(error.message || 'Failed to update user'); + } +}; + +const openDeleteDialog = (user: any) => { + userToDelete.value = user; + showDeleteDialog.value = true; +}; + +const deleteUser = async () => { + try { + await api.delete(`/setup/users/${userToDelete.value.id}`); + toast.success('User deleted successfully'); + showDeleteDialog.value = false; + userToDelete.value = null; + await loadUsers(); + } catch (error: any) { + console.error('Failed to delete user:', error); + toast.error(error.message || 'Failed to delete user'); + } +}; + const getUserName = (user: any) => { if (user.firstName || user.lastName) { return [user.firstName, user.lastName].filter(Boolean).join(' ');