Permission-Based Access Control in Vue Using JWT, Directives & Route Guards

In modern web applications, controlling what each user can see and do is crucial for both security and usability. Whether you’re building a SaaS dashboard or an internal tool, role-based and permission-based access control is a must-have feature.

In this blog, I’ll walk you through how I implemented permission-based access control in a Vue 3 application using:

  • JWT tokens from the backend
  • Custom directives for disabling UI
  • Route guards for blocking unauthorized navigation

This setup provides robust control without cluttering your components.

The Goal

We want to:

✅ Decode user permissions from a JWT token
✅ Enable/disable UI actions based on those permissions
✅ Show tooltips when features are disabled
✅ Hide restricted features entirely
✅ Block unauthorised routes via router guards

Permissions Design

The backend sends a JWT token with a permissions object embedded:

{
"permissions": {
"dashboardAnalytics": "full",
"projectSettings": "readonly",
"userManagement": "denied"
}
}

Each permission can be:

  • full – full access
  • readonly – can view, not modify
  • denied – cannot access at all

1. Decoding JWT & Managing Permissions

Store the permissions in a reactive ref:

// @utils/permission.ts
import { ref } from 'vue'

export const permissionList = ref<Record<string, string>>({})
export const getAllPermissions = (() => {
const apiToken = document.cookie
.split('; ')
.find(row => row.startsWith('api_token='))
?.split('=')[1]
if (apiToken) {
const [, payload] = apiToken.split('.')
if (payload) {
const decodedPayload = JSON.parse(
atob(payload.replace(/-/g, '+').replace(/_/g, '/'))
)
permissionList.value = decodedPayload.permissions || {}
}
}
})()
export const hasPermission = (permission: string): boolean => {
const permValue = permissionList.value?.[permission]
if (permValue === undefined) return true
return permValue === 'full'
}
export const hasPermissionDenied = (permission: string): boolean => {
return permissionList.value?.[permission] === 'denied'
}

The _getAllPermissions()_ function should be called during app initialization (e.g., in _main.ts_ or after login).

2. Vue Directive: v-permission

We created a custom Vue directive to apply permission logic declaratively to components.

// @directives/permission.ts
import { hasPermission } from '@utils/permission'

export default {
mounted(el, binding) {
const permission = typeof binding.value === 'string' ? binding.value : ''
const tooltipMessage = 'You do not have permission to access this feature.'
if (permission && !hasPermission(permission)) {
el.setAttribute('disabled', 'true')
el.classList.add('tw-cursor-not-allowed', '!tw-opacity-50')
el.style.cursor = 'not-allowed'
let tooltipEl: HTMLElement | null = null
const showTooltip = () => {
tooltipEl = document.createElement('div')
tooltipEl.classList.add('custom-tooltip')
tooltipEl.textContent = tooltipMessage
document.body.appendChild(tooltipEl)
const rect = el.getBoundingClientRect()
const tooltipHeight = tooltipEl.offsetHeight
const spaceAbove = rect.top
tooltipEl.style.top = spaceAbove >= tooltipHeight + 6
? `${rect.top - tooltipHeight - 6 + window.scrollY}px`
: `${rect.bottom + 6 + window.scrollY}px`
tooltipEl.style.left = `${rect.left + rect.width / 2 - tooltipEl.offsetWidth / 2 + window.scrollX}px`
}
const hideTooltip = () => {
if (tooltipEl) {
tooltipEl.remove()
tooltipEl = null
}
}
el.addEventListener('mouseenter', showTooltip)
el.addEventListener('mouseleave', hideTooltip)
el.addEventListener('focus', showTooltip)
el.addEventListener('blur', hideTooltip)
const blocker = (e: Event) => {
e.preventDefault()
e.stopImmediatePropagation()
}
el.addEventListener('click', blocker, true)
el.addEventListener('mousedown', blocker, true)
el.addEventListener('keydown', blocker, true)
}
},
}

Usage in Components

Here’s how you’d use it in your template:

<Button
v-permission="'dashboardAnalytics'"
@click="addWidget"
label="Add Widget"
class="btn"
/>

If the user lacks permission, the button will be:

  • Disabled
  • Semi-transparent
  • Unclickable
  • Showing a tooltip

3. Conditionally Hiding UI with v-if

For fully denied permissions, you can use:

<Button
v-if="!hasPermissionDenied('userManagement')"
@click="openUserManager"
label="Manage Users"
/>

This hides the button completely if access is explicitly denied.

4. Securing Routes with Router Guards

To prevent users from manually navigating to protected routes, we add permission metadata and guard logic.

Sample usage:

{
path: '/:orgId/:projectId/dashboard-analytics',
name: 'dashboard-analytics',
meta: {
layout: 'default',
permission: 'dashboardAnalytics',
},
component: () => import('@pages/analytics/index.vue'),
}

🔐 Global Navigation Guard

// router.ts
import { hasPermissionDenied } from '@utils/permission'

router.beforeEach((to, from, next) => {
const permissionKey = to?.meta?.permission
const permissionDenied = permissionKey && hasPermissionDenied(permissionKey as string)
if (permissionDenied) {
if (to.name !== 'access-denied') {
return next({ name: 'access-denied' })
}
return next()
}
NProgress.start()
next()
})

🚫 Access Denied Route

{
path: '/access-denied',
name: 'access-denied',
component: () => import('@pages/common/AccessDenied.vue'),
meta: {
layout: 'minimal',
},
}

With this setup:

  • Unauthorized users can’t see or interact with restricted UI
  • Routes are blocked at the navigation layer
  • Users get clear feedback via tooltips or redirection
  • Templates stay clean and declarative

This approach strikes a great balance between security, usability, and maintainability. You can easily extend it further to support:

  • Role-based logic
  • Multi-tenant permissions
  • Asynchronous permission loading

Action buttons looks like:

I’d love to hear how you’re handling permissions in your apps! Feel free to leave a comment or share your ideas.

#VueJS #FrontendDevelopment #WebSecurity #JWT #AccessControl #Vue3 #RoleBasedAccess

Accueil - Wiki
Copyright © 2011-2026 iteam. Current version is 2.150.0. UTC+08:00, 2026-02-03 06:01
浙ICP备14020137号-1 $Carte des visiteurs$