WIp - manage role permissions per object

This commit is contained in:
Francisco Gaona
2025-12-30 06:16:54 +01:00
parent d15fc918d1
commit 3086f78d34
4 changed files with 316 additions and 59 deletions

View File

@@ -794,4 +794,109 @@ export class ObjectService {
return { success: true };
}
async getObjectPermissions(
tenantId: string,
objectApiName: string,
roleId: string,
) {
const resolvedTenantId = await this.tenantDbService.resolveTenantId(tenantId);
const knex = await this.tenantDbService.getTenantKnexById(resolvedTenantId);
// Get object definition
const objectDef = await ObjectDefinition.query(knex)
.findOne({ apiName: objectApiName });
if (!objectDef) {
throw new NotFoundException(`Object ${objectApiName} not found`);
}
// Get role object permissions
const permission = await knex('role_object_permissions')
.where({ roleId, objectDefinitionId: objectDef.id })
.first();
if (!permission) {
// Return default permissions (all false)
return {
canCreate: false,
canRead: false,
canEdit: false,
canDelete: false,
canViewAll: false,
canModifyAll: false,
};
}
return {
canCreate: Boolean(permission.canCreate),
canRead: Boolean(permission.canRead),
canEdit: Boolean(permission.canEdit),
canDelete: Boolean(permission.canDelete),
canViewAll: Boolean(permission.canViewAll),
canModifyAll: Boolean(permission.canModifyAll),
};
}
async updateObjectPermissions(
tenantId: string,
objectApiName: string,
data: {
roleId: string;
canCreate: boolean;
canRead: boolean;
canEdit: boolean;
canDelete: boolean;
canViewAll: boolean;
canModifyAll: boolean;
},
) {
const resolvedTenantId = await this.tenantDbService.resolveTenantId(tenantId);
const knex = await this.tenantDbService.getTenantKnexById(resolvedTenantId);
// Get object definition
const objectDef = await ObjectDefinition.query(knex)
.findOne({ apiName: objectApiName });
if (!objectDef) {
throw new NotFoundException(`Object ${objectApiName} not found`);
}
// Check if permission already exists
const existing = await knex('role_object_permissions')
.where({ roleId: data.roleId, objectDefinitionId: objectDef.id })
.first();
if (existing) {
// Update existing permission
await knex('role_object_permissions')
.where({ roleId: data.roleId, objectDefinitionId: objectDef.id })
.update({
canCreate: data.canCreate,
canRead: data.canRead,
canEdit: data.canEdit,
canDelete: data.canDelete,
canViewAll: data.canViewAll,
canModifyAll: data.canModifyAll,
updated_at: knex.fn.now(),
});
} else {
// Create new permission
await knex('role_object_permissions').insert({
id: knex.raw('(UUID())'),
roleId: data.roleId,
objectDefinitionId: objectDef.id,
canCreate: data.canCreate,
canRead: data.canRead,
canEdit: data.canEdit,
canDelete: data.canDelete,
canViewAll: data.canViewAll,
canModifyAll: data.canModifyAll,
created_at: knex.fn.now(),
updated_at: knex.fn.now(),
});
}
return { success: true };
}
}

View File

@@ -97,4 +97,30 @@ export class SetupObjectController {
) {
return this.objectService.updateFieldPermission(tenantId, data.roleId, data.fieldDefinitionId, data.canRead, data.canEdit);
}
@Get(':objectApiName/permissions/:roleId')
async getObjectPermissions(
@TenantId() tenantId: string,
@Param('objectApiName') objectApiName: string,
@Param('roleId') roleId: string,
) {
return this.objectService.getObjectPermissions(tenantId, objectApiName, roleId);
}
@Put(':objectApiName/permissions')
async updateObjectPermissions(
@TenantId() tenantId: string,
@Param('objectApiName') objectApiName: string,
@Body() data: {
roleId: string;
canCreate: boolean;
canRead: boolean;
canEdit: boolean;
canDelete: boolean;
canViewAll: boolean;
canModifyAll: boolean;
},
) {
return this.objectService.updateObjectPermissions(tenantId, objectApiName, data);
}
}

View File

