Adds permission management components
Implements components for granting and revoking account permissions. This introduces a `GrantPermissionDialog` for assigning access rights to users, and a `PermissionManager` component to list and revoke existing permissions. The UI provides options to view permissions grouped by user or by account.
This commit is contained in:
parent
e745caffaa
commit
6ecaafb633
2 changed files with 655 additions and 0 deletions
256
src/modules/expenses/components/admin/GrantPermissionDialog.vue
Normal file
256
src/modules/expenses/components/admin/GrantPermissionDialog.vue
Normal file
|
|
@ -0,0 +1,256 @@
|
|||
<script setup lang="ts">
|
||||
import { ref, computed } from 'vue'
|
||||
import { useForm } from 'vee-validate'
|
||||
import { toTypedSchema } from '@vee-validate/zod'
|
||||
import * as z from 'zod'
|
||||
import { useAuth } from '@/composables/useAuthService'
|
||||
import { useToast } from '@/core/composables/useToast'
|
||||
import { injectService, SERVICE_TOKENS } from '@/core/di-container'
|
||||
import type { ExpensesAPI } from '../../services/ExpensesAPI'
|
||||
import type { Account } from '../../types'
|
||||
import { PermissionType } from '../../types'
|
||||
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from '@/components/ui/dialog'
|
||||
import {
|
||||
FormControl,
|
||||
FormDescription,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage
|
||||
} from '@/components/ui/form'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'
|
||||
import { Textarea } from '@/components/ui/textarea'
|
||||
import { Loader2 } from 'lucide-vue-next'
|
||||
|
||||
interface Props {
|
||||
isOpen: boolean
|
||||
accounts: Account[]
|
||||
}
|
||||
|
||||
const props = defineProps<Props>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
close: []
|
||||
permissionGranted: []
|
||||
}>()
|
||||
|
||||
const { user } = useAuth()
|
||||
const toast = useToast()
|
||||
const expensesAPI = injectService<ExpensesAPI>(SERVICE_TOKENS.EXPENSES_API)
|
||||
|
||||
const isGranting = ref(false)
|
||||
|
||||
const adminKey = computed(() => user.value?.wallets?.[0]?.adminkey)
|
||||
|
||||
// Form schema
|
||||
const formSchema = toTypedSchema(
|
||||
z.object({
|
||||
user_id: z.string().min(1, 'User ID is required'),
|
||||
account_id: z.string().min(1, 'Account is required'),
|
||||
permission_type: z.nativeEnum(PermissionType, {
|
||||
errorMap: () => ({ message: 'Permission type is required' })
|
||||
}),
|
||||
notes: z.string().optional(),
|
||||
expires_at: z.string().optional()
|
||||
})
|
||||
)
|
||||
|
||||
// Setup form
|
||||
const form = useForm({
|
||||
validationSchema: formSchema,
|
||||
initialValues: {
|
||||
user_id: '',
|
||||
account_id: '',
|
||||
permission_type: PermissionType.READ,
|
||||
notes: '',
|
||||
expires_at: ''
|
||||
}
|
||||
})
|
||||
|
||||
const { resetForm, meta } = form
|
||||
const isFormValid = computed(() => meta.value.valid)
|
||||
|
||||
// Permission type options
|
||||
const permissionTypes = [
|
||||
{ value: PermissionType.READ, label: 'Read', description: 'View account and balance' },
|
||||
{
|
||||
value: PermissionType.SUBMIT_EXPENSE,
|
||||
label: 'Submit Expense',
|
||||
description: 'Submit expenses to this account'
|
||||
},
|
||||
{ value: PermissionType.MANAGE, label: 'Manage', description: 'Full account management' }
|
||||
]
|
||||
|
||||
// Submit form
|
||||
const onSubmit = form.handleSubmit(async (values) => {
|
||||
if (!adminKey.value) {
|
||||
toast.error('Admin access required')
|
||||
return
|
||||
}
|
||||
|
||||
isGranting.value = true
|
||||
|
||||
try {
|
||||
await expensesAPI.grantPermission(adminKey.value, {
|
||||
user_id: values.user_id,
|
||||
account_id: values.account_id,
|
||||
permission_type: values.permission_type,
|
||||
notes: values.notes || undefined,
|
||||
expires_at: values.expires_at || undefined
|
||||
})
|
||||
|
||||
emit('permissionGranted')
|
||||
resetForm()
|
||||
} catch (error) {
|
||||
console.error('Failed to grant permission:', error)
|
||||
toast.error('Failed to grant permission', {
|
||||
description: error instanceof Error ? error.message : 'Unknown error'
|
||||
})
|
||||
} finally {
|
||||
isGranting.value = false
|
||||
}
|
||||
})
|
||||
|
||||
// Handle dialog close
|
||||
function handleClose() {
|
||||
if (!isGranting.value) {
|
||||
resetForm()
|
||||
emit('close')
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Dialog :open="props.isOpen" @update:open="handleClose">
|
||||
<DialogContent class="sm:max-w-[500px]">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Grant Account Permission</DialogTitle>
|
||||
<DialogDescription>
|
||||
Grant a user permission to access an expense account. Permissions on parent accounts
|
||||
cascade to children.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<form @submit="onSubmit" class="space-y-4">
|
||||
<!-- User ID -->
|
||||
<FormField v-slot="{ componentField }" name="user_id">
|
||||
<FormItem>
|
||||
<FormLabel>User ID *</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
placeholder="Enter user wallet ID"
|
||||
v-bind="componentField"
|
||||
:disabled="isGranting"
|
||||
/>
|
||||
</FormControl>
|
||||
<FormDescription>The wallet ID of the user to grant permission to</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
</FormField>
|
||||
|
||||
<!-- Account -->
|
||||
<FormField v-slot="{ componentField }" name="account_id">
|
||||
<FormItem>
|
||||
<FormLabel>Account *</FormLabel>
|
||||
<Select v-bind="componentField" :disabled="isGranting">
|
||||
<FormControl>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Select account" />
|
||||
</SelectTrigger>
|
||||
</FormControl>
|
||||
<SelectContent>
|
||||
<SelectItem
|
||||
v-for="account in props.accounts"
|
||||
:key="account.id"
|
||||
:value="account.id"
|
||||
>
|
||||
{{ account.name }}
|
||||
</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<FormDescription>Account to grant access to</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
</FormField>
|
||||
|
||||
<!-- Permission Type -->
|
||||
<FormField v-slot="{ componentField }" name="permission_type">
|
||||
<FormItem>
|
||||
<FormLabel>Permission Type *</FormLabel>
|
||||
<Select v-bind="componentField" :disabled="isGranting">
|
||||
<FormControl>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Select permission type" />
|
||||
</SelectTrigger>
|
||||
</FormControl>
|
||||
<SelectContent>
|
||||
<SelectItem
|
||||
v-for="type in permissionTypes"
|
||||
:key="type.value"
|
||||
:value="type.value"
|
||||
>
|
||||
<div>
|
||||
<div class="font-medium">{{ type.label }}</div>
|
||||
<div class="text-sm text-muted-foreground">{{ type.description }}</div>
|
||||
</div>
|
||||
</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<FormDescription>Type of permission to grant</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
</FormField>
|
||||
|
||||
<!-- Expiration Date (Optional) -->
|
||||
<FormField v-slot="{ componentField }" name="expires_at">
|
||||
<FormItem>
|
||||
<FormLabel>Expiration Date (Optional)</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
type="datetime-local"
|
||||
v-bind="componentField"
|
||||
:disabled="isGranting"
|
||||
/>
|
||||
</FormControl>
|
||||
<FormDescription>Leave empty for permanent access</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
</FormField>
|
||||
|
||||
<!-- Notes (Optional) -->
|
||||
<FormField v-slot="{ componentField }" name="notes">
|
||||
<FormItem>
|
||||
<FormLabel>Notes (Optional)</FormLabel>
|
||||
<FormControl>
|
||||
<Textarea
|
||||
placeholder="Add notes about this permission..."
|
||||
v-bind="componentField"
|
||||
:disabled="isGranting"
|
||||
/>
|
||||
</FormControl>
|
||||
<FormDescription>Optional notes for admin reference</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
</FormField>
|
||||
|
||||
<DialogFooter>
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
@click="handleClose"
|
||||
:disabled="isGranting"
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button type="submit" :disabled="isGranting || !isFormValid">
|
||||
<Loader2 v-if="isGranting" class="mr-2 h-4 w-4 animate-spin" />
|
||||
{{ isGranting ? 'Granting...' : 'Grant Permission' }}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</form>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</template>
|
||||
399
src/modules/expenses/components/admin/PermissionManager.vue
Normal file
399
src/modules/expenses/components/admin/PermissionManager.vue
Normal file
|
|
@ -0,0 +1,399 @@
|
|||
<script setup lang="ts">
|
||||
import { ref, onMounted, computed } from 'vue'
|
||||
import { useAuth } from '@/composables/useAuthService'
|
||||
import { useToast } from '@/core/composables/useToast'
|
||||
import { injectService, SERVICE_TOKENS } from '@/core/di-container'
|
||||
import type { ExpensesAPI } from '../../services/ExpensesAPI'
|
||||
import type { AccountPermission, Account } from '../../types'
|
||||
import { PermissionType } from '../../types'
|
||||
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
import { Loader2, Plus, Trash2, Users, Shield } from 'lucide-vue-next'
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow
|
||||
} from '@/components/ui/table'
|
||||
import {
|
||||
AlertDialog,
|
||||
AlertDialogAction,
|
||||
AlertDialogCancel,
|
||||
AlertDialogContent,
|
||||
AlertDialogDescription,
|
||||
AlertDialogFooter,
|
||||
AlertDialogHeader,
|
||||
AlertDialogTitle
|
||||
} from '@/components/ui/alert-dialog'
|
||||
|
||||
import GrantPermissionDialog from './GrantPermissionDialog.vue'
|
||||
|
||||
const { user } = useAuth()
|
||||
const toast = useToast()
|
||||
const expensesAPI = injectService<ExpensesAPI>(SERVICE_TOKENS.EXPENSES_API)
|
||||
|
||||
const permissions = ref<AccountPermission[]>([])
|
||||
const accounts = ref<Account[]>([])
|
||||
const isLoading = ref(false)
|
||||
const showGrantDialog = ref(false)
|
||||
const permissionToRevoke = ref<AccountPermission | null>(null)
|
||||
const showRevokeDialog = ref(false)
|
||||
|
||||
const adminKey = computed(() => user.value?.wallets?.[0]?.adminkey)
|
||||
|
||||
// Get permission type badge variant
|
||||
function getPermissionBadge(type: PermissionType) {
|
||||
switch (type) {
|
||||
case PermissionType.READ:
|
||||
return 'default'
|
||||
case PermissionType.SUBMIT_EXPENSE:
|
||||
return 'secondary'
|
||||
case PermissionType.MANAGE:
|
||||
return 'destructive'
|
||||
default:
|
||||
return 'outline'
|
||||
}
|
||||
}
|
||||
|
||||
// Get permission type label
|
||||
function getPermissionLabel(type: PermissionType): string {
|
||||
switch (type) {
|
||||
case PermissionType.READ:
|
||||
return 'Read'
|
||||
case PermissionType.SUBMIT_EXPENSE:
|
||||
return 'Submit Expense'
|
||||
case PermissionType.MANAGE:
|
||||
return 'Manage'
|
||||
default:
|
||||
return type
|
||||
}
|
||||
}
|
||||
|
||||
// Get account name by ID
|
||||
function getAccountName(accountId: string): string {
|
||||
const account = accounts.value.find((a) => a.id === accountId)
|
||||
return account?.name || accountId
|
||||
}
|
||||
|
||||
// Format date
|
||||
function formatDate(dateString: string): string {
|
||||
const date = new Date(dateString)
|
||||
return date.toLocaleDateString() + ' ' + date.toLocaleTimeString()
|
||||
}
|
||||
|
||||
// Load accounts
|
||||
async function loadAccounts() {
|
||||
if (!adminKey.value) return
|
||||
|
||||
try {
|
||||
accounts.value = await expensesAPI.getAccounts(adminKey.value)
|
||||
} catch (error) {
|
||||
console.error('Failed to load accounts:', error)
|
||||
toast.error('Failed to load accounts', {
|
||||
description: error instanceof Error ? error.message : 'Unknown error'
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Load all permissions
|
||||
async function loadPermissions() {
|
||||
if (!adminKey.value) {
|
||||
toast.error('Admin access required', {
|
||||
description: 'You need admin privileges to manage permissions'
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
isLoading.value = true
|
||||
try {
|
||||
permissions.value = await expensesAPI.listPermissions(adminKey.value)
|
||||
} catch (error) {
|
||||
console.error('Failed to load permissions:', error)
|
||||
toast.error('Failed to load permissions', {
|
||||
description: error instanceof Error ? error.message : 'Unknown error'
|
||||
})
|
||||
} finally {
|
||||
isLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// Handle permission granted
|
||||
function handlePermissionGranted() {
|
||||
showGrantDialog.value = false
|
||||
loadPermissions()
|
||||
toast.success('Permission granted', {
|
||||
description: 'User permission has been successfully granted'
|
||||
})
|
||||
}
|
||||
|
||||
// Confirm revoke permission
|
||||
function confirmRevoke(permission: AccountPermission) {
|
||||
permissionToRevoke.value = permission
|
||||
showRevokeDialog.value = true
|
||||
}
|
||||
|
||||
// Revoke permission
|
||||
async function revokePermission() {
|
||||
if (!adminKey.value || !permissionToRevoke.value) return
|
||||
|
||||
try {
|
||||
await expensesAPI.revokePermission(adminKey.value, permissionToRevoke.value.id)
|
||||
toast.success('Permission revoked', {
|
||||
description: 'User permission has been successfully revoked'
|
||||
})
|
||||
loadPermissions()
|
||||
} catch (error) {
|
||||
console.error('Failed to revoke permission:', error)
|
||||
toast.error('Failed to revoke permission', {
|
||||
description: error instanceof Error ? error.message : 'Unknown error'
|
||||
})
|
||||
} finally {
|
||||
showRevokeDialog.value = false
|
||||
permissionToRevoke.value = null
|
||||
}
|
||||
}
|
||||
|
||||
// Group permissions by user
|
||||
const permissionsByUser = computed(() => {
|
||||
const grouped = new Map<string, AccountPermission[]>()
|
||||
|
||||
for (const permission of permissions.value) {
|
||||
const existing = grouped.get(permission.user_id) || []
|
||||
existing.push(permission)
|
||||
grouped.set(permission.user_id, existing)
|
||||
}
|
||||
|
||||
return grouped
|
||||
})
|
||||
|
||||
// Group permissions by account
|
||||
const permissionsByAccount = computed(() => {
|
||||
const grouped = new Map<string, AccountPermission[]>()
|
||||
|
||||
for (const permission of permissions.value) {
|
||||
const existing = grouped.get(permission.account_id) || []
|
||||
existing.push(permission)
|
||||
grouped.set(permission.account_id, existing)
|
||||
}
|
||||
|
||||
return grouped
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
loadAccounts()
|
||||
loadPermissions()
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="container mx-auto p-6 space-y-6">
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<h1 class="text-3xl font-bold">Permission Management</h1>
|
||||
<p class="text-muted-foreground">
|
||||
Manage user access to expense accounts
|
||||
</p>
|
||||
</div>
|
||||
<Button @click="showGrantDialog = true" :disabled="isLoading">
|
||||
<Plus class="mr-2 h-4 w-4" />
|
||||
Grant Permission
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle class="flex items-center gap-2">
|
||||
<Shield class="h-5 w-5" />
|
||||
Account Permissions
|
||||
</CardTitle>
|
||||
<CardDescription>
|
||||
View and manage all account permissions. Permissions on parent accounts cascade to
|
||||
children.
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<Tabs default-value="by-user" class="w-full">
|
||||
<TabsList class="grid w-full grid-cols-2">
|
||||
<TabsTrigger value="by-user">
|
||||
<Users class="mr-2 h-4 w-4" />
|
||||
By User
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="by-account">
|
||||
<Shield class="mr-2 h-4 w-4" />
|
||||
By Account
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
<!-- By User View -->
|
||||
<TabsContent value="by-user" class="space-y-4">
|
||||
<div v-if="isLoading" class="flex items-center justify-center py-8">
|
||||
<Loader2 class="h-8 w-8 animate-spin text-muted-foreground" />
|
||||
</div>
|
||||
|
||||
<div v-else-if="permissionsByUser.size === 0" class="text-center py-8">
|
||||
<p class="text-muted-foreground">No permissions granted yet</p>
|
||||
</div>
|
||||
|
||||
<div v-else class="space-y-4">
|
||||
<div
|
||||
v-for="[userId, userPermissions] in permissionsByUser"
|
||||
:key="userId"
|
||||
class="border rounded-lg p-4"
|
||||
>
|
||||
<h3 class="font-semibold mb-2">User: {{ userId }}</h3>
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>Account</TableHead>
|
||||
<TableHead>Permission</TableHead>
|
||||
<TableHead>Granted</TableHead>
|
||||
<TableHead>Expires</TableHead>
|
||||
<TableHead>Notes</TableHead>
|
||||
<TableHead class="text-right">Actions</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
<TableRow v-for="permission in userPermissions" :key="permission.id">
|
||||
<TableCell class="font-medium">
|
||||
{{ getAccountName(permission.account_id) }}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Badge :variant="getPermissionBadge(permission.permission_type)">
|
||||
{{ getPermissionLabel(permission.permission_type) }}
|
||||
</Badge>
|
||||
</TableCell>
|
||||
<TableCell>{{ formatDate(permission.granted_at) }}</TableCell>
|
||||
<TableCell>
|
||||
{{ permission.expires_at ? formatDate(permission.expires_at) : 'Never' }}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<span class="text-sm text-muted-foreground">
|
||||
{{ permission.notes || '-' }}
|
||||
</span>
|
||||
</TableCell>
|
||||
<TableCell class="text-right">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
@click="confirmRevoke(permission)"
|
||||
:disabled="isLoading"
|
||||
>
|
||||
<Trash2 class="h-4 w-4 text-destructive" />
|
||||
</Button>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
</div>
|
||||
</TabsContent>
|
||||
|
||||
<!-- By Account View -->
|
||||
<TabsContent value="by-account" class="space-y-4">
|
||||
<div v-if="isLoading" class="flex items-center justify-center py-8">
|
||||
<Loader2 class="h-8 w-8 animate-spin text-muted-foreground" />
|
||||
</div>
|
||||
|
||||
<div v-else-if="permissionsByAccount.size === 0" class="text-center py-8">
|
||||
<p class="text-muted-foreground">No permissions granted yet</p>
|
||||
</div>
|
||||
|
||||
<div v-else class="space-y-4">
|
||||
<div
|
||||
v-for="[accountId, accountPermissions] in permissionsByAccount"
|
||||
:key="accountId"
|
||||
class="border rounded-lg p-4"
|
||||
>
|
||||
<h3 class="font-semibold mb-2">Account: {{ getAccountName(accountId) }}</h3>
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>User</TableHead>
|
||||
<TableHead>Permission</TableHead>
|
||||
<TableHead>Granted</TableHead>
|
||||
<TableHead>Expires</TableHead>
|
||||
<TableHead>Notes</TableHead>
|
||||
<TableHead class="text-right">Actions</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
<TableRow v-for="permission in accountPermissions" :key="permission.id">
|
||||
<TableCell class="font-medium">{{ permission.user_id }}</TableCell>
|
||||
<TableCell>
|
||||
<Badge :variant="getPermissionBadge(permission.permission_type)">
|
||||
{{ getPermissionLabel(permission.permission_type) }}
|
||||
</Badge>
|
||||
</TableCell>
|
||||
<TableCell>{{ formatDate(permission.granted_at) }}</TableCell>
|
||||
<TableCell>
|
||||
{{ permission.expires_at ? formatDate(permission.expires_at) : 'Never' }}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<span class="text-sm text-muted-foreground">
|
||||
{{ permission.notes || '-' }}
|
||||
</span>
|
||||
</TableCell>
|
||||
<TableCell class="text-right">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
@click="confirmRevoke(permission)"
|
||||
:disabled="isLoading"
|
||||
>
|
||||
<Trash2 class="h-4 w-4 text-destructive" />
|
||||
</Button>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
</div>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<!-- Grant Permission Dialog -->
|
||||
<GrantPermissionDialog
|
||||
:is-open="showGrantDialog"
|
||||
:accounts="accounts"
|
||||
@close="showGrantDialog = false"
|
||||
@permission-granted="handlePermissionGranted"
|
||||
/>
|
||||
|
||||
<!-- Revoke Confirmation Dialog -->
|
||||
<AlertDialog :open="showRevokeDialog" @update:open="showRevokeDialog = $event">
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>Revoke Permission?</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
Are you sure you want to revoke this permission? The user will immediately lose access.
|
||||
<div v-if="permissionToRevoke" class="mt-4 p-4 bg-muted rounded-md">
|
||||
<p class="font-medium">Permission Details:</p>
|
||||
<p class="text-sm mt-2">
|
||||
<strong>User:</strong> {{ permissionToRevoke.user_id }}
|
||||
</p>
|
||||
<p class="text-sm">
|
||||
<strong>Account:</strong> {{ getAccountName(permissionToRevoke.account_id) }}
|
||||
</p>
|
||||
<p class="text-sm">
|
||||
<strong>Type:</strong> {{ getPermissionLabel(permissionToRevoke.permission_type) }}
|
||||
</p>
|
||||
</div>
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
||||
<AlertDialogAction @click="revokePermission" class="bg-destructive text-destructive-foreground hover:bg-destructive/90">
|
||||
Revoke
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
</div>
|
||||
</template>
|
||||
Loading…
Add table
Add a link
Reference in a new issue