Add record access strategy

This commit is contained in:
Francisco Gaona
2026-01-05 07:48:22 +01:00
parent 838a010fb2
commit 16907aadf8
97 changed files with 11350 additions and 208 deletions

View File

@@ -0,0 +1,231 @@
<template>
<div class="min-h-screen bg-background">
<NuxtLayout name="default">
<main class="container mx-auto px-4 py-8">
<div class="flex items-center justify-between">
<div>
<Button variant="ghost" size="sm" @click="navigateTo('/setup/roles')" class="mb-2">
Back to Roles
</Button>
<h1 class="text-3xl font-bold">{{ role?.name || 'Role' }}</h1>
<p class="text-muted-foreground">{{ role?.description || 'No description' }}</p>
</div>
</div>
<div v-if="loading" class="flex items-center justify-center py-12">
<div class="animate-spin rounded-full h-8 w-8 border-b-2 border-primary"></div>
</div>
<Tabs v-else default-value="details" class="w-full">
<TabsList>
<TabsTrigger value="details">Details</TabsTrigger>
<TabsTrigger value="users">Users</TabsTrigger>
</TabsList>
<TabsContent value="details" class="mt-6">
<Card>
<CardHeader>
<CardTitle>Role Information</CardTitle>
</CardHeader>
<CardContent class="space-y-4">
<div class="grid grid-cols-2 gap-4">
<div>
<Label class="text-muted-foreground">Name</Label>
<p class="font-medium">{{ role?.name }}</p>
</div>
<div>
<Label class="text-muted-foreground">Guard</Label>
<Badge variant="outline">{{ role?.guardName || 'tenant' }}</Badge>
</div>
<div class="col-span-2">
<Label class="text-muted-foreground">Description</Label>
<p class="font-medium">{{ role?.description || 'No description' }}</p>
</div>
<div>
<Label class="text-muted-foreground">Created At</Label>
<p class="font-medium">{{ formatDate(role?.createdAt) }}</p>
</div>
<div>
<Label class="text-muted-foreground">Updated At</Label>
<p class="font-medium">{{ formatDate(role?.updatedAt) }}</p>
</div>
</div>
</CardContent>
</Card>
</TabsContent>
<TabsContent value="users" class="mt-6">
<Card>
<CardHeader>
<div class="flex items-center justify-between">
<div>
<CardTitle>Assigned Users</CardTitle>
<CardDescription>Manage user assignments for this role</CardDescription>
</div>
<Button @click="showAddUserDialog = true" size="sm">
<Plus class="mr-2 h-4 w-4" />
Add User
</Button>
</div>
</CardHeader>
<CardContent>
<div v-if="roleUsers.length === 0" class="text-center py-8 text-muted-foreground">
No users assigned. Add users to grant them this role.
</div>
<div v-else class="space-y-2">
<div
v-for="user in roleUsers"
:key="user.id"
class="flex items-center justify-between p-3 border rounded-lg"
>
<div>
<p class="font-medium">{{ getUserName(user) }}</p>
<p class="text-sm text-muted-foreground">{{ user.email }}</p>
</div>
<Button variant="ghost" size="sm" @click="removeUser(user.id)">
<X class="h-4 w-4" />
</Button>
</div>
</div>
</CardContent>
</Card>
</TabsContent>
</Tabs>
<!-- Add User Dialog -->
<Dialog v-model:open="showAddUserDialog">
<DialogContent>
<DialogHeader>
<DialogTitle>Add User</DialogTitle>
<DialogDescription>
Select a user to assign this role
</DialogDescription>
</DialogHeader>
<div class="space-y-4">
<div class="space-y-2">
<Label>Available Users</Label>
<Select v-model="selectedUserId" @update:model-value="(value) => selectedUserId = value">
<SelectTrigger>
<SelectValue placeholder="Choose a user" />
</SelectTrigger>
<SelectContent>
<SelectItem v-for="user in availableUsers" :key="user.id" :value="user.id">
{{ getUserName(user) }} ({{ user.email }})
</SelectItem>
</SelectContent>
</Select>
</div>
</div>
<DialogFooter>
<Button variant="outline" @click="showAddUserDialog = false">Cancel</Button>
<Button @click="addUser" :disabled="!selectedUserId">
Add User
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</main>
</NuxtLayout>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted, computed } from 'vue';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '~/components/ui/card';
import { Button } from '~/components/ui/button';
import { Tabs, TabsContent, TabsList, TabsTrigger } from '~/components/ui/tabs';
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from '~/components/ui/dialog';
import { Label } from '~/components/ui/label';
import { Badge } from '~/components/ui/badge';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '~/components/ui/select';
import { Plus, X } from 'lucide-vue-next';
definePageMeta({
layout: 'default',
});
const route = useRoute();
const { api } = useApi();
const { toast } = useToast();
const loading = ref(true);
const role = ref<any>(null);
const roleUsers = ref<any[]>([]);
const allUsers = ref<any[]>([]);
const showAddUserDialog = ref(false);
const selectedUserId = ref('');
const availableUsers = computed(() => {
const assignedIds = new Set(roleUsers.value.map(u => u.id));
return allUsers.value.filter(u => !assignedIds.has(u.id));
});
const loadRole = async () => {
try {
loading.value = true;
const roleId = route.params.id;
const response = await api.get(`/setup/roles/${roleId}`);
role.value = response;
roleUsers.value = response.users || [];
} catch (error: any) {
console.error('Failed to load role:', error);
toast.error('Failed to load role');
} finally {
loading.value = false;
}
};
const loadAllUsers = async () => {
try {
const response = await api.get('/setup/users');
allUsers.value = response || [];
} catch (error: any) {
console.error('Failed to load users:', error);
}
};
const addUser = async () => {
if (!selectedUserId.value) return;
try {
await api.post(`/setup/roles/${route.params.id}/users`, {
userId: selectedUserId.value,
});
toast.success('User added successfully');
showAddUserDialog.value = false;
selectedUserId.value = '';
await loadRole();
} catch (error: any) {
console.error('Failed to add user:', error);
toast.error(error.message || 'Failed to add user');
}
};
const removeUser = async (userId: string) => {
try {
await api.delete(`/setup/roles/${route.params.id}/users/${userId}`);
toast.success('User removed successfully');
await loadRole();
} catch (error: any) {
console.error('Failed to remove user:', error);
toast.error(error.message || 'Failed to remove user');
}
};
const getUserName = (user: any) => {
if (!user) return 'Unknown';
if (user.firstName || user.lastName) {
return [user.firstName, user.lastName].filter(Boolean).join(' ');
}
return user.email || 'Unknown';
};
const formatDate = (date: string) => {
if (!date) return 'N/A';
return new Date(date).toLocaleDateString();
};
onMounted(async () => {
await Promise.all([loadRole(), loadAllUsers()]);
});
</script>