@@ -15,60 +15,135 @@
No roles available. Create roles first to manage field-level permissions.
</div>
<div v-else class="space-y-4">
<div class="rounded-md border">
<table class="w-full">
<thead>
<tr class="border-b bg-muted/50">
<th class="p-3 text-left font-medium">Field</th>
<th
v-for="role in roles"
:key="role.id"
class="p-3 text-center font-medium border-l"
:colspan="2"
>
{{ role.name }}
</th>
</tr>
<tr class="border-b bg-muted/30">
<th class="p-2 text-left text-xs font-medium text-muted-foreground"></th>
<template v-for="role in roles" :key="`${role.id}-headers`">
<th class="p-2 text-center text-xs font-medium text-muted-foreground border-l">Read</th>
<th class="p-2 text-center text-xs font-medium text-muted-foreground">Edit</th>
</template>
</tr>
</thead>
<tbody>
<tr
v-for="field in fields"
:key="field.id"
class="border-b hover:bg-muted/30"
>
<td class="p-3">
<div>
<div class="font-medium">{{ field.label }}</div>
<div class="text-xs text-muted-foreground">{{ field.apiName }}</div>
</div>
</td>
<template v-for="role in roles" :key="`${field.id}-${role.id}`">
<td class="p-3 text-center border-l">
<div v-else class="space-y-6">
<!-- Role Selector -->
<div class="space-y-2">
<Label>Select Role</Label>
<Select v-model="selectedRoleId" @update:model-value="(value) => selectedRoleId = value">
<SelectTrigger class="w-full">
<SelectValue placeholder="Choose a role to configure permissions" />
</SelectTrigger>
<SelectContent>
<SelectItem v-for="role in roles" :key="role.id" :value="role.id">
{{ role.name }}
</SelectItem>
</SelectContent>
</Select>
</div>
<!-- Object-Level Permissions -->
<div v-if="selectedRoleId" class="space-y-2">
<h3 class="text-sm font-medium">Object-Level Permissions</h3>
<div class="rounded-md border">
<table class="w-full">
<thead>
<tr class="border-b bg-muted/50">
<th class="p-3 text-left font-medium">Permission</th>
<th class="p-3 text-center font-medium">Enabled</th>
</tr>
</thead>
<tbody>
<tr class="border-b hover:bg-muted/30">
<td class="p-3">Create</td>
<td class="p-3 text-center">
<Checkbox
:model-value="hasPermission(field.id, role.id, 'read')"
@update:model-value="(checked: boolean) => updatePermission(field.id, role.id, 'read', checked)"
:model-value="objectPermissions.canCreate"
@update:model-value="(checked: boolean) => updateObjectPermission('canCreate', checked)"
/>
</td>
</tr>
<tr class="border-b hover:bg-muted/30">
<td class="p-3">Read</td>
<td class="p-3 text-center">
<Checkbox
:model-value="objectPermissions.canRead"
@update:model-value="(checked: boolean) => updateObjectPermission('canRead', checked)"
/>
</td>
</tr>
<tr class="border-b hover:bg-muted/30">
<td class="p-3">Edit</td>
<td class="p-3 text-center">
<Checkbox
:model-value="objectPermissions.canEdit"
@update:model-value="(checked: boolean) => updateObjectPermission('canEdit', checked)"
/>
</td>
</tr>
<tr class="border-b hover:bg-muted/30">
<td class="p-3">Delete</td>
<td class="p-3 text-center">
<Checkbox
:model-value="objectPermissions.canDelete"
@update:model-value="(checked: boolean) => updateObjectPermission('canDelete', checked)"
/>
</td>
</tr>
<tr class="border-b hover:bg-muted/30">
<td class="p-3">View All</td>
<td class="p-3 text-center">
<Checkbox
:model-value="objectPermissions.canViewAll"
@update:model-value="(checked: boolean) => updateObjectPermission('canViewAll', checked)"
/>
</td>
</tr>
<tr class="hover:bg-muted/30">
<td class="p-3">Modify All</td>
<td class="p-3 text-center">
<Checkbox
:model-value="objectPermissions.canModifyAll"
@update:model-value="(checked: boolean) => updateObjectPermission('canModifyAll', checked)"
/>
</td>
</tr>
</tbody>
</table>
</div>
</div>
<!-- Field-Level Permissions -->
<div v-if="selectedRoleId" class="space-y-2">
<h3 class="text-sm font-medium">Field-Level Permissions</h3>
<div class="rounded-md border">
<table class="w-full">
<thead>
<tr class="border-b bg-muted/50">
<th class="p-3 text-left font-medium">Field</th>
<th class="p-3 text-center font-medium">Read</th>
<th class="p-3 text-center font-medium">Edit</th>
</tr>
</thead>
<tbody>
<tr
v-for="field in fields"
:key="field.id"
class="border-b hover:bg-muted/30"
>
<td class="p-3">
<div>
<div class="font-medium">{{ field.label }}</div>
<div class="text-xs text-muted-foreground">{{ field.apiName }}</div>
</div>
</td>
<td class="p-3 text-center">
<Checkbox
:model-value="hasPermission(field.id, selectedRoleId, 'read')"
@update:model-value="(checked: boolean) => updatePermission(field.id, selectedRoleId, 'read', checked)"
:disabled="field.isSystem"
/>
</td>
<td class="p-3 text-center">
<Checkbox
:model-value="hasPermission(field.id, role.id, 'edit')"
@update:model-value="(checked: boolean) => updatePermission(field.id, role.id, 'edit', checked)"
:disabled="field.isSystem || !hasPermission(field.id, role.id, 'read')"
:model-value="hasPermission(field.id, selectedRoleId, 'edit')"
@update:model-value="(checked: boolean) => updatePermission(field.id, selectedRoleId, 'edit', checked)"
:disabled="field.isSystem || !hasPermission(field.id, selectedRoleId, 'read')"
/>
</td>
</template>
</tr>
</tbody>
</table>
</tr>
</tbody>
</table>
</div>
</div>
<div class="flex items-center gap-2 text-sm text-muted-foreground">
@@ -86,13 +161,16 @@
</template>
<script setup lang="ts">
import { ref, onMounted, computed } from 'vue';
import { ref, onMounted, computed, watch } from 'vue';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '~/components/ui/card';
import { Checkbox } from '~/components/ui/checkbox';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '~/components/ui/select';
import { Label } from '~/components/ui/label';
import { Info } from 'lucide-vue-next';
const props = defineProps<{
objectId: string;
objectApiName: string;
fields: any[];
}>();
@@ -102,7 +180,16 @@ const { toast } = useToast();
const loading = ref(true);
const saving = ref(false);
const roles = ref<any[]>([]);
const selectedRoleId = ref<string>('');
const permissions = ref<Map<string, Map<string, { canRead: boolean; canEdit: boolean }>>>(new Map());
const objectPermissions = ref({
canCreate: false,
canRead: false,
canEdit: false,
canDelete: false,
canViewAll: false,
canModifyAll: false,
});
// Load roles and permissions
onMounted(async () => {
@@ -149,8 +236,6 @@ const hasPermission = (fieldId: string, roleId: string, type: 'read' | 'edit'):
};
const updatePermission = async (fieldId: string, roleId: string, type: 'read' | 'edit', checked: boolean) => {
console.log('updatePermission called:', { fieldId, roleId, type, checked });
try {
saving.value = true;
@@ -180,22 +265,14 @@ const updatePermission = async (fieldId: string, roleId: string, type: 'read' |
}
}
console.log('Saving permission:', {
roleId,
fieldDefinitionId: fieldId,
canRead: perm.canRead,
canEdit: perm.canEdit,
});
// Save to backend
const result = await api.put(`/setup/objects/${props.objectId}/field-permissions`, {
await api.put(`/setup/objects/${props.objectId}/field-permissions`, {
roleId,
fieldDefinitionId: fieldId,
canRead: perm.canRead,
canEdit: perm.canEdit,
});
console.log('Save result:', result);
toast.success('Permission updated');
} catch (error: any) {
@@ -216,4 +293,52 @@ const updatePermission = async (fieldId: string, roleId: string, type: 'read' |
saving.value = false;
}
};
const updateObjectPermission = async (permission: string, checked: boolean) => {
if (!selectedRoleId.value) return;
try {
saving.value = true;
// Update local state
(objectPermissions.value as any)[permission] = checked;
// Save to backend
await api.put(`/setup/objects/${props.objectApiName}/permissions`, {
roleId: selectedRoleId.value,
...objectPermissions.value,
});
toast.success('Object permission updated');
} catch (error: any) {
console.error('Failed to update object permission:', error);
toast.error(error.message || 'Failed to update permission');
// Revert change
(objectPermissions.value as any)[permission] = !checked;
} finally {
saving.value = false;
}
};
// Load object permissions when role changes
watch(selectedRoleId, async (roleId) => {
if (!roleId) return;
try {
const response = await api.get(`/setup/objects/${props.objectApiName}/permissions/${roleId}`);
if (response) {
objectPermissions.value = {
canCreate: Boolean(response.canCreate),
canRead: Boolean(response.canRead),
canEdit: Boolean(response.canEdit),
canDelete: Boolean(response.canDelete),
canViewAll: Boolean(response.canViewAll),
canModifyAll: Boolean(response.canModifyAll),
};
}
} catch (error: any) {
console.error('Failed to load object permissions:', error);
}
});
</script>

View File

@@ -44,8 +44,9 @@
</Card>
<FieldLevelSecurity
v-if="objectId && fields && fields.length > 0"
v-if="objectId && objectApiName && fields && fields.length > 0"
:object-id="objectId"
:object-api-name="objectApiName"
:fields="fields"
/>