Compare commits
2 Commits
56c0c3838d
...
3086f78d34
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3086f78d34 | ||
|
|
d15fc918d1 |
@@ -583,7 +583,10 @@ export class ObjectService {
|
|||||||
throw new NotFoundException('Record not found');
|
throw new NotFoundException('Record not found');
|
||||||
}
|
}
|
||||||
|
|
||||||
return record;
|
// Filter fields based on field-level permissions
|
||||||
|
const filteredRecord = await this.authService.filterReadableFields(record, objectDefModel.fields, user);
|
||||||
|
|
||||||
|
return filteredRecord;
|
||||||
}
|
}
|
||||||
|
|
||||||
async createRecord(
|
async createRecord(
|
||||||
@@ -738,4 +741,162 @@ export class ObjectService {
|
|||||||
|
|
||||||
return { success: true };
|
return { success: true };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async getFieldPermissions(tenantId: string, objectId: string) {
|
||||||
|
const resolvedTenantId = await this.tenantDbService.resolveTenantId(tenantId);
|
||||||
|
const knex = await this.tenantDbService.getTenantKnexById(resolvedTenantId);
|
||||||
|
|
||||||
|
// Get all field permissions for this object's fields
|
||||||
|
const permissions = await knex('role_field_permissions as rfp')
|
||||||
|
.join('field_definitions as fd', 'fd.id', 'rfp.fieldDefinitionId')
|
||||||
|
.where('fd.objectDefinitionId', objectId)
|
||||||
|
.select('rfp.*');
|
||||||
|
|
||||||
|
return permissions;
|
||||||
|
}
|
||||||
|
|
||||||
|
async updateFieldPermission(
|
||||||
|
tenantId: string,
|
||||||
|
roleId: string,
|
||||||
|
fieldDefinitionId: string,
|
||||||
|
canRead: boolean,
|
||||||
|
canEdit: boolean,
|
||||||
|
) {
|
||||||
|
const resolvedTenantId = await this.tenantDbService.resolveTenantId(tenantId);
|
||||||
|
const knex = await this.tenantDbService.getTenantKnexById(resolvedTenantId);
|
||||||
|
|
||||||
|
// Check if permission already exists
|
||||||
|
const existing = await knex('role_field_permissions')
|
||||||
|
.where({ roleId, fieldDefinitionId })
|
||||||
|
.first();
|
||||||
|
|
||||||
|
if (existing) {
|
||||||
|
// Update existing permission
|
||||||
|
await knex('role_field_permissions')
|
||||||
|
.where({ roleId, fieldDefinitionId })
|
||||||
|
.update({
|
||||||
|
canRead,
|
||||||
|
canEdit,
|
||||||
|
updated_at: knex.fn.now(),
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// Create new permission
|
||||||
|
await knex('role_field_permissions').insert({
|
||||||
|
id: knex.raw('(UUID())'),
|
||||||
|
roleId,
|
||||||
|
fieldDefinitionId,
|
||||||
|
canRead,
|
||||||
|
canEdit,
|
||||||
|
created_at: knex.fn.now(),
|
||||||
|
updated_at: knex.fn.now(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
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 };
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import {
|
|||||||
Get,
|
Get,
|
||||||
Post,
|
Post,
|
||||||
Patch,
|
Patch,
|
||||||
|
Put,
|
||||||
Param,
|
Param,
|
||||||
Body,
|
Body,
|
||||||
UseGuards,
|
UseGuards,
|
||||||
@@ -11,6 +12,7 @@ import { ObjectService } from './object.service';
|
|||||||
import { FieldMapperService } from './field-mapper.service';
|
import { FieldMapperService } from './field-mapper.service';
|
||||||
import { JwtAuthGuard } from '../auth/jwt-auth.guard';
|
import { JwtAuthGuard } from '../auth/jwt-auth.guard';
|
||||||
import { TenantId } from '../tenant/tenant.decorator';
|
import { TenantId } from '../tenant/tenant.decorator';
|
||||||
|
import { TenantDatabaseService } from '../tenant/tenant-database.service';
|
||||||
|
|
||||||
@Controller('setup/objects')
|
@Controller('setup/objects')
|
||||||
@UseGuards(JwtAuthGuard)
|
@UseGuards(JwtAuthGuard)
|
||||||
@@ -18,6 +20,7 @@ export class SetupObjectController {
|
|||||||
constructor(
|
constructor(
|
||||||
private objectService: ObjectService,
|
private objectService: ObjectService,
|
||||||
private fieldMapperService: FieldMapperService,
|
private fieldMapperService: FieldMapperService,
|
||||||
|
private tenantDbService: TenantDatabaseService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
@Get()
|
@Get()
|
||||||
@@ -77,4 +80,47 @@ export class SetupObjectController {
|
|||||||
) {
|
) {
|
||||||
return this.objectService.updateObjectDefinition(tenantId, objectApiName, data);
|
return this.objectService.updateObjectDefinition(tenantId, objectApiName, data);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Get(':objectId/field-permissions')
|
||||||
|
async getFieldPermissions(
|
||||||
|
@TenantId() tenantId: string,
|
||||||
|
@Param('objectId') objectId: string,
|
||||||
|
) {
|
||||||
|
return this.objectService.getFieldPermissions(tenantId, objectId);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Put(':objectId/field-permissions')
|
||||||
|
async updateFieldPermission(
|
||||||
|
@TenantId() tenantId: string,
|
||||||
|
@Param('objectId') objectId: string,
|
||||||
|
@Body() data: { roleId: string; fieldDefinitionId: string; canRead: boolean; canEdit: boolean },
|
||||||
|
) {
|
||||||
|
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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -156,7 +156,20 @@ export class AbilityFactory {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check all roles for field permission
|
// Collect all field permissions from all roles
|
||||||
|
const allFieldPermissions: RoleFieldPermission[] = [];
|
||||||
|
for (const role of user.roles) {
|
||||||
|
if (role.fieldPermissions) {
|
||||||
|
allFieldPermissions.push(...role.fieldPermissions);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If there are NO field permissions configured at all, allow by default
|
||||||
|
if (allFieldPermissions.length === 0) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If field permissions exist, check for explicit grants (union of all roles)
|
||||||
for (const role of user.roles) {
|
for (const role of user.roles) {
|
||||||
if (role.fieldPermissions) {
|
if (role.fieldPermissions) {
|
||||||
const fieldPerm = role.fieldPermissions.find(fp => fp.fieldDefinitionId === fieldDefinitionId);
|
const fieldPerm = role.fieldPermissions.find(fp => fp.fieldDefinitionId === fieldDefinitionId);
|
||||||
@@ -167,8 +180,8 @@ export class AbilityFactory {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Default: allow if no explicit restriction
|
// Field permissions exist but this field is not explicitly granted → deny
|
||||||
return true;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -2,8 +2,12 @@ import { Module } from '@nestjs/common';
|
|||||||
import { RbacService } from './rbac.service';
|
import { RbacService } from './rbac.service';
|
||||||
import { AbilityFactory } from './ability.factory';
|
import { AbilityFactory } from './ability.factory';
|
||||||
import { AuthorizationService } from './authorization.service';
|
import { AuthorizationService } from './authorization.service';
|
||||||
|
import { SetupRolesController } from './setup-roles.controller';
|
||||||
|
import { TenantModule } from '../tenant/tenant.module';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
|
imports: [TenantModule],
|
||||||
|
controllers: [SetupRolesController],
|
||||||
providers: [RbacService, AbilityFactory, AuthorizationService],
|
providers: [RbacService, AbilityFactory, AuthorizationService],
|
||||||
exports: [RbacService, AbilityFactory, AuthorizationService],
|
exports: [RbacService, AbilityFactory, AuthorizationService],
|
||||||
})
|
})
|
||||||
|
|||||||
23
backend/src/rbac/setup-roles.controller.ts
Normal file
23
backend/src/rbac/setup-roles.controller.ts
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
import {
|
||||||
|
Controller,
|
||||||
|
Get,
|
||||||
|
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 { Role } from '../models/role.model';
|
||||||
|
|
||||||
|
@Controller('setup/roles')
|
||||||
|
@UseGuards(JwtAuthGuard)
|
||||||
|
export class SetupRolesController {
|
||||||
|
constructor(private tenantDbService: TenantDatabaseService) {}
|
||||||
|
|
||||||
|
@Get()
|
||||||
|
async getRoles(@TenantId() tenantId: string) {
|
||||||
|
const resolvedTenantId = await this.tenantDbService.resolveTenantId(tenantId);
|
||||||
|
const knex = await this.tenantDbService.getTenantKnexById(resolvedTenantId);
|
||||||
|
|
||||||
|
return await Role.query(knex).select('*').orderBy('name', 'asc');
|
||||||
|
}
|
||||||
|
}
|
||||||
344
frontend/components/FieldLevelSecurity.vue
Normal file
344
frontend/components/FieldLevelSecurity.vue
Normal file
@@ -0,0 +1,344 @@
|
|||||||
|
<template>
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>Field-Level Security</CardTitle>
|
||||||
|
<CardDescription>
|
||||||
|
Control which fields each role can read and edit
|
||||||
|
</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div v-if="loading" class="flex items-center justify-center py-8">
|
||||||
|
<div class="animate-spin rounded-full h-8 w-8 border-b-2 border-primary"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-else-if="roles.length === 0" class="text-sm text-muted-foreground py-4">
|
||||||
|
No roles available. Create roles first to manage field-level permissions.
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<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="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, selectedRoleId, 'edit')"
|
||||||
|
@update:model-value="(checked: boolean) => updatePermission(field.id, selectedRoleId, 'edit', checked)"
|
||||||
|
:disabled="field.isSystem || !hasPermission(field.id, selectedRoleId, 'read')"
|
||||||
|
/>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex items-center gap-2 text-sm text-muted-foreground">
|
||||||
|
<Info class="h-4 w-4" />
|
||||||
|
<span>System fields are always readable. Edit permissions require read permission first. Changes save automatically.</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="saving" class="flex items-center gap-2 text-sm text-primary">
|
||||||
|
<div class="animate-spin rounded-full h-4 w-4 border-b-2 border-primary"></div>
|
||||||
|
<span>Saving...</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
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[];
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const { api } = useApi();
|
||||||
|
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 () => {
|
||||||
|
try {
|
||||||
|
loading.value = true;
|
||||||
|
|
||||||
|
// Load roles
|
||||||
|
const rolesResponse = await api.get('/setup/roles');
|
||||||
|
roles.value = rolesResponse || [];
|
||||||
|
|
||||||
|
// Load existing permissions for this object
|
||||||
|
const permsResponse = await api.get(`/setup/objects/${props.objectId}/field-permissions`);
|
||||||
|
|
||||||
|
// Build permissions map: fieldId -> roleId -> {canRead, canEdit}
|
||||||
|
const permsMap = new Map();
|
||||||
|
if (permsResponse && Array.isArray(permsResponse)) {
|
||||||
|
for (const perm of permsResponse) {
|
||||||
|
if (!permsMap.has(perm.fieldDefinitionId)) {
|
||||||
|
permsMap.set(perm.fieldDefinitionId, new Map());
|
||||||
|
}
|
||||||
|
permsMap.get(perm.fieldDefinitionId).set(perm.roleId, {
|
||||||
|
canRead: Boolean(perm.canRead),
|
||||||
|
canEdit: Boolean(perm.canEdit),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
permissions.value = permsMap;
|
||||||
|
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error('Failed to load field permissions:', error);
|
||||||
|
toast.error('Failed to load field permissions');
|
||||||
|
} finally {
|
||||||
|
loading.value = false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const hasPermission = (fieldId: string, roleId: string, type: 'read' | 'edit'): boolean => {
|
||||||
|
const fieldPerms = permissions.value.get(fieldId);
|
||||||
|
if (!fieldPerms) return true; // Default to true if no permissions set
|
||||||
|
const rolePerm = fieldPerms.get(roleId);
|
||||||
|
if (!rolePerm) return true; // Default to true if no permissions set
|
||||||
|
const value = type === 'read' ? rolePerm.canRead : rolePerm.canEdit;
|
||||||
|
return Boolean(value); // Convert 1/0 to true/false
|
||||||
|
};
|
||||||
|
|
||||||
|
const updatePermission = async (fieldId: string, roleId: string, type: 'read' | 'edit', checked: boolean) => {
|
||||||
|
try {
|
||||||
|
saving.value = true;
|
||||||
|
|
||||||
|
// Get current permissions
|
||||||
|
if (!permissions.value.has(fieldId)) {
|
||||||
|
permissions.value.set(fieldId, new Map());
|
||||||
|
}
|
||||||
|
const fieldPerms = permissions.value.get(fieldId)!;
|
||||||
|
|
||||||
|
if (!fieldPerms.has(roleId)) {
|
||||||
|
fieldPerms.set(roleId, { canRead: true, canEdit: true });
|
||||||
|
}
|
||||||
|
const perm = fieldPerms.get(roleId)!;
|
||||||
|
|
||||||
|
// Update permission
|
||||||
|
if (type === 'read') {
|
||||||
|
perm.canRead = checked;
|
||||||
|
// If disabling read, also disable edit
|
||||||
|
if (!checked) {
|
||||||
|
perm.canEdit = false;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
perm.canEdit = checked;
|
||||||
|
// If enabling edit, also enable read
|
||||||
|
if (checked) {
|
||||||
|
perm.canRead = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save to backend
|
||||||
|
await api.put(`/setup/objects/${props.objectId}/field-permissions`, {
|
||||||
|
roleId,
|
||||||
|
fieldDefinitionId: fieldId,
|
||||||
|
canRead: perm.canRead,
|
||||||
|
canEdit: perm.canEdit,
|
||||||
|
});
|
||||||
|
|
||||||
|
toast.success('Permission updated');
|
||||||
|
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error('Failed to update field permission:', error);
|
||||||
|
toast.error(error.message || 'Failed to update permission');
|
||||||
|
|
||||||
|
// Revert change
|
||||||
|
if (!permissions.value.has(fieldId)) return;
|
||||||
|
const fieldPerms = permissions.value.get(fieldId)!;
|
||||||
|
if (!fieldPerms.has(roleId)) return;
|
||||||
|
const perm = fieldPerms.get(roleId)!;
|
||||||
|
if (type === 'read') {
|
||||||
|
perm.canRead = !checked;
|
||||||
|
} else {
|
||||||
|
perm.canEdit = !checked;
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
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>
|
||||||
@@ -43,20 +43,20 @@
|
|||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
<Card>
|
<FieldLevelSecurity
|
||||||
<CardHeader>
|
v-if="objectId && objectApiName && fields && fields.length > 0"
|
||||||
<CardTitle>Field-Level Security</CardTitle>
|
:object-id="objectId"
|
||||||
<CardDescription>
|
:object-api-name="objectApiName"
|
||||||
Control field visibility and editability by role (coming soon)
|
:fields="fields"
|
||||||
</CardDescription>
|
/>
|
||||||
</CardHeader>
|
|
||||||
<CardContent>
|
<div v-else-if="!objectId" class="text-sm text-muted-foreground">
|
||||||
<div class="text-sm text-muted-foreground">
|
Object ID not available
|
||||||
Field-level permissions will be managed through role configuration.
|
</div>
|
||||||
Navigate to Setup → Roles to configure field access for each role.
|
|
||||||
</div>
|
<div v-else-if="!fields || fields.length === 0" class="text-sm text-muted-foreground">
|
||||||
</CardContent>
|
No fields available
|
||||||
</Card>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -65,10 +65,13 @@ import { ref, watch } from 'vue';
|
|||||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '~/components/ui/card';
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '~/components/ui/card';
|
||||||
import { Label } from '~/components/ui/label';
|
import { Label } from '~/components/ui/label';
|
||||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '~/components/ui/select';
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '~/components/ui/select';
|
||||||
|
import FieldLevelSecurity from '~/components/FieldLevelSecurity.vue';
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
objectApiName: string;
|
objectApiName: string;
|
||||||
|
objectId?: string;
|
||||||
orgWideDefault?: string;
|
orgWideDefault?: string;
|
||||||
|
fields?: any[];
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
|
|||||||
@@ -60,7 +60,9 @@
|
|||||||
<TabsContent value="access" class="mt-6">
|
<TabsContent value="access" class="mt-6">
|
||||||
<ObjectAccessSettings
|
<ObjectAccessSettings
|
||||||
:object-api-name="object.apiName"
|
:object-api-name="object.apiName"
|
||||||
|
:object-id="object.id"
|
||||||
:org-wide-default="object.orgWideDefault"
|
:org-wide-default="object.orgWideDefault"
|
||||||
|
:fields="object.fields"
|
||||||
@update="handleAccessUpdate"
|
@update="handleAccessUpdate"
|
||||||
/>
|
/>
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
|
|||||||
Reference in New Issue
Block a user