Files
neo/frontend/components/RolePermissionsEditor.vue
Francisco Gaona 88f656c3f5 WIP - permissions
2025-12-28 05:43:03 +01:00

266 lines
8.1 KiB
Vue

<template>
<div class="space-y-6">
<div v-if="loading" class="text-center py-8">Loading...</div>
<div v-else class="space-y-6">
<!-- Object Permissions -->
<div
v-for="obj in objects"
:key="obj.id"
class="border rounded-lg p-4 space-y-3"
>
<div class="flex items-center justify-between">
<h3 class="font-semibold">{{ obj.label }}</h3>
<Button
variant="ghost"
size="sm"
@click="toggleObjectExpanded(obj.id)"
>
{{ expandedObjects[obj.id] ? 'Collapse' : 'Expand' }}
</Button>
</div>
<div v-if="expandedObjects[obj.id]" class="space-y-4">
<!-- CRUD Permissions -->
<div class="grid grid-cols-2 gap-3">
<div class="flex items-center space-x-2">
<Checkbox
:id="`${obj.id}-read`"
:checked="hasPermission(obj.apiName, 'read')"
@update:checked="(val) => setPermission(obj.apiName, 'read', val)"
/>
<Label :for="`${obj.id}-read`" class="cursor-pointer">Read</Label>
</div>
<div class="flex items-center space-x-2">
<Checkbox
:id="`${obj.id}-create`"
:checked="hasPermission(obj.apiName, 'create')"
@update:checked="(val) => setPermission(obj.apiName, 'create', val)"
/>
<Label :for="`${obj.id}-create`" class="cursor-pointer">Create</Label>
</div>
<div class="flex items-center space-x-2">
<Checkbox
:id="`${obj.id}-update`"
:checked="hasPermission(obj.apiName, 'update')"
@update:checked="(val) => setPermission(obj.apiName, 'update', val)"
/>
<Label :for="`${obj.id}-update`" class="cursor-pointer">Update</Label>
</div>
<div class="flex items-center space-x-2">
<Checkbox
:id="`${obj.id}-delete`"
:checked="hasPermission(obj.apiName, 'delete')"
@update:checked="(val) => setPermission(obj.apiName, 'delete', val)"
/>
<Label :for="`${obj.id}-delete`" class="cursor-pointer">Delete</Label>
</div>
</div>
<!-- Advanced: Condition-based permissions -->
<div class="border-t pt-3">
<div class="flex items-center space-x-2 mb-2">
<Checkbox
:id="`${obj.id}-conditions`"
:checked="hasConditions(obj.apiName)"
@update:checked="(val) => toggleConditions(obj.apiName, val)"
/>
<Label :for="`${obj.id}-conditions`" class="cursor-pointer text-sm">
Apply conditions (e.g., own records only)
</Label>
</div>
<div v-if="hasConditions(obj.apiName)" class="ml-6 space-y-2">
<div class="text-sm text-muted-foreground">
Only allow access to records where:
</div>
<div class="flex gap-2">
<Input
v-model="getConditions(obj.apiName).field"
placeholder="Field name (e.g., ownerId)"
class="flex-1"
/>
<Input
v-model="getConditions(obj.apiName).value"
placeholder="Value (e.g., $userId)"
class="flex-1"
/>
</div>
</div>
</div>
</div>
</div>
<!-- Save Button -->
<div class="flex justify-end gap-2">
<Button variant="outline" @click="$emit('cancel')">Cancel</Button>
<Button @click="savePermissions" :disabled="saving">
{{ saving ? 'Saving...' : 'Save Permissions' }}
</Button>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label'
import { Checkbox } from '@/components/ui/checkbox'
interface Props {
role: any
}
const props = defineProps<Props>()
const emit = defineEmits(['saved', 'cancel'])
const { api } = useApi()
const { toast } = useToast()
const loading = ref(true)
const saving = ref(false)
const objects = ref<any[]>([])
const expandedObjects = ref<Record<string, boolean>>({})
// Store permissions as CASL-like rules
const permissions = ref<Record<string, {
actions: string[]
conditions?: any
}>>({})
const fetchObjects = async () => {
try {
loading.value = true
objects.value = await api.get('/setup/objects')
// Expand all objects by default
objects.value.forEach(obj => {
expandedObjects.value[obj.id] = true
})
} catch (e: any) {
console.error('Error fetching objects:', e)
} finally {
loading.value = false
}
}
const fetchRolePermissions = async () => {
try {
const rules = await api.get(`/role-rules/role/${props.role.id}`)
// Parse existing rules into our format
if (rules && rules.length > 0 && rules[0].rulesJson) {
const rulesJson = rules[0].rulesJson
rulesJson.forEach((rule: any) => {
if (!permissions.value[rule.subject]) {
permissions.value[rule.subject] = { actions: [] }
}
if (Array.isArray(rule.action)) {
permissions.value[rule.subject].actions.push(...rule.action)
} else {
permissions.value[rule.subject].actions.push(rule.action)
}
if (rule.conditions) {
permissions.value[rule.subject].conditions = rule.conditions
}
})
}
} catch (e: any) {
console.error('Error fetching role permissions:', e)
}
}
const toggleObjectExpanded = (objectId: string) => {
expandedObjects.value[objectId] = !expandedObjects.value[objectId]
}
const hasPermission = (subject: string, action: string): boolean => {
return permissions.value[subject]?.actions.includes(action) || false
}
const setPermission = (subject: string, action: string, value: boolean) => {
if (!permissions.value[subject]) {
permissions.value[subject] = { actions: [] }
}
if (value) {
if (!permissions.value[subject].actions.includes(action)) {
permissions.value[subject].actions.push(action)
}
} else {
permissions.value[subject].actions = permissions.value[subject].actions.filter(a => a !== action)
}
}
const hasConditions = (subject: string): boolean => {
return !!permissions.value[subject]?.conditions
}
const toggleConditions = (subject: string, value: boolean) => {
if (!permissions.value[subject]) {
permissions.value[subject] = { actions: [] }
}
if (value) {
permissions.value[subject].conditions = { field: 'ownerId', value: '$userId' }
} else {
delete permissions.value[subject].conditions
}
}
const getConditions = (subject: string) => {
if (!permissions.value[subject]?.conditions) {
return { field: '', value: '' }
}
const cond = permissions.value[subject].conditions
// Convert CASL condition format to simple field/value
const field = Object.keys(cond)[0] || ''
const value = cond[field] || ''
return { field, value }
}
const savePermissions = async () => {
try {
saving.value = true
// Convert our permission structure to CASL rules format
const rules: any[] = []
Object.entries(permissions.value).forEach(([subject, perm]) => {
if (perm.actions.length > 0) {
const rule: any = {
action: perm.actions,
subject,
}
if (perm.conditions) {
const cond = getConditions(subject)
if (cond.field && cond.value) {
rule.conditions = { [cond.field]: cond.value }
}
}
rules.push(rule)
}
})
await api.post('/role-rules', {
roleId: props.role.id,
rulesJson: rules,
})
emit('saved')
} catch (e: any) {
console.error('Error saving permissions:', e)
toast.error('Failed to save permissions')
} finally {
saving.value = false
}
}
onMounted(async () => {
await fetchObjects()
await fetchRolePermissions()
})
</script>