View File

@@ -0,0 +1,285 @@
<template>
<div class="min-h-screen bg-background">
<NuxtLayout name="default">
<main class="container mx-auto px-4 py-8">
<div class="flex items-center justify-between">
<div>
<h1 class="text-3xl font-bold">Roles</h1>
<p class="text-muted-foreground">Manage roles and permissions</p>
</div>
<Button @click="showCreateDialog = true">
<Plus class="mr-2 h-4 w-4" />
New Role
</Button>
</div>
<div class="border rounded-lg">
<Table>
<TableHeader>
<TableRow>
<TableHead>Name</TableHead>
<TableHead>Description</TableHead>
<TableHead>Guard</TableHead>
<TableHead>Users</TableHead>
<TableHead>Created</TableHead>
<TableHead class="text-right">Actions</TableHead>
</TableRow>
</TableHeader>
<TableBody>
<TableRow v-if="loading">
<TableCell :colspan="6" class="text-center py-8">
<div class="flex items-center justify-center">
<div class="animate-spin rounded-full h-8 w-8 border-b-2 border-primary"></div>
</div>
</TableCell>
</TableRow>
<TableRow v-else-if="roles.length === 0">
<TableCell :colspan="6" class="text-center py-8 text-muted-foreground">
No roles found. Create your first role to get started.
</TableCell>
</TableRow>
<TableRow v-else v-for="role in roles" :key="role.id" class="cursor-pointer hover:bg-muted/50" @click="navigateTo(`/setup/roles/${role.id}`)">
<TableCell class="font-medium">{{ role.name }}</TableCell>
<TableCell>{{ role.description || 'No description' }}</TableCell>
<TableCell>
<Badge variant="outline">{{ role.guardName || 'tenant' }}</Badge>
</TableCell>
<TableCell>
{{ role.userCount || 0 }} users
</TableCell>
<TableCell>{{ formatDate(role.createdAt) }}</TableCell>
<TableCell class="text-right" @click.stop>
<div class="flex items-center justify-end gap-1">
<Button variant="ghost" size="icon" @click="navigateTo(`/setup/roles/${role.id}`)">
<Eye class="h-4 w-4" />
</Button>
<Button variant="ghost" size="icon" @click="openEditDialog(role)">
<Edit class="h-4 w-4" />
</Button>
<Button variant="ghost" size="icon" @click="openDeleteDialog(role)">
<Trash2 class="h-4 w-4" />
</Button>
</div>
</TableCell>
</TableRow>
</TableBody>
</Table>
</div>
<!-- Create Role Dialog -->
<Dialog v-model:open="showCreateDialog">
<DialogContent>
<DialogHeader>
<DialogTitle>Create New Role</DialogTitle>
<DialogDescription>
Add a new role to the system
</DialogDescription>
</DialogHeader>
<div class="space-y-4">
<div class="space-y-2">
<Label for="name">Name</Label>
<Input id="name" v-model="newRole.name" placeholder="Sales Manager" />
</div>
<div class="space-y-2">
<Label for="description">Description (Optional)</Label>
<Input id="description" v-model="newRole.description" placeholder="Manages sales team and deals" />
</div>
<div class="space-y-2">
<Label for="guardName">Guard Name</Label>
<Select v-model="newRole.guardName" @update:model-value="(value) => newRole.guardName = value">
<SelectTrigger>
<SelectValue placeholder="Select guard" />
</SelectTrigger>
<SelectContent>
<SelectItem value="tenant">Tenant</SelectItem>
<SelectItem value="central">Central</SelectItem>
</SelectContent>
</Select>
</div>
</div>
<DialogFooter>
<Button variant="outline" @click="showCreateDialog = false">Cancel</Button>
<Button @click="createRole" :disabled="!newRole.name">
Create Role
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
<!-- Edit Role Dialog -->
<Dialog v-model:open="showEditDialog">
<DialogContent>
<DialogHeader>
<DialogTitle>Edit Role</DialogTitle>
<DialogDescription>
Update role information
</DialogDescription>
</DialogHeader>
<div class="space-y-4">
<div class="space-y-2">
<Label for="edit-name">Name</Label>
<Input id="edit-name" v-model="editRole.name" placeholder="Role name" />
</div>
<div class="space-y-2">
<Label for="edit-description">Description</Label>
<Input id="edit-description" v-model="editRole.description" placeholder="Role description" />
</div>
<div class="space-y-2">
<Label for="edit-guardName">Guard Name</Label>
<Select v-model="editRole.guardName" @update:model-value="(value) => editRole.guardName = value">
<SelectTrigger>
<SelectValue placeholder="Select guard" />
</SelectTrigger>
<SelectContent>
<SelectItem value="tenant">Tenant</SelectItem>
<SelectItem value="central">Central</SelectItem>
</SelectContent>
</Select>
</div>
</div>
<DialogFooter>
<Button variant="outline" @click="showEditDialog = false">Cancel</Button>
<Button @click="updateRole" :disabled="!editRole.name">
Update Role
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
<!-- Delete Confirmation Dialog -->
<Dialog v-model:open="showDeleteDialog">
<DialogContent>
<DialogHeader>
<DialogTitle>Delete Role</DialogTitle>
<DialogDescription>
Are you sure you want to delete this role? This action cannot be undone.
</DialogDescription>
</DialogHeader>
<DialogFooter>
<Button variant="outline" @click="showDeleteDialog = false">Cancel</Button>
<Button variant="destructive" @click="deleteRole">
Delete Role
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</main>
</NuxtLayout>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted } from 'vue';
import { Button } from '~/components/ui/button';
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '~/components/ui/table';
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from '~/components/ui/dialog';
import { Input } from '~/components/ui/input';
import { Label } from '~/components/ui/label';
import { Badge } from '~/components/ui/badge';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '~/components/ui/select';
import { Plus, Eye, Edit, Trash2 } from 'lucide-vue-next';
definePageMeta({
layout: 'default',
});
const { api } = useApi();
const { toast } = useToast();
const loading = ref(true);
const roles = ref<any[]>([]);
const showCreateDialog = ref(false);
const showEditDialog = ref(false);
const showDeleteDialog = ref(false);
const newRole = ref({
name: '',
description: '',
guardName: 'tenant',
});
const editRole = ref({
id: '',
name: '',
description: '',
guardName: 'tenant',
});
const roleToDelete = ref<any>(null);
const loadRoles = async () => {
try {
loading.value = true;
const response = await api.get('/setup/roles');
roles.value = response || [];
} catch (error: any) {
console.error('Failed to load roles:', error);
toast.error('Failed to load roles');
} finally {
loading.value = false;
}
};
const createRole = async () => {
try {
await api.post('/setup/roles', newRole.value);
toast.success('Role created successfully');
showCreateDialog.value = false;
newRole.value = { name: '', description: '', guardName: 'tenant' };
await loadRoles();
} catch (error: any) {
console.error('Failed to create role:', error);
toast.error(error.message || 'Failed to create role');
}
};
const openEditDialog = (role: any) => {
editRole.value = {
id: role.id,
name: role.name,
description: role.description || '',
guardName: role.guardName || 'tenant',
};
showEditDialog.value = true;
};
const updateRole = async () => {
try {
await api.patch(`/setup/roles/${editRole.value.id}`, {
name: editRole.value.name,
description: editRole.value.description,
guardName: editRole.value.guardName,
});
toast.success('Role updated successfully');
showEditDialog.value = false;
await loadRoles();
} catch (error: any) {
console.error('Failed to update role:', error);
toast.error(error.message || 'Failed to update role');
}
};
const openDeleteDialog = (role: any) => {
roleToDelete.value = role;
showDeleteDialog.value = true;
};
const deleteRole = async () => {
try {
await api.delete(`/setup/roles/${roleToDelete.value.id}`);
toast.success('Role deleted successfully');
showDeleteDialog.value = false;
roleToDelete.value = null;
await loadRoles();
} catch (error: any) {
console.error('Failed to delete role:', error);
toast.error(error.message || 'Failed to delete role');
}
};
const formatDate = (date: string) => {
if (!date) return 'N/A';
return new Date(date).toLocaleDateString();
};
onMounted(() => {
loadRoles();
});
</script>