266 lines
8.1 KiB
Vue
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>
|