374 lines
11 KiB
Vue
374 lines
11 KiB
Vue
<template>
|
|
<div class="space-y-4">
|
|
<!-- Existing Shares List -->
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle>Current Shares</CardTitle>
|
|
<CardDescription>Users who have access to this record</CardDescription>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<div v-if="loading" class="flex justify-center py-8">
|
|
<div class="animate-spin h-8 w-8 border-4 border-primary border-t-transparent rounded-full"></div>
|
|
</div>
|
|
<div v-else-if="shares.length === 0" class="text-center py-8 text-muted-foreground">
|
|
No shares yet. Click "Add Share" to share this record.
|
|
</div>
|
|
<div v-else class="space-y-2">
|
|
<div
|
|
v-for="share in shares"
|
|
:key="share.id"
|
|
class="flex items-center justify-between p-3 border rounded-lg"
|
|
>
|
|
<div class="flex-1">
|
|
<div class="font-medium">{{ getUserName(share.granteeUser) }}</div>
|
|
<div class="text-sm text-muted-foreground">
|
|
Access: {{ formatActions(share.actions) }}
|
|
<span v-if="share.fields && share.fields.length > 0">
|
|
• Fields: {{ share.fields.join(', ') }}
|
|
</span>
|
|
<span v-if="share.expiresAt">
|
|
• Expires: {{ formatDate(share.expiresAt) }}
|
|
</span>
|
|
</div>
|
|
<div class="text-xs text-muted-foreground mt-1">
|
|
Granted by {{ getUserName(share.grantedByUser) }}
|
|
on {{ formatDate(share.createdAt) }}
|
|
</div>
|
|
</div>
|
|
<Button
|
|
variant="ghost"
|
|
size="sm"
|
|
@click="revokeShare(share.id)"
|
|
:disabled="revoking === share.id"
|
|
>
|
|
<Trash2 class="h-4 w-4" />
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
|
|
<!-- Add New Share -->
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle>Add Share</CardTitle>
|
|
<CardDescription>Grant access to another user</CardDescription>
|
|
</CardHeader>
|
|
<CardContent class="space-y-4">
|
|
<!-- User Selection -->
|
|
<div class="space-y-2">
|
|
<Label for="user">User</Label>
|
|
<select
|
|
id="user"
|
|
v-model="newShare.userId"
|
|
class="w-full px-3 py-2 border rounded-md bg-background"
|
|
>
|
|
<option value="">Select a user</option>
|
|
<option
|
|
v-for="user in availableUsers"
|
|
:key="user.id"
|
|
:value="user.id"
|
|
>
|
|
{{ user.name }}
|
|
</option>
|
|
</select>
|
|
</div>
|
|
|
|
<!-- Access Level -->
|
|
<div class="space-y-2">
|
|
<Label>Access Level</Label>
|
|
<div class="space-y-2">
|
|
<div class="flex items-center space-x-2">
|
|
<input
|
|
type="checkbox"
|
|
id="share-read"
|
|
v-model="newShare.canRead"
|
|
:disabled="!canGrantRead"
|
|
class="rounded border-gray-300"
|
|
/>
|
|
<Label
|
|
for="share-read"
|
|
class="font-normal cursor-pointer"
|
|
:class="{ 'text-muted-foreground': !canGrantRead }"
|
|
>
|
|
Read
|
|
<span v-if="!canGrantRead" class="text-xs">(You don't have read permission)</span>
|
|
</Label>
|
|
</div>
|
|
<div class="flex items-center space-x-2">
|
|
<input
|
|
type="checkbox"
|
|
id="share-update"
|
|
v-model="newShare.canUpdate"
|
|
:disabled="!canGrantUpdate"
|
|
class="rounded border-gray-300"
|
|
/>
|
|
<Label
|
|
for="share-update"
|
|
class="font-normal cursor-pointer"
|
|
:class="{ 'text-muted-foreground': !canGrantUpdate }"
|
|
>
|
|
Update
|
|
<span v-if="!canGrantUpdate" class="text-xs">(You don't have update permission)</span>
|
|
</Label>
|
|
</div>
|
|
<div class="flex items-center space-x-2">
|
|
<input
|
|
type="checkbox"
|
|
id="share-delete"
|
|
v-model="newShare.canDelete"
|
|
:disabled="!canGrantDelete"
|
|
class="rounded border-gray-300"
|
|
/>
|
|
<Label
|
|
for="share-delete"
|
|
class="font-normal cursor-pointer"
|
|
:class="{ 'text-muted-foreground': !canGrantDelete }"
|
|
>
|
|
Delete
|
|
<span v-if="!canGrantDelete" class="text-xs">(You don't have delete permission)</span>
|
|
</Label>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Field-Level Access (Optional) -->
|
|
<div class="space-y-2">
|
|
<div class="flex items-center space-x-2">
|
|
<input
|
|
type="checkbox"
|
|
id="limit-fields"
|
|
v-model="newShare.limitFields"
|
|
class="rounded border-gray-300"
|
|
/>
|
|
<Label for="limit-fields" class="font-normal cursor-pointer">
|
|
Limit access to specific fields
|
|
</Label>
|
|
</div>
|
|
|
|
<div v-if="newShare.limitFields" class="ml-6 space-y-2 mt-2">
|
|
<Label>Select Fields</Label>
|
|
<div class="space-y-1 max-h-48 overflow-y-auto border rounded p-2">
|
|
<div
|
|
v-for="field in availableFields"
|
|
:key="field.apiName"
|
|
class="flex items-center space-x-2"
|
|
>
|
|
<input
|
|
type="checkbox"
|
|
:id="`field-${field.apiName}`"
|
|
:value="field.apiName"
|
|
v-model="newShare.selectedFields"
|
|
class="rounded border-gray-300"
|
|
/>
|
|
<Label :for="`field-${field.apiName}`" class="font-normal cursor-pointer">
|
|
{{ field.label }}
|
|
</Label>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Expiration (Optional) -->
|
|
<div class="space-y-2">
|
|
<div class="flex items-center space-x-2">
|
|
<input
|
|
type="checkbox"
|
|
id="set-expiration"
|
|
v-model="newShare.hasExpiration"
|
|
class="rounded border-gray-300"
|
|
/>
|
|
<Label for="set-expiration" class="font-normal cursor-pointer">
|
|
Set expiration date
|
|
</Label>
|
|
</div>
|
|
|
|
<div v-if="newShare.hasExpiration" class="ml-6">
|
|
<input
|
|
type="datetime-local"
|
|
v-model="newShare.expiresAt"
|
|
class="w-full px-3 py-2 border rounded-md bg-background"
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<Button
|
|
@click="createShare"
|
|
:disabled="!canCreateShare || creating"
|
|
class="w-full"
|
|
>
|
|
<Share2 class="h-4 w-4 mr-2" />
|
|
{{ creating ? 'Creating...' : 'Add Share' }}
|
|
</Button>
|
|
</CardContent>
|
|
</Card>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
import { ref, computed, onMounted } from 'vue'
|
|
import { Share2, Trash2 } from 'lucide-vue-next'
|
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
|
|
import { Button } from '@/components/ui/button'
|
|
import { Label } from '@/components/ui/label'
|
|
import { useApi } from '@/composables/useApi'
|
|
import { useToast } from '@/composables/useToast'
|
|
|
|
interface Props {
|
|
objectApiName: string
|
|
recordId: string
|
|
currentUserPermissions: {
|
|
canRead: boolean
|
|
canUpdate: boolean
|
|
canDelete: boolean
|
|
}
|
|
fields: Array<{ apiName: string; label: string }>
|
|
}
|
|
|
|
const props = defineProps<Props>()
|
|
const { api } = useApi()
|
|
const { showToast } = useToast()
|
|
|
|
const shares = ref<any[]>([])
|
|
const loading = ref(true)
|
|
const revoking = ref<string | null>(null)
|
|
const creating = ref(false)
|
|
const availableUsers = ref<any[]>([])
|
|
|
|
const newShare = ref({
|
|
userId: '',
|
|
canRead: true,
|
|
canUpdate: false,
|
|
canDelete: false,
|
|
limitFields: false,
|
|
selectedFields: [] as string[],
|
|
hasExpiration: false,
|
|
expiresAt: ''
|
|
})
|
|
|
|
const canGrantRead = computed(() => props.currentUserPermissions.canRead)
|
|
const canGrantUpdate = computed(() => props.currentUserPermissions.canUpdate)
|
|
const canGrantDelete = computed(() => props.currentUserPermissions.canDelete)
|
|
|
|
const availableFields = computed(() => {
|
|
return props.fields.filter(f => !['id', 'created_at', 'updated_at', 'ownerId'].includes(f.apiName))
|
|
})
|
|
|
|
const canCreateShare = computed(() => {
|
|
return newShare.value.userId &&
|
|
(newShare.value.canRead || newShare.value.canUpdate || newShare.value.canDelete)
|
|
})
|
|
|
|
const getUserName = (user: any) => {
|
|
if (!user) return 'Unknown'
|
|
return user.name || user.email || 'Unknown'
|
|
}
|
|
|
|
const formatActions = (actions: string[]) => {
|
|
return actions.map(a => a.charAt(0).toUpperCase() + a.slice(1)).join(', ')
|
|
}
|
|
|
|
const formatDate = (date: string) => {
|
|
return new Date(date).toLocaleDateString('en-US', {
|
|
year: 'numeric',
|
|
month: 'short',
|
|
day: 'numeric',
|
|
hour: '2-digit',
|
|
minute: '2-digit'
|
|
})
|
|
}
|
|
|
|
const fetchShares = async () => {
|
|
loading.value = true
|
|
try {
|
|
const response = await api.get(`/rbac/shares/${props.objectApiName}/${props.recordId}`)
|
|
shares.value = response
|
|
} catch (error) {
|
|
console.error('Failed to fetch shares:', error)
|
|
showToast('Failed to load shares', 'error')
|
|
} finally {
|
|
loading.value = false
|
|
}
|
|
}
|
|
|
|
const fetchAvailableUsers = async () => {
|
|
try {
|
|
const response = await api.get('/rbac/users')
|
|
availableUsers.value = response
|
|
} catch (error) {
|
|
console.error('Failed to fetch users:', error)
|
|
}
|
|
}
|
|
|
|
const createShare = async () => {
|
|
creating.value = true
|
|
try {
|
|
const actions: string[] = []
|
|
if (newShare.value.canRead) actions.push('read')
|
|
if (newShare.value.canUpdate) actions.push('update')
|
|
if (newShare.value.canDelete) actions.push('delete')
|
|
|
|
const payload: any = {
|
|
objectApiName: props.objectApiName,
|
|
recordId: props.recordId,
|
|
granteeUserId: newShare.value.userId,
|
|
actions
|
|
}
|
|
|
|
if (newShare.value.limitFields && newShare.value.selectedFields.length > 0) {
|
|
payload.fields = newShare.value.selectedFields
|
|
}
|
|
|
|
if (newShare.value.hasExpiration && newShare.value.expiresAt) {
|
|
payload.expiresAt = new Date(newShare.value.expiresAt).toISOString()
|
|
}
|
|
|
|
await api.post('/rbac/shares', payload)
|
|
|
|
showToast('Share created successfully', 'success')
|
|
|
|
// Reset form
|
|
newShare.value = {
|
|
userId: '',
|
|
canRead: true,
|
|
canUpdate: false,
|
|
canDelete: false,
|
|
limitFields: false,
|
|
selectedFields: [],
|
|
hasExpiration: false,
|
|
expiresAt: ''
|
|
}
|
|
|
|
await fetchShares()
|
|
} catch (error: any) {
|
|
console.error('Failed to create share:', error)
|
|
showToast(error.message || 'Failed to create share', 'error')
|
|
} finally {
|
|
creating.value = false
|
|
}
|
|
}
|
|
|
|
const revokeShare = async (shareId: string) => {
|
|
if (!confirm('Are you sure you want to revoke this share?')) return
|
|
|
|
revoking.value = shareId
|
|
try {
|
|
await api.delete(`/rbac/shares/${shareId}`)
|
|
|
|
showToast('Share revoked successfully', 'success')
|
|
|
|
await fetchShares()
|
|
} catch (error: any) {
|
|
console.error('Failed to revoke share:', error)
|
|
showToast(error.message || 'Failed to revoke share', 'error')
|
|
} finally {
|
|
revoking.value = null
|
|
}
|
|
}
|
|
|
|
onMounted(() => {
|
|
fetchShares()
|
|
fetchAvailableUsers()
|
|
})
|
|
</script>
|