How-To: Implementing Role-Based Access Control with usePermissions
¶
The usePermissions
composable provides a robust system for implementing role-based access control in VC-Shell applications. This guide demonstrates how to effectively control access to features, UI elements, and navigation based on user permissions in your application components.
Prerequisites¶
- Understanding of Vue 3 Composition API and template syntax.
- Familiarity with the
usePermissions
composable (see usePermissions API Reference). - Basic knowledge of VC-Shell's application architecture and user management.
- Understanding of role-based access control concepts and security patterns.
Core Concept¶
The Permission system provides two main approaches for permission checks:
- Composable Usage: Using
usePermissions()
in TypeScript code - Template Helper: Using the global
$hasAccess
function directly in Vue templates - Automatic Administrator Access: Administrators automatically bypass all permission checks
- Flexible Permission Types: Supports single permissions, multiple permissions (OR logic), and conditional access
The system integrates seamlessly with VC-Shell's navigation, dashboard, and UI components.
import { usePermissions } from '@vc-shell/framework';
const { hasAccess } = usePermissions();
// Single permission check
if (hasAccess('create-products')) {
// User can create products
}
// Multiple permissions (OR logic)
if (hasAccess(['edit-products', 'manage-products'])) {
// User has either permission
}
Implementation Strategies¶
1. Component Template Permission Control¶
Control visibility of UI elements based on user permissions:
<!-- ProductManagement.vue -->
<template>
<div class="product-management">
<!-- Create button - only for users with create permission -->
<VcButton
v-if="$hasAccess('create-products')"
@click="createProduct"
>
Create Product
</VcButton>
<!-- Product list with conditional actions -->
<VcTable :items="products">
<template #actions="{ item }">
<VcButton
v-if="$hasAccess('edit-products')"
size="sm"
@click="editProduct(item)"
>
Edit
</VcButton>
<VcButton
v-if="$hasAccess('delete-products')"
size="sm"
variant="danger"
@click="deleteProduct(item)"
>
Delete
</VcButton>
<VcButton
v-if="$hasAccess(['publish-products', 'manage-products'])"
size="sm"
variant="outline"
@click="publishProduct(item)"
>
Publish
</VcButton>
</template>
</VcTable>
<!-- Admin-only section -->
<div v-if="$hasAccess('admin-access')" class="admin-section">
<h3>Administrative Actions</h3>
<VcButton @click="bulkOperations">Bulk Operations</VcButton>
<VcButton @click="systemSettings">System Settings</VcButton>
</div>
<!-- Conditional content based on multiple permissions -->
<div v-if="$hasAccess('view-analytics') && $hasAccess('view-reports')">
<ProductAnalytics />
</div>
</div>
</template>
<script lang="ts" setup>
import { usePermissions } from '@vc-shell/framework';
const { hasAccess } = usePermissions();
function createProduct() {
// Additional permission check in logic if needed
if (!hasAccess('create-products')) {
notification.error('Permission denied');
return;
}
// Create product logic
}
function deleteProduct(product: Product) {
// Multiple permission check example
if (!hasAccess(['delete-products', 'admin-access'])) {
notification.error('Insufficient permissions');
return;
}
// Delete logic
}
</script>
2. Computed Properties for Complex Permission Logic¶
Use computed properties for reactive permission checks:
<!-- UserManagement.vue -->
<template>
<div class="user-management">
<div class="user-actions">
<VcButton
v-if="canCreateUsers"
@click="createUser"
>
Create User
</VcButton>
<VcButton
v-if="canManageRoles"
@click="manageRoles"
>
Manage Roles
</VcButton>
</div>
<VcTable :items="users">
<template #actions="{ item }">
<VcButton
v-if="canEditUser(item)"
size="sm"
@click="editUser(item)"
>
Edit
</VcButton>
<VcButton
v-if="canDeleteUser(item)"
size="sm"
variant="danger"
@click="deleteUser(item)"
>
Delete
</VcButton>
</template>
</VcTable>
</div>
</template>
<script lang="ts" setup>
import { computed } from 'vue';
import { usePermissions } from '@vc-shell/framework';
const { hasAccess } = usePermissions();
// Computed properties for permissions
const canCreateUsers = computed(() => hasAccess('create-users'));
const canManageRoles = computed(() => hasAccess(['manage-roles', 'admin-access']));
// Function-based permission checks for dynamic data
function canEditUser(user: User) {
if (hasAccess('admin-access')) return true;
if (hasAccess('edit-users')) return true;
if (hasAccess('edit-own-profile') && user.id === currentUser.value?.id) return true;
return false;
}
function canDeleteUser(user: User) {
return hasAccess(['delete-users', 'admin-access']) && user.id !== currentUser.value?.id;
}
</script>
3. Blade Component Permission Control¶
Control blade access and functionality based on permissions:
<!-- ProductDetailsBlade.vue -->
<script lang="ts" setup>
import { computed } from 'vue';
import { usePermissions } from '@vc-shell/framework';
// Define blade permissions
defineOptions({
name: 'ProductDetails',
url: '/product',
permissions: 'view-products' // Required to open this blade
});
const { hasAccess } = usePermissions();
// Internal permission checks for blade functionality
const canEdit = computed(() => hasAccess('edit-products'));
const canDelete = computed(() => hasAccess('delete-products'));
const canPublish = computed(() => hasAccess(['publish-products', 'admin-access']));
function handleSave() {
if (!hasAccess('edit-products')) {
notification.error('You do not have permission to edit products');
return;
}
// Save logic
}
function handleDelete() {
if (!hasAccess('delete-products')) {
notification.error('You do not have permission to delete products');
return;
}
// Delete logic
}
</script>
<template>
<VcBlade title="Product Details">
<div class="product-form">
<!-- Form fields -->
<VcInput v-model="product.name" label="Product Name" :readonly="!canEdit" />
<!-- Action buttons based on permissions -->
<div class="actions">
<VcButton
v-if="canEdit"
@click="handleSave"
>
Save
</VcButton>
<VcButton
v-if="canPublish"
variant="outline"
@click="handlePublish"
>
Publish
</VcButton>
<VcButton
v-if="canDelete"
variant="danger"
@click="handleDelete"
>
Delete
</VcButton>
</div>
</div>
</VcBlade>
</template>
4. Menu Registration with Permissions¶
Register application menu items with permission requirements:
// bootstrap.ts
import { addMenuItem } from '@vc-shell/framework';
export function setupApplicationMenu() {
// Public menu item (no permissions required)
addMenuItem({
id: 'dashboard',
title: 'Dashboard',
icon: 'fas fa-home',
priority: 0,
url: '/'
});
// Permission-protected menu items
addMenuItem({
id: 'products',
title: 'Products',
icon: 'fas fa-box',
priority: 10,
url: '/products',
permissions: 'view-products' // Single permission
});
addMenuItem({
id: 'orders',
title: 'Orders',
icon: 'fas fa-shopping-cart',
priority: 20,
url: '/orders',
permissions: ['view-orders', 'manage-orders'] // Multiple permissions (OR)
});
addMenuItem({
id: 'users',
title: 'User Management',
icon: 'fas fa-users',
priority: 30,
url: '/users',
permissions: 'manage-users'
});
addMenuItem({
id: 'admin',
title: 'Administration',
icon: 'fas fa-cog',
priority: 100,
url: '/admin',
permissions: 'admin-access' // Admin only
});
}
5. Dashboard Widget Permission Control¶
Control dashboard widget visibility using permissions:
// dashboard-setup.ts
import { registerDashboardWidget } from '@vc-shell/framework';
import SalesWidget from './widgets/SalesWidget.vue';
import UserAnalyticsWidget from './widgets/UserAnalyticsWidget.vue';
import AdminWidget from './widgets/AdminWidget.vue';
export function setupDashboardWidgets() {
// Widget visible to all users
registerDashboardWidget({
id: 'welcome',
name: 'Welcome',
component: WelcomeWidget,
size: { width: 2, height: 1 },
position: { x: 0, y: 0 }
});
// Sales widget - requires view-sales permission
registerDashboardWidget({
id: 'sales-overview',
name: 'Sales Overview',
component: SalesWidget,
size: { width: 3, height: 2 },
position: { x: 2, y: 0 },
permissions: ['view-sales-data'] // Only users with this permission see the widget
});
// Analytics widget - requires multiple permissions
registerDashboardWidget({
id: 'user-analytics',
name: 'User Analytics',
component: UserAnalyticsWidget,
size: { width: 2, height: 2 },
position: { x: 0, y: 1 },
permissions: ['view-analytics', 'view-user-data'] // OR logic
});
// Admin-only widget
registerDashboardWidget({
id: 'admin-panel',
name: 'Admin Panel',
component: AdminWidget,
size: { width: 4, height: 1 },
position: { x: 0, y: 3 },
permissions: 'admin-access'
});
}
6. Composable with Permission Checks¶
Create composables that integrate permission checking:
// useProductOperations.ts
import { usePermissions } from '@vc-shell/framework';
export function useProductOperations() {
const { hasAccess } = usePermissions();
function createProduct(productData: ProductData) {
if (!hasAccess('create-products')) {
throw new Error('Permission denied: create-products required');
}
// Create product logic
return api.createProduct(productData);
}
function updateProduct(id: string, productData: ProductData) {
if (!hasAccess('edit-products')) {
throw new Error('Permission denied: edit-products required');
}
// Update product logic
return api.updateProduct(id, productData);
}
function deleteProduct(id: string) {
if (!hasAccess(['delete-products', 'admin-access'])) {
throw new Error('Permission denied: delete-products or admin-access required');
}
// Delete product logic
return api.deleteProduct(id);
}
function publishProduct(id: string) {
if (!hasAccess(['publish-products', 'admin-access'])) {
throw new Error('Permission denied: publish-products or admin-access required');
}
// Publish product logic
return api.publishProduct(id);
}
// Return permission-checked functions
return {
createProduct: hasAccess('create-products') ? createProduct : null,
updateProduct: hasAccess('edit-products') ? updateProduct : null,
deleteProduct: hasAccess(['delete-products', 'admin-access']) ? deleteProduct : null,
publishProduct: hasAccess(['publish-products', 'admin-access']) ? publishProduct : null,
// Permission flags for UI
canCreate: hasAccess('create-products'),
canEdit: hasAccess('edit-products'),
canDelete: hasAccess(['delete-products', 'admin-access']),
canPublish: hasAccess(['publish-products', 'admin-access'])
};
}
7. Router Navigation Guards¶
Implement route-level permission protection for regular pages (not blades). When adding routes, it's important to place them correctly in your routing configuration. Pages that should appear within the main application layout (with the shell's header, sidebar, etc.) should be added as children
of the main App
component route. Standalone pages like Login
are defined at the top level.
Blades automatically handle permissions through defineOptions
, which are processed by the framework and added to meta.permissions
internally.
// router/index.ts
import { createRouter, createWebHistory } from 'vue-router';
import { usePermissions, App, Login } from '@vc-shell/framework';
const router = createRouter({
history: createWebHistory(),
routes: [
// Main application route with shared layout
{
path: '/',
component: App,
name: 'App',
meta: {
root: true,
},
// Routes that should render inside the main App layout go here
children: [
{
path: '', // Default child route, often the dashboard
name: 'Dashboard',
component: () => import('@/pages/Dashboard.vue'),
// No permissions required for dashboard
},
{
path: 'reports', // resolves to /reports
name: 'Reports',
component: () => import('@/pages/Reports.vue'),
meta: {
permissions: ['view-reports', 'view-analytics'] // User needs ANY of these
}
},
{
path: 'settings', // resolves to /settings
name: 'Settings',
component: () => import('@/pages/Settings.vue'),
meta: {
permissions: 'admin-access'
}
},
],
},
// Standalone pages (like Login) which do not use the main App layout
{
path: '/login',
name: 'Login',
component: Login,
},
// Note: Blade routes are handled automatically by the framework.
// They use permissions from defineOptions, so no manual route config is needed for them.
]
});
// Navigation guard for permission checking (applies to regular pages only)
router.beforeEach((to, from, next) => {
const { hasAccess } = usePermissions();
// Check if route requires permissions
if (to.meta.permissions) {
if (hasAccess(to.meta.permissions as string | string[])) {
next();
} else {
// Redirect to unauthorized page or show error
next({ name: 'Unauthorized' });
}
} else {
next();
}
});
export default router;
Important Notes:
- Nested vs. Top-level Routes: As shown above, place pages intended to be inside your main application UI as
children
of theApp
route. Standalone pages (e.g., authentication) are placed at the top level. - Blades: Use
permissions
indefineOptions
- framework handles route protection automatically. - Regular Pages: Require manual
meta.permissions
configuration and navigation guards as shown in the example. - Framework Integration: Blade permissions from
defineOptions
are automatically converted tometa.permissions
by the framework.
8. Form Field Conditional Rendering¶
Control form field visibility based on permissions:
<!-- ProductForm.vue -->
<template>
<VcForm @submit="handleSubmit">
<!-- Basic fields visible to all users with view permission -->
<VcInput
v-model="product.name"
label="Product Name"
:readonly="!canEdit"
/>
<VcTextarea
v-model="product.description"
label="Description"
:readonly="!canEdit"
/>
<!-- Price field - only for users with price management permission -->
<VcInput
v-if="$hasAccess('manage-prices')"
v-model="product.price"
label="Price"
type="number"
:readonly="!canEdit"
/>
<!-- Internal notes - admin only -->
<VcTextarea
v-if="$hasAccess('admin-access')"
v-model="product.internalNotes"
label="Internal Notes"
/>
<!-- Inventory section - requires inventory permission -->
<div v-if="$hasAccess('manage-inventory')" class="inventory-section">
<h3>Inventory Management</h3>
<VcInput
v-model="product.stockQuantity"
label="Stock Quantity"
type="number"
/>
<VcInput
v-model="product.reorderLevel"
label="Reorder Level"
type="number"
/>
</div>
<!-- Action buttons -->
<div class="form-actions">
<VcButton
v-if="canEdit"
type="submit"
>
Save
</VcButton>
<VcButton
v-if="$hasAccess('publish-products')"
variant="outline"
@click="handlePublish"
>
Save & Publish
</VcButton>
</div>
</VcForm>
</template>
<script lang="ts" setup>
import { computed } from 'vue';
import { usePermissions } from '@vc-shell/framework';
const { hasAccess } = usePermissions();
const canEdit = computed(() => hasAccess(['edit-products', 'admin-access']));
function handleSubmit() {
if (!canEdit.value) {
notification.error('You do not have permission to edit products');
return;
}
// Submit logic
}
function handlePublish() {
if (!hasAccess('publish-products')) {
notification.error('You do not have permission to publish products');
return;
}
// Publish logic
}
</script>
Best Practices¶
-
Template vs Script Usage: Use
$hasAccess
in templates for simple visibility control andhasAccess
in script for complex logic. -
Permission Naming: Use clear, descriptive permission names following a consistent pattern (e.g.,
action-resource
format likeedit-products
,view-orders
). -
Multiple Permissions: When using arrays, remember it implements OR logic - user needs ANY of the permissions.
-
Administrator Handling: The system automatically grants administrators access to everything - no need to check for admin role explicitly.
-
Early Returns: Check permissions early in functions to avoid unnecessary processing.
-
User Feedback: Always provide clear feedback when users lack permissions using notifications or disabled states.
-
Computed Properties: Use computed properties for permissions that affect reactive UI elements.
-
Defensive Programming: Always validate permissions in business logic, not just in the UI.