WIP - permissions
This commit is contained in:
265
frontend/components/RolePermissionsEditor.vue
Normal file
265
frontend/components/RolePermissionsEditor.vue
Normal file
@@ -0,0 +1,265 @@
|
||||
<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>
|
||||
Reference in New Issue
Block a user